package workspace

import (
	"context"
	"errors"
	"log/slog"
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/api"
	"github.com/tidbcloud/AgentTeam/internal/api/dto"
	"github.com/tidbcloud/AgentTeam/internal/api/dto/converter"
	"github.com/tidbcloud/AgentTeam/internal/core/authidentity"
	"github.com/tidbcloud/AgentTeam/internal/core/secrets"
	"github.com/tidbcloud/AgentTeam/internal/mailer"
	storeworkspace "github.com/tidbcloud/AgentTeam/internal/store/workspace"
)

const workspaceInviteTokenTTL = 7 * 24 * time.Hour

// WorkspaceHandler owns workspace membership, invite, and usage routes.
type WorkspaceHandler struct {
	workspaceService WorkspaceService
	planStore        PlanStore
	organizations    OrganizationProvider
	mailer           mailer.Mailer
	publicBaseURL    string
	machineRuntime   MachineRuntime
	realtime         WorkspaceRealtime
	unreadRuntime    WorkspaceUnreadReader
	hostedModels     HostedAvailabilityClient
	meshXEnabled     bool
	hostedPricing    HostedPricingClient
	logger           *slog.Logger
}

// List handles GET /api/v1/workspaces.
// It returns all workspaces the authenticated user has access to, with
// workspace-level unread existence flags.
func (handler *WorkspaceHandler) List(r api.AuthenticatedRequest) (api.Response, error) {
	includeUnread, err := parseWorkspaceListIncludeUnread(r.Gin.Query("includeUnread"))
	if err != nil {
		return api.Response{}, err
	}

	summaries, err := handler.workspaceService.ListUserWorkspaces(r.Context(), r.UserID())
	if err != nil {
		return api.Response{}, err
	}

	workspaces := make([]dto.WorkspaceResponse, 0, len(summaries))
	for _, summary := range summaries {
		workspaces = append(workspaces, presentWorkspace(summary, includeUnread && handler.workspaceHasUnread(summary), handler.meshXEnabled))
	}

	return api.OK(workspaces), nil
}

// Create handles POST /api/v1/workspaces.
// It creates a workspace inside the explicitly selected organization.
func (handler *WorkspaceHandler) Create(r api.AuthenticatedJSON[dto.WorkspaceCreateRequest]) (api.Response, error) {
	orgs, ok := handler.organizationProvider()
	if !ok {
		return api.Response{}, api.NewHTTPError(http.StatusNotFound, "Organization provider not installed")
	}

	request := r.Body
	organizationID := strings.TrimSpace(request.OrganizationID)
	if organizationID == "" {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "organizationId is required")
	}
	name := strings.TrimSpace(request.Name)
	if name == "" {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "name is required")
	}

	created, err := orgs.CreateOrganizationWorkspaceForUser(r.Context(), r.UserID(), organizationID, name, strings.TrimSpace(request.Slug))
	if err != nil {
		return api.Response{}, err
	}
	summary, err := handler.workspaceService.GetWorkspaceForUser(r.Context(), r.UserID(), created.ID)
	if err != nil {
		return api.Response{}, err
	}

	return api.OK(presentWorkspace(summary, false, handler.meshXEnabled)), nil
}

// Update handles PATCH /api/v1/workspaces/:workspaceId.
// It renames the specified workspace and returns the updated workspace summary.
func (handler *WorkspaceHandler) Update(r api.AuthenticatedJSON[dto.WorkspaceUpdateRequest]) (api.Response, error) {
	workspaceID := strings.TrimSpace(r.Gin.Param("workspaceId"))
	if workspaceID == "" {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "workspaceId is required")
	}

	name := strings.TrimSpace(r.Body.Name)
	if name == "" {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "name is required")
	}

	summary, err := handler.workspaceService.UpdateWorkspaceNameForUser(r.Context(), r.UserID(), workspaceID, name)
	if err != nil {
		return api.Response{}, err
	}
	return api.OK(presentWorkspace(summary, handler.workspaceHasUnread(summary), handler.meshXEnabled)), nil
}

func (handler *WorkspaceHandler) workspaceHasUnread(summary storeworkspace.Summary) bool {
	if handler == nil || handler.unreadRuntime == nil {
		return false
	}
	return handler.unreadRuntime.WorkspaceHasUnread(summary.WorkspaceID, summary.MemberID)
}

// Organization handles GET /api/v1/workspaces/:workspaceId/organization.
// It returns the organization that owns the specified workspace for the
// authenticated workspace member.
func (handler *WorkspaceHandler) Organization(r api.AuthenticatedRequest) (api.Response, error) {
	orgs, ok := handler.organizationProvider()
	if !ok {
		return api.Response{}, api.NewHTTPError(http.StatusNotFound, "Organization provider not installed")
	}

	workspaceID := strings.TrimSpace(r.Gin.Param("workspaceId"))
	if workspaceID == "" {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "workspaceId is required")
	}

	if _, err := handler.workspaceService.GetWorkspaceForUser(r.Context(), r.UserID(), workspaceID); err != nil {
		return api.Response{}, err
	}
	organizationSummary, err := orgs.GetWorkspaceOrganizationForUser(r.Context(), r.UserID(), workspaceID)
	if err != nil {
		return api.Response{}, err
	}
	return api.OK(converter.ToOrgSummary(organizationSummary)), nil
}

// CurrentMembers handles GET /api/v1/members for the current workspace.
func (handler *WorkspaceHandler) CurrentMembers(r api.WorkspaceScopedRequest) (api.Response, error) {
	return handler.members(r.AuthenticatedRequest, r.WorkspaceID())
}

func sumUnreadCounts(counts map[string]int64) int64 {
	var total int64
	for _, count := range counts {
		if count > 0 {
			total += count
		}
	}
	return total
}

// Members handles GET /api/v1/workspaces/:workspaceId/members.
// It returns all members of the specified workspace, enriched with organization access sources.
func (handler *WorkspaceHandler) Members(r api.AuthenticatedRequest) (api.Response, error) {
	workspaceID := strings.TrimSpace(r.Gin.Param("workspaceId"))
	if workspaceID == "" {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "workspaceId is required")
	}
	return handler.members(r, workspaceID)
}

func (handler *WorkspaceHandler) members(r api.AuthenticatedRequest, workspaceID string) (api.Response, error) {
	limit, err := parseWorkspaceMemberDirectoryLimit(r.Gin.Query("limit"))
	if err != nil {
		return api.Response{}, err
	}

	return handler.workspaceMemberPage(r.Context(), r.UserID(), workspaceID, storeworkspace.WorkspaceMemberListOptions{
		Limit:  limit,
		Cursor: strings.TrimSpace(r.Gin.Query("cursor")),
		Query:  strings.TrimSpace(r.Gin.Query("q")),
	})
}

// MembersSearch handles POST /api/v1/workspaces/:workspaceId/members/search.
// It searches the workspace member directory with filters supplied in the JSON
// body so large filter sets do not overflow URL limits.
func (handler *WorkspaceHandler) MembersSearch(r api.AuthenticatedJSON[dto.WorkspaceMemberSearchRequest]) (api.Response, error) {
	workspaceID := strings.TrimSpace(r.Gin.Param("workspaceId"))
	if workspaceID == "" {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "workspaceId is required")
	}
	limit, err := parseWorkspaceMemberDirectoryBodyLimit(r.Body.Limit)
	if err != nil {
		return api.Response{}, err
	}

	return handler.workspaceMemberPage(r.Context(), r.UserID(), workspaceID, storeworkspace.WorkspaceMemberListOptions{
		Limit:          limit,
		Cursor:         strings.TrimSpace(r.Body.Cursor),
		Query:          strings.TrimSpace(r.Body.Query),
		ExcludeUserIDs: r.Body.ExcludeUserIDs,
	})
}

func (handler *WorkspaceHandler) workspaceMemberPage(ctx context.Context, userID string, workspaceID string, options storeworkspace.WorkspaceMemberListOptions) (api.Response, error) {
	page, err := handler.workspaceService.ListWorkspaceMembersForUserPage(ctx, userID, workspaceID, options)
	if err != nil {
		return api.Response{}, err
	}
	if err := handler.enrichWorkspaceMemberAccessSources(ctx, userID, workspaceID, page.Members); err != nil {
		return api.Response{}, err
	}

	return api.OK(converter.ToWorkspaceMemberPageResponse(page)), nil
}

// Member handles GET /api/v1/workspaces/:workspaceId/members/:memberId.
// It returns one active human workspace member by canonical member id.
func (handler *WorkspaceHandler) Member(r api.AuthenticatedRequest) (api.Response, error) {
	workspaceID := strings.TrimSpace(r.Gin.Param("workspaceId"))
	memberID := strings.TrimSpace(r.Gin.Param("memberId"))
	if workspaceID == "" || memberID == "" {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "Invalid request")
	}

	member, err := handler.workspaceService.GetWorkspaceMemberForUser(r.Context(), r.UserID(), workspaceID, memberID)
	if err != nil {
		return api.Response{}, err
	}
	members := []storeworkspace.WorkspaceMember{member}
	if err := handler.enrichWorkspaceMemberAccessSources(r.Context(), r.UserID(), workspaceID, members); err != nil {
		return api.Response{}, err
	}
	return api.OK(map[string]dto.WorkspaceMemberResponse{"member": converter.ToWorkspaceMemberResponse(members[0])}), nil
}

// MembersLookup handles POST /api/v1/workspaces/:workspaceId/members:batchLookup.
// It returns active human workspace members for the requested bounded id set.
func (handler *WorkspaceHandler) MembersLookup(r api.AuthenticatedJSON[dto.WorkspaceMemberLookupRequest]) (api.Response, error) {
	workspaceID := strings.TrimSpace(r.Gin.Param("workspaceId"))
	if workspaceID == "" {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "workspaceId is required")
	}
	members, err := handler.workspaceService.LookupWorkspaceMembersForUser(r.Context(), r.UserID(), workspaceID, storeworkspace.WorkspaceMemberLookupOptions{
		MemberIDs: r.Body.MemberIDs,
		UserIDs:   r.Body.UserIDs,
	})
	if err != nil {
		return api.Response{}, err
	}
	if err := handler.enrichWorkspaceMemberAccessSources(r.Context(), r.UserID(), workspaceID, members); err != nil {
		return api.Response{}, err
	}
	return api.OK(map[string][]dto.WorkspaceMemberResponse{"members": converter.ToWorkspaceMemberResponses(members)}), nil
}

func parseWorkspaceInviteListLimit(rawLimit string) (int, error) {
	trimmed := strings.TrimSpace(rawLimit)
	if trimmed == "" {
		return storeworkspace.DefaultWorkspaceInviteListLimit, nil
	}
	limit, err := strconv.Atoi(trimmed)
	if err != nil {
		return 0, api.NewHTTPError(http.StatusBadRequest, "limit must be an integer")
	}
	if limit <= 0 {
		return 0, api.NewHTTPError(http.StatusBadRequest, "limit must be a positive integer")
	}
	if limit > storeworkspace.MaxWorkspaceInviteListLimit {
		return storeworkspace.MaxWorkspaceInviteListLimit, nil
	}
	return limit, nil
}

func parseWorkspaceListIncludeUnread(raw string) (bool, error) {
	switch strings.ToLower(strings.TrimSpace(raw)) {
	case "", "1", "t", "true", "yes", "y":
		return true, nil
	case "0", "f", "false", "no", "n":
		return false, nil
	default:
		return false, api.NewHTTPError(http.StatusBadRequest, "includeUnread must be a boolean")
	}
}

func parseWorkspaceMemberDirectoryLimit(rawLimit string) (int, error) {
	trimmed := strings.TrimSpace(rawLimit)
	if trimmed == "" {
		return storeworkspace.DefaultWorkspaceMemberDirectoryLimit, nil
	}
	limit, err := strconv.Atoi(trimmed)
	if err != nil {
		return 0, api.NewHTTPError(http.StatusBadRequest, "limit must be an integer")
	}
	if limit <= 0 {
		return 0, api.NewHTTPError(http.StatusBadRequest, "limit must be a positive integer")
	}
	if limit > storeworkspace.MaxWorkspaceMemberDirectoryLimit {
		return storeworkspace.MaxWorkspaceMemberDirectoryLimit, nil
	}
	return limit, nil
}

func parseWorkspaceMemberDirectoryBodyLimit(limit int) (int, error) {
	if limit == 0 {
		return storeworkspace.DefaultWorkspaceMemberDirectoryLimit, nil
	}
	if limit < 0 {
		return 0, api.NewHTTPError(http.StatusBadRequest, "limit must be a positive integer")
	}
	if limit > storeworkspace.MaxWorkspaceMemberDirectoryLimit {
		return storeworkspace.MaxWorkspaceMemberDirectoryLimit, nil
	}
	return limit, nil
}

func (handler *WorkspaceHandler) enrichWorkspaceMemberAccessSources(ctx context.Context, actorUserID string, workspaceID string, members []storeworkspace.WorkspaceMember) error {
	for i := range members {
		members[i].AccessSources = []string{}
	}
	if len(members) == 0 {
		return nil
	}
	if err := handler.workspaceService.RequireWorkspaceMemberManagementForUser(ctx, actorUserID, workspaceID); err != nil {
		if errors.Is(err, api.ErrForbidden) {
			return nil
		}
		return err
	}
	orgs, ok := handler.organizationProvider()
	if !ok {
		return nil
	}
	userIDs := make([]string, 0, len(members))
	for _, member := range members {
		userID := strings.TrimSpace(member.UserID)
		if userID != "" {
			userIDs = append(userIDs, userID)
		}
	}
	accessViews, err := orgs.ListWorkspaceMemberAccessSourcesForUsers(ctx, workspaceID, userIDs)
	if err != nil {
		return err
	}
	accessSourcesByUserID := make(map[string][]string, len(accessViews))
	for _, view := range accessViews {
		accessSourcesByUserID[view.UserID] = append([]string(nil), view.Sources...)
	}
	for i := range members {
		if sources, ok := accessSourcesByUserID[members[i].UserID]; ok {
			members[i].AccessSources = append([]string(nil), sources...)
		}
	}
	return nil
}

// MembersUpdate handles PATCH /api/v1/workspaces/:workspaceId/members/:memberId.
// It updates the role of a member on the specified workspace.
func (handler *WorkspaceHandler) MembersUpdate(r api.AuthenticatedJSON[dto.WorkspaceMemberRoleUpdateRequest]) (api.Response, error) {

	workspaceID := strings.TrimSpace(r.Gin.Param("workspaceId"))
	memberID := strings.TrimSpace(r.Gin.Param("memberId"))
	if workspaceID == "" || memberID == "" {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "Invalid request")
	}

	request := r.Body
	member, err := handler.workspaceService.GetWorkspaceMemberForUser(r.Context(), r.UserID(), workspaceID, memberID)
	if err != nil {
		return api.Response{}, err
	}

	if err := handler.workspaceService.UpdateWorkspaceMemberRoleForUser(r.Context(), r.UserID(), workspaceID, member.UserID, request.Role); err != nil {
		return api.Response{}, err
	}
	if handler.realtime != nil {
		// Role-sensitive Apps, App visibility, and Service Account capabilities
		// are browser projections for the affected viewer. Broadcast the role
		// mutation with its target so other viewers can patch member lists without
		// refetching the whole workspace directory.
		handler.realtime.EmitWorkspaceMemberRoleUpdated(workspaceID, member.UserID, strings.TrimSpace(request.Role))
	}

	return api.OK(dto.EmptyResponse{}), nil
}

// MemberOffboardingImpact handles GET /api/v1/workspaces/:workspaceId/members/by-user/:userId/offboarding-impact.
// It returns the active runtime resources that will be revoked or archived when
// the target human member is removed from the workspace.
func (handler *WorkspaceHandler) MemberOffboardingImpact(r api.AuthenticatedRequest) (api.Response, error) {
	workspaceID := strings.TrimSpace(r.Gin.Param("workspaceId"))
	userID := strings.TrimSpace(r.Gin.Param("userId"))
	if workspaceID == "" || userID == "" {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "Invalid request")
	}

	impact, err := handler.workspaceService.WorkspaceMemberOffboardingImpactForUser(r.Context(), r.UserID(), workspaceID, userID)
	if err != nil {
		return api.Response{}, err
	}
	return api.OK(converter.ToWorkspaceMemberOffboardingImpactResponse(impact)), nil
}

// MembersDelete handles DELETE /api/v1/workspaces/:workspaceId/members/:memberId.
// It removes an active human member from the specified workspace.
func (handler *WorkspaceHandler) MembersDelete(r api.AuthenticatedRequest) (api.Response, error) {

	workspaceID := strings.TrimSpace(r.Gin.Param("workspaceId"))
	memberID := strings.TrimSpace(r.Gin.Param("memberId"))
	if workspaceID == "" || memberID == "" {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "Invalid request")
	}

	member, err := handler.workspaceService.GetWorkspaceMemberForUser(r.Context(), r.UserID(), workspaceID, memberID)
	if err != nil {
		return api.Response{}, err
	}
	revokedMachineIDs, err := handler.workspaceService.RemoveWorkspaceMemberForUser(r.Context(), r.UserID(), workspaceID, member.UserID)
	if err != nil {
		return api.Response{}, err
	}
	for _, machineID := range revokedMachineIDs {
		if handler.machineRuntime != nil {
			handler.machineRuntime.DisconnectMachine(machineID)
		}
	}
	if handler.realtime != nil {
		handler.realtime.DisconnectWorkspaceUser(workspaceID, member.UserID)
		handler.realtime.EmitWorkspaceMembersUpdated(workspaceID)
	}

	return api.OK(dto.EmptyResponse{}), nil
}

// Usage handles GET /api/v1/workspaces/:workspaceId/usage.
// It returns the resource usage statistics for the specified workspace.
func (handler *WorkspaceHandler) Usage(r api.AuthenticatedRequest) (api.Response, error) {

	workspaceID := strings.TrimSpace(r.Gin.Param("workspaceId"))
	if workspaceID == "" {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "workspaceId is required")
	}

	if _, err := handler.workspaceService.GetWorkspaceForUser(r.Context(), r.UserID(), workspaceID); err != nil {
		return api.Response{}, err
	}
	if handler.planStore == nil {
		return api.Response{}, api.NewHTTPError(http.StatusInternalServerError, "Plan store not configured")
	}
	usage, err := handler.planStore.LoadWorkspaceUsageView(r.Context(), workspaceID, r.UserID())
	if err != nil {
		return api.Response{}, err
	}

	return api.OK(converter.ToWorkspaceUsageView(usage)), nil
}

// InvitesList handles GET /api/v1/workspaces/:workspaceId/invites.
// It returns all pending invites for the specified workspace.
func (handler *WorkspaceHandler) InvitesList(r api.AuthenticatedRequest) (api.Response, error) {

	workspaceID := strings.TrimSpace(r.Gin.Param("workspaceId"))
	if workspaceID == "" {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "workspaceId is required")
	}

	limit, err := parseWorkspaceInviteListLimit(r.Gin.Query("limit"))
	if err != nil {
		return api.Response{}, err
	}
	options := storeworkspace.WorkspaceInviteListOptions{
		Limit:  limit,
		Cursor: strings.TrimSpace(r.Gin.Query("cursor")),
	}
	page := storeworkspace.WorkspaceInvitePage{}
	if pager, ok := handler.workspaceService.(interface {
		ListWorkspaceInvitesForUserPage(context.Context, string, string, storeworkspace.WorkspaceInviteListOptions) (storeworkspace.WorkspaceInvitePage, error)
	}); ok {
		var err error
		page, err = pager.ListWorkspaceInvitesForUserPage(r.Context(), r.UserID(), workspaceID, options)
		if err != nil {
			return api.Response{}, err
		}
	} else {
		invites, err := handler.workspaceService.ListWorkspaceInvitesForUser(r.Context(), r.UserID(), workspaceID)
		if err != nil {
			return api.Response{}, err
		}
		page.Invites = invites
	}
	for index := range page.Invites {
		page.Invites[index].InviteLink = mailer.PendingInviteLink(handler.publicBaseURL, page.Invites[index].ID)
	}

	if strings.TrimSpace(r.Gin.Query("limit")) == "" && strings.TrimSpace(r.Gin.Query("cursor")) == "" {
		return api.OK(converter.ToWorkspaceInviteResponses(page.Invites)), nil
	}
	return api.OK(converter.ToWorkspaceInvitePageResponse(page)), nil
}

// InvitesCreate handles POST /api/v1/workspaces/:workspaceId/invites.
// It creates an email- or mobile-bound workspace invite and returns a link for copying.
func (handler *WorkspaceHandler) InvitesCreate(r api.AuthenticatedJSON[dto.WorkspaceInviteCreateRequest]) (api.Response, error) {

	workspaceID := strings.TrimSpace(r.Gin.Param("workspaceId"))
	if workspaceID == "" {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "workspaceId is required")
	}

	rawIdentifier, requestedKind, err := workspaceInviteIdentifierFromRequest(r.Body)
	if err != nil {
		return api.Response{}, err
	}
	identifier, err := authidentity.ResolveInviteIdentifier(rawIdentifier)
	if err != nil {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, err.Error())
	}
	if requestedKind != "" && identifier.Kind != requestedKind {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "invite identifier does not match selected target kind")
	}

	token, err := secrets.NewToken(tokenByteLength)
	if err != nil {
		return api.Response{}, err
	}

	tokenHash := secrets.HashToken(token)
	expiresAt := time.Now().UTC().Add(workspaceInviteTokenTTL)
	var inviteID string
	switch identifier.Kind {
	case authidentity.KindEmail:
		inviteID, err = handler.workspaceService.CreateWorkspaceInvite(r.Context(), r.UserID(), workspaceID, identifier.Value, tokenHash, expiresAt)
	case authidentity.KindMobile:
		inviteID, err = handler.workspaceService.CreateWorkspaceMobileInvite(r.Context(), r.UserID(), workspaceID, identifier.Value, tokenHash, expiresAt)
	default:
		err = api.NewHTTPError(http.StatusBadRequest, "identifier must be a valid email or mobile number")
	}
	if err != nil {
		return api.Response{}, err
	}

	return api.OK(dto.WorkspaceInviteCreateResponse{
		InviteID:   inviteID,
		InviteLink: mailer.PendingInviteLink(handler.publicBaseURL, inviteID),
	}), nil
}

func workspaceInviteIdentifierFromRequest(request dto.WorkspaceInviteCreateRequest) (string, authidentity.Kind, error) {
	identifier := strings.TrimSpace(request.Identifier)
	email := strings.TrimSpace(request.Email)
	mobile := strings.TrimSpace(request.Mobile)
	count := 0
	for _, value := range []string{identifier, email, mobile} {
		if value != "" {
			count++
		}
	}
	switch {
	case count == 0:
		return "", "", api.NewHTTPError(http.StatusBadRequest, "identifier is required")
	case count > 1:
		return "", "", api.NewHTTPError(http.StatusBadRequest, "provide only one invite identifier")
	case identifier != "":
		return identifier, "", nil
	case email != "":
		return email, authidentity.KindEmail, nil
	default:
		return mobile, authidentity.KindMobile, nil
	}
}

// InvitesDelete handles DELETE /api/v1/workspaces/:workspaceId/invites/:inviteId.
// It revokes the specified workspace invite.
func (handler *WorkspaceHandler) InvitesDelete(r api.AuthenticatedRequest) (api.Response, error) {

	workspaceID := strings.TrimSpace(r.Gin.Param("workspaceId"))
	inviteID := strings.TrimSpace(r.Gin.Param("inviteId"))
	if workspaceID == "" || inviteID == "" {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "Invalid request")
	}

	if err := handler.workspaceService.RevokeWorkspaceInviteForUser(r.Context(), r.UserID(), workspaceID, inviteID); err != nil {
		return api.Response{}, err
	}

	return api.OK(dto.EmptyResponse{}), nil
}

func (handler *WorkspaceHandler) organizationProvider() (OrganizationProvider, bool) {
	if handler.organizations == nil {
		return nil, false
	}
	return handler.organizations, true
}
