// Package organization coordinates organization workflows that cross identity,
// workspace, and channel/scope boundaries.
package organization

import (
	"context"
	"errors"
	"fmt"
	"strings"
	"time"

	"github.com/go-sql-driver/mysql"

	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/pinyinsearch"
	"github.com/tidbcloud/AgentTeam/internal/store/identity"
	"github.com/tidbcloud/AgentTeam/internal/store/model"
	"github.com/tidbcloud/AgentTeam/internal/store/organization"
	"github.com/tidbcloud/AgentTeam/internal/store/taskboard"
	"github.com/tidbcloud/AgentTeam/internal/store/workspace"
)

// IdentityDirectoryStore is the identity-domain surface needed by organization
// directory sync and SSO auto-join flows.
type IdentityDirectoryStore interface {
	FindUserByEmail(ctx context.Context, email string) (model.User, error)
	FindUserByID(ctx context.Context, userID string) (model.User, error)
	EnsureOrganizationDirectoryUser(ctx context.Context, params identity.OrganizationDirectoryUserParams) (model.User, bool, error)
}

// WorkspaceAuthorization is the workspace permission surface used by
// organization administration flows.
type WorkspaceAuthorization interface {
	RequireWorkspaceMemberManagementForUser(ctx context.Context, userID string, workspaceID string) error
	RequireWorkspaceMemberRoleManagementForUser(ctx context.Context, userID string, workspaceID string) error
	RequireWorkspaceRuntimeManagementForUser(ctx context.Context, userID string, workspaceID string) error
}

// ScopeProjectionStore is the narrow scope projection surface organization
// membership reconciliation needs. It deliberately avoids the broader
// private-agent projection lifecycle API.
type ScopeProjectionStore interface {
	EnsureAllChannelScopeParticipantProjection(ctx context.Context, tx coredb.Tx, workspaceID string, memberID string, agentState string, createdAt time.Time) error
	MarkMemberScopeProjectionsRemoved(ctx context.Context, execer coredb.QueryExecer, memberID string, removedAt time.Time) error
	RemovePrivateEntriesForOwner(ctx context.Context, execer coredb.QueryExecer, ownerMemberID string, removedAt time.Time) error
}

// ServiceAccountAudienceStore owns service-account audience projections that
// must follow the Human workspace membership lifecycle.
type ServiceAccountAudienceStore interface {
	EnsureHumanAudienceMembershipsInTx(ctx context.Context, tx coredb.Tx, workspaceID string, memberID string, now time.Time) error
}

// Dependencies contains the narrow ports coordinated by the organization core
// bridge.
type Dependencies struct {
	Identity                IdentityDirectoryStore
	Workspace               WorkspaceAuthorization
	Scopes                  ScopeProjectionStore
	ServiceAccounts         ServiceAccountAudienceStore
	DefaultWorkspaceGroupID string
}

// CoreBridge implements store/organization.CoreBridge without exposing the
// legacy root storefacade or a broad graph to the organization store.
type CoreBridge struct {
	identity                IdentityDirectoryStore
	workspace               WorkspaceAuthorization
	scopes                  ScopeProjectionStore
	serviceAccounts         ServiceAccountAudienceStore
	defaultWorkspaceGroupID string
}

// NewCoreBridge builds the organization bridge used by store/organization.
func NewCoreBridge(deps Dependencies) *CoreBridge {
	return &CoreBridge{
		identity:                deps.Identity,
		workspace:               deps.Workspace,
		scopes:                  deps.Scopes,
		serviceAccounts:         deps.ServiceAccounts,
		defaultWorkspaceGroupID: strings.TrimSpace(deps.DefaultWorkspaceGroupID),
	}
}

func (bridge *CoreBridge) FindUserByEmail(ctx context.Context, email string) (model.User, error) {
	if bridge == nil || bridge.identity == nil {
		return model.User{}, fmt.Errorf("organization identity directory store is not configured")
	}
	return bridge.identity.FindUserByEmail(ctx, email)
}

func (bridge *CoreBridge) FindUserByID(ctx context.Context, userID string) (model.User, error) {
	if bridge == nil || bridge.identity == nil {
		return model.User{}, fmt.Errorf("organization identity directory store is not configured")
	}
	return bridge.identity.FindUserByID(ctx, userID)
}

func (bridge *CoreBridge) EnsureOrganizationDirectoryUser(ctx context.Context, params organization.EnsureOrganizationDirectoryUserParams) (model.User, bool, error) {
	if bridge == nil || bridge.identity == nil {
		return model.User{}, false, fmt.Errorf("organization identity directory store is not configured")
	}
	return bridge.identity.EnsureOrganizationDirectoryUser(ctx, identity.OrganizationDirectoryUserParams{
		Email:            params.Email,
		DisplayName:      params.DisplayName,
		Provider:         params.Provider,
		ProviderTenantID: params.ProviderTenantID,
		ExternalID:       params.ExternalID,
		EmailVerified:    params.EmailVerified,
	})
}

func (bridge *CoreBridge) RequireWorkspaceMemberManagementForUser(ctx context.Context, userID string, workspaceID string) error {
	if bridge == nil || bridge.workspace == nil {
		return fmt.Errorf("organization workspace authorization store is not configured")
	}
	return bridge.workspace.RequireWorkspaceMemberManagementForUser(ctx, userID, workspaceID)
}

func (bridge *CoreBridge) RequireWorkspaceMemberRoleManagementForUser(ctx context.Context, userID string, workspaceID string) error {
	if bridge == nil || bridge.workspace == nil {
		return fmt.Errorf("organization workspace authorization store is not configured")
	}
	return bridge.workspace.RequireWorkspaceMemberRoleManagementForUser(ctx, userID, workspaceID)
}

func (bridge *CoreBridge) RequireWorkspaceRuntimeManagementForUser(ctx context.Context, userID string, workspaceID string) error {
	if bridge == nil || bridge.workspace == nil {
		return fmt.Errorf("organization workspace authorization store is not configured")
	}
	return bridge.workspace.RequireWorkspaceRuntimeManagementForUser(ctx, userID, workspaceID)
}

func (bridge *CoreBridge) CreateDefaultWorkspaceWithNewOrganizationInTx(ctx context.Context, tx coredb.Tx, owner model.User, name string) (organization.WorkspaceSummary, error) {
	summary, err := workspace.CreatePersonalWorkspaceWithNewOrganization(ctx, tx, owner, name, bridge.defaultWorkspaceGroupID)
	if errors.Is(err, workspace.ErrConflict) {
		return organization.WorkspaceSummary{}, fmt.Errorf("workspace slug already taken: %w", organization.ErrConflict)
	}
	if err != nil {
		return organization.WorkspaceSummary{}, err
	}
	if err := taskboard.EnsureDefaultTaskFieldOptions(ctx, tx, summary.WorkspaceID); err != nil {
		return organization.WorkspaceSummary{}, err
	}
	return organization.WorkspaceSummary{
		OrganizationID: summary.OrganizationID,
		WorkspaceID:    summary.WorkspaceID,
		Slug:           summary.Slug,
		Name:           summary.Name,
		MemberID:       summary.MemberID,
		WorkspaceRole:  summary.WorkspaceRole,
	}, nil
}

func (bridge *CoreBridge) CreateWorkspaceInTx(ctx context.Context, tx coredb.Tx, organizationID string, owner model.User, name string) (organization.WorkspaceSummary, error) {
	summary, err := workspace.CreatePersonalWorkspaceForOrganization(ctx, tx, organizationID, owner, name, bridge.defaultWorkspaceGroupID)
	if errors.Is(err, workspace.ErrConflict) {
		return organization.WorkspaceSummary{}, fmt.Errorf("workspace slug already taken: %w", organization.ErrConflict)
	}
	if err != nil {
		return organization.WorkspaceSummary{}, err
	}
	if err := taskboard.EnsureDefaultTaskFieldOptions(ctx, tx, summary.WorkspaceID); err != nil {
		return organization.WorkspaceSummary{}, err
	}
	return organization.WorkspaceSummary{
		OrganizationID: summary.OrganizationID,
		WorkspaceID:    summary.WorkspaceID,
		Slug:           summary.Slug,
		Name:           summary.Name,
		MemberID:       summary.MemberID,
		WorkspaceRole:  summary.WorkspaceRole,
	}, nil
}

func (bridge *CoreBridge) CreateWorkspaceWithSlugInTx(ctx context.Context, tx coredb.Tx, organizationID string, owner model.User, name string, slug string) (organization.WorkspaceSummary, error) {
	summary, err := workspace.CreateWorkspaceForOrganization(ctx, tx, organizationID, owner, name, slug, bridge.defaultWorkspaceGroupID)
	if errors.Is(err, workspace.ErrConflict) {
		return organization.WorkspaceSummary{}, fmt.Errorf("workspace slug already taken: %w", organization.ErrConflict)
	}
	if err != nil {
		return organization.WorkspaceSummary{}, err
	}
	if err := taskboard.EnsureDefaultTaskFieldOptions(ctx, tx, summary.WorkspaceID); err != nil {
		return organization.WorkspaceSummary{}, err
	}
	return organization.WorkspaceSummary{
		OrganizationID: summary.OrganizationID,
		WorkspaceID:    summary.WorkspaceID,
		Slug:           summary.Slug,
		Name:           summary.Name,
		MemberID:       summary.MemberID,
		WorkspaceRole:  summary.WorkspaceRole,
	}, nil
}

func (bridge *CoreBridge) EnsureHumanWorkspaceMemberInTx(ctx context.Context, tx coredb.Tx, workspaceID string, userID string, requestedRole string) (string, string, error) {
	return workspace.EnsureHumanWorkspaceMemberInTx(ctx, tx, workspaceID, userID, requestedRole, bridge.ensureAllChannelMembershipInTx, bridge.ensureServiceAccountAudienceMembershipsInTx)
}

func (bridge *CoreBridge) RestoreHumanWorkspaceMemberBaselineInTx(ctx context.Context, tx coredb.Tx, workspaceID string, userID string, baselineRole string) (string, error) {
	return workspace.RestoreHumanWorkspaceMemberBaselineInTx(ctx, tx, workspaceID, userID, baselineRole, bridge.ensureAllChannelMembershipInTx, bridge.ensureServiceAccountAudienceMembershipsInTx)
}

func (bridge *CoreBridge) DeactivateHumanWorkspaceMemberInTx(ctx context.Context, tx coredb.Tx, workspaceID string, userID string) (string, error) {
	return workspace.DeactivateHumanWorkspaceMemberInTx(ctx, tx, workspaceID, userID, bridge.markMemberScopeProjectionsRemoved, bridge.removePrivateEntriesForOwner)
}

func (bridge *CoreBridge) ArchiveWorkspaceMemberRuntimeResourcesInTx(ctx context.Context, tx coredb.Tx, workspaceID string, userID string) ([]string, error) {
	if bridge == nil || bridge.scopes == nil {
		return nil, fmt.Errorf("organization scope projection store is not configured")
	}
	trimmedWorkspaceID := strings.TrimSpace(workspaceID)
	trimmedUserID := strings.TrimSpace(userID)
	if trimmedWorkspaceID == "" || trimmedUserID == "" {
		return nil, nil
	}

	revokedMachineIDs, err := loadWorkspaceMemberActiveMachineIDsForOrganizationRemoval(ctx, tx, trimmedWorkspaceID, trimmedUserID)
	if err != nil {
		return nil, err
	}
	agents, err := loadWorkspaceMemberRuntimeAgentsForOrganizationRemoval(ctx, tx, trimmedWorkspaceID, trimmedUserID)
	if err != nil {
		return nil, err
	}

	now := time.Now().UTC()
	agentIDs := make([]string, 0, len(agents))
	for _, item := range agents {
		agentIDs = append(agentIDs, item.agentID)
		if err := bridge.scopes.MarkMemberScopeProjectionsRemoved(ctx, tx, item.memberID, now); err != nil {
			return nil, err
		}
		if _, err := tx.Exec(ctx, `
UPDATE members
SET agent_display_name_key = NULL,
    updated_at = $2
WHERE id = $1
`, item.memberID, now); err != nil {
			return nil, fmt.Errorf("release archived agent display name key: %w", err)
		}
	}
	if err := deleteKnowledgeBaseAgentGrantsForOrganizationRemoval(ctx, tx, trimmedWorkspaceID, agentIDs); err != nil {
		return nil, err
	}

	if _, err := tx.Exec(ctx, `
UPDATE agents a
JOIN machines mac
  ON mac.workspace_id = a.workspace_id
 AND mac.id = a.machine_id
SET a.status = 'archived',
    a.name = CONCAT('archived-', a.id),
    a.auth_token_hash = NULL,
    a.auto_wake_enabled = FALSE,
    a.updated_at = $3
WHERE a.workspace_id = $1
  AND mac.created_by_user_id = $2
  AND a.status <> 'archived'
`, trimmedWorkspaceID, trimmedUserID, now); err != nil {
		return nil, fmt.Errorf("archive member runtime agents: %w", err)
	}
	for _, item := range agents {
		pinyinFull, pinyinInitials := pinyinsearch.Variants("archived-" + item.agentID)
		if _, err := tx.Exec(ctx, `
UPDATE agents
SET name_pinyin = $2,
    name_pinyin_initials = $3
WHERE id = $1
`, item.agentID, pinyinFull, pinyinInitials); err != nil {
			return nil, fmt.Errorf("sync archived agent name pinyin: %w", err)
		}
	}
	if _, err := tx.Exec(ctx, `
UPDATE machines
SET status = 'revoked',
    updated_at = $3
WHERE workspace_id = $1
  AND created_by_user_id = $2
  AND status = 'active'
`, trimmedWorkspaceID, trimmedUserID, now); err != nil {
		return nil, fmt.Errorf("revoke member runtime machines: %w", err)
	}
	return revokedMachineIDs, nil
}

func (bridge *CoreBridge) ensureAllChannelMembershipInTx(ctx context.Context, tx coredb.Tx, workspaceID string, memberID string) error {
	if bridge == nil || bridge.scopes == nil {
		return fmt.Errorf("organization scope projection store is not configured")
	}
	if err := bridge.scopes.EnsureAllChannelScopeParticipantProjection(ctx, tx, workspaceID, memberID, "no-wake", time.Now().UTC()); err != nil {
		return fmt.Errorf("join #all for workspace member projection: %w", err)
	}
	return nil
}

func (bridge *CoreBridge) markMemberScopeProjectionsRemoved(ctx context.Context, execer coredb.QueryExecer, memberID string, removedAt time.Time) error {
	if bridge == nil || bridge.scopes == nil {
		return fmt.Errorf("organization scope projection store is not configured")
	}
	return bridge.scopes.MarkMemberScopeProjectionsRemoved(ctx, execer, memberID, removedAt)
}

func (bridge *CoreBridge) ensureServiceAccountAudienceMembershipsInTx(ctx context.Context, tx coredb.Tx, workspaceID string, memberID string) error {
	if bridge == nil || bridge.serviceAccounts == nil {
		return fmt.Errorf("organization service-account audience store is not configured")
	}
	return bridge.serviceAccounts.EnsureHumanAudienceMembershipsInTx(ctx, tx, workspaceID, memberID, time.Now().UTC())
}

func (bridge *CoreBridge) removePrivateEntriesForOwner(ctx context.Context, execer coredb.QueryExecer, ownerMemberID string, removedAt time.Time) error {
	if bridge == nil || bridge.scopes == nil {
		return fmt.Errorf("organization scope projection store is not configured")
	}
	return bridge.scopes.RemovePrivateEntriesForOwner(ctx, execer, ownerMemberID, removedAt)
}

type organizationRemovalRuntimeAgent struct {
	agentID  string
	memberID string
}

func loadWorkspaceMemberActiveMachineIDsForOrganizationRemoval(ctx context.Context, queryer interface {
	Query(context.Context, string, ...any) (coredb.Rows, error)
}, workspaceID string, userID string) ([]string, error) {
	rows, err := queryer.Query(ctx, `
SELECT id
FROM machines
WHERE workspace_id = $1
  AND created_by_user_id = $2
  AND status = 'active'
ORDER BY id ASC
FOR UPDATE
`, workspaceID, userID)
	if err != nil {
		return nil, fmt.Errorf("list member runtime machines for organization removal: %w", err)
	}
	defer rows.Close()

	machineIDs := make([]string, 0)
	for rows.Next() {
		var machineID string
		if err := rows.Scan(&machineID); err != nil {
			return nil, fmt.Errorf("scan member runtime machine for organization removal: %w", err)
		}
		machineIDs = append(machineIDs, strings.TrimSpace(machineID))
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate member runtime machines for organization removal: %w", err)
	}
	return machineIDs, nil
}

func loadWorkspaceMemberRuntimeAgentsForOrganizationRemoval(ctx context.Context, queryer interface {
	Query(context.Context, string, ...any) (coredb.Rows, error)
}, workspaceID string, userID string) ([]organizationRemovalRuntimeAgent, error) {
	rows, err := queryer.Query(ctx, `
SELECT a.id, a.member_id
FROM agents a
JOIN machines mac
  ON mac.workspace_id = a.workspace_id
 AND mac.id = a.machine_id
WHERE a.workspace_id = $1
  AND mac.created_by_user_id = $2
  AND a.status <> 'archived'
ORDER BY a.id ASC
FOR UPDATE
`, workspaceID, userID)
	if err != nil {
		return nil, fmt.Errorf("list member runtime agents for organization removal: %w", err)
	}
	defer rows.Close()

	agents := make([]organizationRemovalRuntimeAgent, 0)
	for rows.Next() {
		var item organizationRemovalRuntimeAgent
		if err := rows.Scan(&item.agentID, &item.memberID); err != nil {
			return nil, fmt.Errorf("scan member runtime agent for organization removal: %w", err)
		}
		agents = append(agents, item)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate member runtime agents for organization removal: %w", err)
	}
	return agents, nil
}

func deleteKnowledgeBaseAgentGrantsForOrganizationRemoval(ctx context.Context, execer coredb.QueryExecer, workspaceID string, agentIDs []string) error {
	if strings.TrimSpace(workspaceID) == "" || len(agentIDs) == 0 {
		return nil
	}
	seen := make(map[string]struct{}, len(agentIDs))
	for _, agentID := range agentIDs {
		trimmedAgentID := strings.TrimSpace(agentID)
		if trimmedAgentID == "" {
			continue
		}
		if _, ok := seen[trimmedAgentID]; ok {
			continue
		}
		seen[trimmedAgentID] = struct{}{}
		if _, err := execer.Exec(ctx, `
DELETE FROM knowledge_base_grants
WHERE workspace_id = $1
  AND subject_type = 'agent'
  AND subject_id = $2
`, workspaceID, trimmedAgentID); err != nil {
			if isMissingKnowledgeBaseGrantsTableForOrganizationRemoval(err) {
				return nil
			}
			return fmt.Errorf("delete knowledge base agent grants: %w", err)
		}
	}
	return nil
}

func isMissingKnowledgeBaseGrantsTableForOrganizationRemoval(err error) bool {
	var mysqlErr *mysql.MySQLError
	if errors.As(err, &mysqlErr) && mysqlErr.Number == 1146 {
		return true
	}
	message := strings.ToLower(err.Error())
	return strings.Contains(message, "knowledge_base_grants") && strings.Contains(message, "doesn't exist")
}
