// Package authflow owns authentication workflows that cross identity,
// workspace, and organization boundaries.
package authflow

import (
	"context"
	"fmt"

	"github.com/tidbcloud/AgentTeam/internal/core/authidentity"
	"github.com/tidbcloud/AgentTeam/internal/store/identity"
	"github.com/tidbcloud/AgentTeam/internal/store/organization"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
	"github.com/tidbcloud/AgentTeam/internal/store/workspace"
)

// RegistrationAllowlistStore is the identity-domain registration allowlist
// surface needed by invite-only registration checks.
type RegistrationAllowlistStore interface {
	ListActiveRegistrationAllowlistPatterns(ctx context.Context) ([]string, error)
}

// WorkspaceInviteStore is the workspace invite surface needed by auth
// registration and invite acceptance flows.
type WorkspaceInviteStore interface {
	AcceptWorkspaceInviteByTokenIfExists(ctx context.Context, user identity.User, token string) (workspace.Summary, bool, error)
	AcceptWorkspaceInviteByIDIfExists(ctx context.Context, user identity.User, inviteID string) (workspace.Summary, bool, error)
	DismissPendingWorkspaceInviteForUser(ctx context.Context, user identity.User, inviteID string) (bool, error)
	FindWorkspaceInviteInfoByID(ctx context.Context, inviteID string) (workspace.WorkspaceInviteInfo, bool, error)
	FindWorkspaceInviteInfoByToken(ctx context.Context, token string) (workspace.WorkspaceInviteInfo, bool, error)
	HasPendingWorkspaceInviteForEmail(ctx context.Context, email string) (bool, error)
	ListPendingWorkspaceInvitesForUser(ctx context.Context, user identity.User) ([]workspace.PendingWorkspaceInvite, error)
}

// OrganizationProvider is the organization-auth surface needed by authflow.
// It intentionally excludes broader organization administration methods.
type OrganizationProvider interface {
	AcceptOrganizationInviteByTokenIfExists(ctx context.Context, user identity.User, token string) (organization.OrganizationInviteAcceptance, bool, error)
	AutoJoinOrganizationsForVerifiedEmail(ctx context.Context, user identity.User) error
	AutoJoinOrganizationsForVerifiedMobile(ctx context.Context, user identity.User) error
	EnsureDefaultWorkspaceForUser(ctx context.Context, user identity.User) error
	FindOrganizationInviteInfoByToken(ctx context.Context, token string) (organization.OrganizationInviteInfo, bool, error)
	HasPendingOrganizationInviteForEmail(ctx context.Context, email string) (bool, error)
}

type OrganizationDefaultWorkspaceReporter interface {
	EnsureDefaultWorkspaceForUserAndReport(ctx context.Context, user identity.User) (organizationID string, created bool, err error)
	MarkDefaultWorkspaceBootstrapCompleted(ctx context.Context, userID string, organizationID string) error
}

type DefaultWorkspacePostCommitProvisioner interface {
	EnsureDefaultWorkspacePostCommit(ctx context.Context, organizationID string, userID string) error
}

type DefaultWorkspaceOptions struct {
	ProvisionOnboardingTrial bool
}

// Dependencies contains the narrow domain/service ports authflow orchestrates.
type Dependencies struct {
	RegistrationAllowlist             RegistrationAllowlistStore
	WorkspaceInvites                  WorkspaceInviteStore
	Organizations                     OrganizationProvider
	DefaultWorkspacePostCommit        DefaultWorkspacePostCommitProvisioner
	RequireDefaultWorkspacePostCommit bool
}

// Service coordinates auth flows across identity, workspace, and organization
// without exposing storefacade or storegraph to API handlers.
type Service struct {
	registrationAllowlist             RegistrationAllowlistStore
	workspaceInvites                  WorkspaceInviteStore
	organizations                     OrganizationProvider
	defaultWorkspacePostCommit        DefaultWorkspacePostCommitProvisioner
	requireDefaultWorkspacePostCommit bool
}

func New(deps Dependencies) *Service {
	return &Service{
		registrationAllowlist:             deps.RegistrationAllowlist,
		workspaceInvites:                  deps.WorkspaceInvites,
		organizations:                     deps.Organizations,
		defaultWorkspacePostCommit:        deps.DefaultWorkspacePostCommit,
		requireDefaultWorkspacePostCommit: deps.RequireDefaultWorkspacePostCommit,
	}
}

// RegistrationAllowedForEmail evaluates the dynamic invite/allowlist portion of
// invite-only registration policy. Static config allowlist decisions are passed
// in by the API layer because that policy belongs to route configuration.
func (service *Service) RegistrationAllowedForEmail(ctx context.Context, email string, staticallyAllowed bool) (bool, error) {
	if staticallyAllowed {
		return true, nil
	}

	if service == nil {
		return false, nil
	}

	if service.registrationAllowlist != nil {
		dbAllowlistPatterns, err := service.registrationAllowlist.ListActiveRegistrationAllowlistPatterns(ctx)
		if err != nil {
			return false, fmt.Errorf("load registration allowlist for email %q: %w", email, err)
		}
		for _, pattern := range dbAllowlistPatterns {
			if authidentity.EmailMatchesAllowlistPattern(email, pattern) {
				return true, nil
			}
		}
	}

	if service.workspaceInvites != nil {
		workspaceInvite, err := service.workspaceInvites.HasPendingWorkspaceInviteForEmail(ctx, email)
		if err != nil {
			return false, fmt.Errorf("check pending workspace invite for registration email %q: %w", email, err)
		}
		if workspaceInvite {
			return true, nil
		}
	}

	if service.organizations == nil {
		return false, nil
	}
	organizationInvite, err := service.organizations.HasPendingOrganizationInviteForEmail(ctx, email)
	if err != nil {
		return false, fmt.Errorf("check pending organization invite for registration email %q: %w", email, err)
	}
	return organizationInvite, nil
}

func (service *Service) EnsureDefaultWorkspace(ctx context.Context, user identity.User, options DefaultWorkspaceOptions) error {
	if service == nil || service.organizations == nil {
		return fmt.Errorf("organization authflow dependency is not configured")
	}
	if reporter, ok := service.organizations.(OrganizationDefaultWorkspaceReporter); ok {
		organizationID, shouldRunPostCommit, err := reporter.EnsureDefaultWorkspaceForUserAndReport(ctx, user)
		if err != nil {
			return err
		}
		if !shouldRunPostCommit {
			return nil
		}
		if options.ProvisionOnboardingTrial {
			if err := service.ensureDefaultWorkspacePostCommit(ctx, organizationID, user.ID); err != nil {
				return err
			}
		}
		return reporter.MarkDefaultWorkspaceBootstrapCompleted(ctx, user.ID, organizationID)
	}
	if err := service.organizations.EnsureDefaultWorkspaceForUser(ctx, user); err != nil {
		return err
	}
	if options.ProvisionOnboardingTrial && service.requireDefaultWorkspacePostCommit {
		return fmt.Errorf("organization dependency cannot report default workspace initialization")
	}
	return nil
}

func (service *Service) ensureDefaultWorkspacePostCommit(ctx context.Context, organizationID string, userID string) error {
	if !service.requireDefaultWorkspacePostCommit {
		return nil
	}
	if service.defaultWorkspacePostCommit == nil {
		return fmt.Errorf("default workspace post-commit provisioner is not configured")
	}
	return service.defaultWorkspacePostCommit.EnsureDefaultWorkspacePostCommit(ctx, organizationID, userID)
}

// InviteInfo describes a workspace or organization invite resolved for auth API
// presentation. Only one of Workspace or Organization is populated.
type InviteInfo struct {
	Kind         InviteKind
	Workspace    workspace.WorkspaceInviteInfo
	Organization organization.OrganizationInviteInfo
}

type InviteKind string

const (
	InviteKindWorkspace    InviteKind = "workspace"
	InviteKindOrganization InviteKind = "organization"
)

func (service *Service) FindInviteInfoByToken(ctx context.Context, token string) (InviteInfo, bool, error) {
	if service != nil && service.workspaceInvites != nil {
		workspaceInfo, found, err := service.workspaceInvites.FindWorkspaceInviteInfoByToken(ctx, token)
		if err != nil {
			return InviteInfo{}, found, err
		}
		if found {
			return InviteInfo{Kind: InviteKindWorkspace, Workspace: workspaceInfo}, true, nil
		}
	}

	if service == nil || service.organizations == nil {
		return InviteInfo{}, false, nil
	}
	organizationInfo, found, err := service.organizations.FindOrganizationInviteInfoByToken(ctx, token)
	if err != nil {
		return InviteInfo{}, found, err
	}
	if found {
		return InviteInfo{Kind: InviteKindOrganization, Organization: organizationInfo}, true, nil
	}
	return InviteInfo{}, false, nil
}

func (service *Service) FindWorkspaceInviteInfoByID(ctx context.Context, inviteID string) (InviteInfo, bool, error) {
	if service == nil || service.workspaceInvites == nil {
		return InviteInfo{}, false, nil
	}
	workspaceInfo, found, err := service.workspaceInvites.FindWorkspaceInviteInfoByID(ctx, inviteID)
	if err != nil {
		return InviteInfo{}, found, err
	}
	if found {
		return InviteInfo{Kind: InviteKindWorkspace, Workspace: workspaceInfo}, true, nil
	}
	return InviteInfo{}, false, nil
}

// InviteAcceptance describes a workspace or organization invite acceptance.
// Only one of Workspace or Organization is populated.
type InviteAcceptance struct {
	Kind         InviteKind
	Workspace    workspace.Summary
	Organization organization.OrganizationInviteAcceptance
}

// PendingInvite describes an invite available to the current signed-in user.
// Only one of Workspace or Organization is populated.
type PendingInvite struct {
	Kind         InviteKind
	Workspace    workspace.PendingWorkspaceInvite
	Organization organization.OrganizationInviteView
}

func (service *Service) ListPendingInvitesForUser(ctx context.Context, user identity.User) ([]PendingInvite, error) {
	if service == nil || service.workspaceInvites == nil || !user.HasVerifiedIdentity() {
		return []PendingInvite{}, nil
	}
	workspaceInvites, err := service.workspaceInvites.ListPendingWorkspaceInvitesForUser(ctx, user)
	if err != nil {
		return nil, err
	}
	pending := make([]PendingInvite, 0, len(workspaceInvites))
	for _, invite := range workspaceInvites {
		pending = append(pending, PendingInvite{
			Kind:      InviteKindWorkspace,
			Workspace: invite,
		})
	}
	return pending, nil
}

func (service *Service) AcceptPendingInviteByID(ctx context.Context, user identity.User, inviteID string) (InviteAcceptance, bool, error) {
	if !user.HasVerifiedIdentity() {
		return InviteAcceptance{}, false, storeerr.Forbidden("Email or mobile must be verified to accept this invite")
	}
	if service == nil || service.workspaceInvites == nil {
		return InviteAcceptance{}, false, nil
	}
	summary, found, err := service.workspaceInvites.AcceptWorkspaceInviteByIDIfExists(ctx, user, inviteID)
	if err != nil {
		return InviteAcceptance{}, found, err
	}
	if found {
		return InviteAcceptance{Kind: InviteKindWorkspace, Workspace: summary}, true, nil
	}
	return InviteAcceptance{}, false, nil
}

func (service *Service) DismissPendingInviteByID(ctx context.Context, user identity.User, inviteID string) (bool, error) {
	if !user.HasVerifiedIdentity() {
		return false, storeerr.Forbidden("Email or mobile must be verified to dismiss this invite")
	}
	if service == nil || service.workspaceInvites == nil {
		return false, nil
	}
	return service.workspaceInvites.DismissPendingWorkspaceInviteForUser(ctx, user, inviteID)
}

func (service *Service) AcceptInviteByToken(ctx context.Context, user identity.User, token string) (InviteAcceptance, bool, error) {
	if service != nil && service.workspaceInvites != nil {
		summary, found, err := service.workspaceInvites.AcceptWorkspaceInviteByTokenIfExists(ctx, user, token)
		if err != nil {
			return InviteAcceptance{}, found, err
		}
		if found {
			return InviteAcceptance{Kind: InviteKindWorkspace, Workspace: summary}, true, nil
		}
	}

	if service == nil || service.organizations == nil {
		return InviteAcceptance{}, false, nil
	}
	accepted, found, err := service.organizations.AcceptOrganizationInviteByTokenIfExists(ctx, user, token)
	if err != nil {
		return InviteAcceptance{}, found, err
	}
	if found {
		return InviteAcceptance{Kind: InviteKindOrganization, Organization: accepted}, true, nil
	}
	return InviteAcceptance{}, false, nil
}

// AutoJoinOrganizationsAfterVerifiedLogin runs the organization auto-join side
// effect for a verified email/mobile login. It preserves the auth API's
// historical precedence: prefer the login identifier kind, then fall back to the
// other verified identifier if the preferred one is absent.
func (service *Service) AutoJoinOrganizationsAfterVerifiedLogin(ctx context.Context, user identity.User, loginIdentifierKind authidentity.Kind) error {
	if service == nil || service.organizations == nil {
		return nil
	}

	tryEmail := func() (bool, error) {
		if !user.HasVerifiedEmail() {
			return false, nil
		}
		return true, service.organizations.AutoJoinOrganizationsForVerifiedEmail(ctx, user)
	}
	tryMobile := func() (bool, error) {
		if !user.HasVerifiedMobile() {
			return false, nil
		}
		return true, service.organizations.AutoJoinOrganizationsForVerifiedMobile(ctx, user)
	}

	switch loginIdentifierKind {
	case authidentity.KindEmail:
		if tried, err := tryEmail(); tried {
			return err
		}
	case authidentity.KindMobile:
		if tried, err := tryMobile(); tried {
			return err
		}
	}

	if tried, err := tryEmail(); tried {
		return err
	}
	_, err := tryMobile()
	return err
}
