package app

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

	"github.com/tidbcloud/AgentTeam/internal/core/domain"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/agent"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
	"github.com/tidbcloud/AgentTeam/internal/store/taskboard"
)

// ActivateWorkflowRunCallbackTaskWatch promotes the authenticated callback
// Agent into the existing Task watch mechanism after an agent-selected Task
// workScope has passed callback authorization. The caller supplies the same
// transaction that wins callback completion, so a failed watch update also
// rolls back the callback result.
func (store *Store) ActivateWorkflowRunCallbackTaskWatch(
	ctx context.Context,
	tx coredb.Tx,
	workspaceID string,
	agentID string,
	scope domain.MessageScopeValue,
) error {
	workspaceID = strings.TrimSpace(workspaceID)
	agentID = strings.TrimSpace(agentID)
	if tx == nil || workspaceID == "" || agentID == "" {
		return badRequest("workflow Run callback Task watch authority is incomplete")
	}
	normalized, err := domain.NormalizeMessageScopeValue(scope)
	if err != nil || normalized.Kind != domain.MessageScopeKindTask {
		return badRequest("workflow Run callback Task watch requires a Task scope")
	}
	authenticated, err := loadWorkflowRunCallbackAgent(ctx, tx, workspaceID, agentID, true)
	if err != nil {
		return err
	}
	return store.applyWorkflowCallbackTaskWatchHandoffTx(
		ctx,
		tx,
		workspaceID,
		message.AgentRecipient{
			AgentID:       authenticated.AgentID,
			AgentMemberID: authenticated.MemberID,
		},
		message.WorkflowCallbackTaskWatchHandoff{TaskScopeID: normalized.ScopeID},
		time.Now().UTC(),
	)
}

type workflowRunCallbackScopeRecord struct {
	Scope    domain.MessageScopeValue
	ParentID string
}

// AuthorizeWorkflowRunCallbackScope validates one first-class scope emitted by
// an Agent callback. A callback may only publish a scope currently writable by
// both the authenticated wake Agent and the frozen Human invocation owner.
// This boundary never creates or reinterprets topology: the exact active scope
// and kind must already exist in the frozen workspace.
func (store *Store) AuthorizeWorkflowRunCallbackScope(
	ctx context.Context,
	workspaceID string,
	agentID string,
	ownerUserID string,
	authorMemberID string,
	candidate domain.MessageScopeValue,
) (domain.MessageScopeValue, error) {
	var authorized domain.MessageScopeValue
	err := coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
		var err error
		authorized, err = store.AuthorizeWorkflowRunCallbackScopeTx(
			ctx,
			tx,
			workspaceID,
			agentID,
			ownerUserID,
			authorMemberID,
			candidate,
		)
		return err
	})
	return authorized, err
}

// AuthorizeWorkflowRunCallbackRelatedScopes validates the context-only scopes
// before Step get exposes them to the authenticated callback Agent. Unlike a
// workScope, related scopes require read visibility—not discussion write
// authority—from both the Agent and the frozen Human invocation owner. The
// whole frozen list shares one transaction and one principal hydration so its
// cost does not multiply Agent/Human authorization work per scope.
func (store *Store) AuthorizeWorkflowRunCallbackRelatedScopes(
	ctx context.Context,
	workspaceID string,
	agentID string,
	ownerUserID string,
	authorMemberID string,
	candidates []domain.MessageScopeValue,
) ([]domain.MessageScopeValue, error) {
	var authorized []domain.MessageScopeValue
	err := coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
		var err error
		authorized, err = store.AuthorizeWorkflowRunCallbackRelatedScopesTx(
			ctx,
			tx,
			workspaceID,
			agentID,
			ownerUserID,
			authorMemberID,
			candidates,
		)
		return err
	})
	return authorized, err
}

// AuthorizeWorkflowRunCallbackRelatedScopesTx is the transaction-aware form
// used for context-only scopes emitted in callback WorkItems. It intentionally
// checks read visibility, not discussion write authority, inside the same
// first-result-wins transaction that persists the callback receipt.
func (store *Store) AuthorizeWorkflowRunCallbackRelatedScopesTx(
	ctx context.Context,
	tx coredb.Tx,
	workspaceID string,
	agentID string,
	ownerUserID string,
	authorMemberID string,
	candidates []domain.MessageScopeValue,
) ([]domain.MessageScopeValue, error) {
	workspaceID = strings.TrimSpace(workspaceID)
	agentID = strings.TrimSpace(agentID)
	ownerUserID = strings.TrimSpace(ownerUserID)
	authorMemberID = strings.TrimSpace(authorMemberID)
	if tx == nil || workspaceID == "" || agentID == "" || ownerUserID == "" || authorMemberID == "" {
		return nil, badRequest("workflow Run callback related scope authority is incomplete")
	}
	scopes := make([]domain.MessageScopeValue, len(candidates))
	seen := make(map[domain.MessageScopeValue]struct{}, len(candidates))
	for index, candidate := range candidates {
		scope, err := domain.NormalizeMessageScopeValue(candidate)
		if err != nil {
			return nil, badRequest(fmt.Sprintf("workflow Run relatedScopes[%d] is invalid: %v", index, err))
		}
		if _, duplicate := seen[scope]; duplicate {
			return nil, badRequest(fmt.Sprintf("workflow Run relatedScopes[%d] is duplicated", index))
		}
		seen[scope] = struct{}{}
		scopes[index] = scope
	}
	authenticatedAgent, err := loadWorkflowRunCallbackAgent(ctx, tx, workspaceID, agentID, false)
	if err != nil {
		return nil, err
	}
	member, err := store.loadAuthorizedHumanWorkspaceMember(
		ctx,
		tx,
		ownerUserID,
		workspaceID,
		workspaceAccessAction,
	)
	if err != nil {
		return nil, err
	}
	if strings.TrimSpace(member.Member.ID) != authorMemberID {
		return nil, ErrForbidden
	}
	visible := message.NewSQLVisibleScopeAuthorizer(tx)
	scopeIDs := make([]string, len(scopes))
	for index, scope := range scopes {
		scopeIDs[index] = scope.ScopeID
	}
	agentAccess := make(map[string]message.VisibleScopeReadAccess, len(scopeIDs))
	humanAccess := make(map[string]message.VisibleScopeReadAccess, len(scopeIDs))
	for start := 0; start < len(scopeIDs); start += message.MaxVisibleScopesPerBatch {
		end := start + message.MaxVisibleScopesPerBatch
		if end > len(scopeIDs) {
			end = len(scopeIDs)
		}
		chunk := scopeIDs[start:end]
		agentChunk, err := visible.CanReadVisibleScopesForAgent(
			ctx,
			workspaceID,
			authenticatedAgent.MemberID,
			authenticatedAgent.AgentID,
			chunk,
		)
		if err != nil {
			return nil, fmt.Errorf("authorize workflow Run Agent relatedScopes: %w", err)
		}
		for scopeID, access := range agentChunk {
			agentAccess[scopeID] = access
		}
		humanChunk, err := visible.CanReadVisibleScopes(
			ctx,
			workspaceID,
			authorMemberID,
			chunk,
		)
		if err != nil {
			return nil, fmt.Errorf("authorize workflow Run Human relatedScopes: %w", err)
		}
		for scopeID, access := range humanChunk {
			humanAccess[scopeID] = access
		}
	}
	authorized := make([]domain.MessageScopeValue, 0, len(scopes))
	for index, scope := range scopes {
		agentScope, agentExists := agentAccess[scope.ScopeID]
		humanScope, humanExists := humanAccess[scope.ScopeID]
		if !agentExists || !humanExists {
			return nil, ErrNotFound
		}
		storedKind, err := workflowRunCallbackScopeKind(agentScope.ScopeType)
		if err != nil {
			return nil, err
		}
		if humanScope.ScopeType != agentScope.ScopeType {
			return nil, badRequest(fmt.Sprintf("workflow Run relatedScopes[%d] scope type changed during authorization", index))
		}
		if storedKind != scope.Kind {
			return nil, badRequest(fmt.Sprintf(
				"workflow Run relatedScopes[%d] kind %q does not match stored scope kind %q",
				index,
				scope.Kind,
				storedKind,
			))
		}
		if !agentScope.Readable || !humanScope.Readable {
			return nil, ErrForbidden
		}
		authorized = append(authorized, scope)
	}
	return authorized, nil
}

// AuthorizeWorkflowRunCallbackScopeTx is the transaction-aware form used by
// Workflow callback completion. Every scope emitted by one callback is checked
// through this method inside the same first-result-wins transaction that
// persists the callback receipt.
func (store *Store) AuthorizeWorkflowRunCallbackScopeTx(
	ctx context.Context,
	tx coredb.Tx,
	workspaceID string,
	agentID string,
	ownerUserID string,
	authorMemberID string,
	candidate domain.MessageScopeValue,
) (domain.MessageScopeValue, error) {
	workspaceID = strings.TrimSpace(workspaceID)
	agentID = strings.TrimSpace(agentID)
	ownerUserID = strings.TrimSpace(ownerUserID)
	authorMemberID = strings.TrimSpace(authorMemberID)
	if tx == nil || workspaceID == "" || agentID == "" || ownerUserID == "" || authorMemberID == "" {
		return domain.MessageScopeValue{}, badRequest("workflow Run callback scope authority is incomplete")
	}
	scope, err := domain.NormalizeMessageScopeValue(candidate)
	if err != nil {
		return domain.MessageScopeValue{}, badRequest(fmt.Sprintf("workflow Run message scope is invalid: %v", err))
	}
	// Preserve scope-not-found/kind-mismatch semantics before principal
	// authorization. Non-Task authority stays on this transaction's ordinary
	// TiDB snapshot; Task authority follows the Task writer's existing
	// task-row-first serialization below.
	preflight, err := loadWorkflowRunCallbackScopeWithoutLock(ctx, tx, workspaceID, scope.ScopeID)
	if err != nil {
		return domain.MessageScopeValue{}, err
	}
	if preflight.Scope.Kind != scope.Kind {
		return domain.MessageScopeValue{}, badRequest(fmt.Sprintf(
			"workflow Run message scope kind %q does not match stored scope kind %q",
			scope.Kind,
			preflight.Scope.Kind,
		))
	}
	if scope.Kind != domain.MessageScopeKindTask {
		return store.authorizeWorkflowRunCallbackNonTaskScope(
			ctx,
			tx,
			workspaceID,
			agentID,
			ownerUserID,
			authorMemberID,
			preflight,
		)
	}
	authenticatedAgent, err := loadWorkflowRunCallbackAgent(ctx, tx, workspaceID, agentID, true)
	if err != nil {
		return domain.MessageScopeValue{}, err
	}
	record, err := store.authorizeWorkflowRunScopeForFrozenHuman(
		ctx,
		tx,
		workspaceID,
		ownerUserID,
		authorMemberID,
		scope,
	)
	if err != nil {
		return domain.MessageScopeValue{}, err
	}
	if err := store.authorizeWorkflowRunCallbackAgentScope(ctx, tx, authenticatedAgent, record); err != nil {
		return domain.MessageScopeValue{}, err
	}
	return record.Scope, nil
}

// authorizeWorkflowRunCallbackNonTaskScope deliberately uses the ordinary
// Human and Agent message-writer reads without adding authorization-row locks.
// ResolveAwaitingEffect invokes this method inside the same database
// transaction that wins and persists callback completion. Consequently every
// authorization fact and the effect write share one TiDB snapshot: a revoke
// committed before that snapshot is denied, while a revoke overlapping the
// callback can be linearized after the snapshot-authorized callback. Avoiding
// extra FOR UPDATE locks also preserves the ordinary writer's exact semantics
// and cannot invert the established profile/membership/scope destruction order.
func (store *Store) authorizeWorkflowRunCallbackNonTaskScope(
	ctx context.Context,
	tx coredb.Tx,
	workspaceID string,
	agentID string,
	ownerUserID string,
	authorMemberID string,
	record workflowRunCallbackScopeRecord,
) (domain.MessageScopeValue, error) {
	authenticatedAgent, err := loadWorkflowRunCallbackAgent(
		ctx,
		tx,
		workspaceID,
		agentID,
		false,
	)
	if err != nil {
		return domain.MessageScopeValue{}, err
	}
	if err := store.authorizeWorkflowRunCallbackHumanScope(
		ctx,
		tx,
		workspaceID,
		ownerUserID,
		authorMemberID,
		record,
	); err != nil {
		return domain.MessageScopeValue{}, err
	}
	if err := store.authorizeWorkflowRunCallbackAgentScope(
		ctx,
		tx,
		authenticatedAgent,
		record,
	); err != nil {
		return domain.MessageScopeValue{}, err
	}
	return record.Scope, nil
}

// authorizeWorkflowRunScopeForFrozenHuman is shared by scope-form message
// dispatch and Task callback scope output. It locks the exact existing scope
// in the caller's transaction so a concurrent archive/kind change cannot pass
// between current Human authorization and the dependent durable write. Non-Task
// callback output deliberately uses authorizeWorkflowRunCallbackNonTaskScope
// instead, preserving ordinary writer snapshot semantics.
func (store *Store) authorizeWorkflowRunScopeForFrozenHuman(
	ctx context.Context,
	tx coredb.Tx,
	workspaceID string,
	ownerUserID string,
	authorMemberID string,
	candidate domain.MessageScopeValue,
) (workflowRunCallbackScopeRecord, error) {
	workspaceID = strings.TrimSpace(workspaceID)
	ownerUserID = strings.TrimSpace(ownerUserID)
	authorMemberID = strings.TrimSpace(authorMemberID)
	if workspaceID == "" || ownerUserID == "" || authorMemberID == "" {
		return workflowRunCallbackScopeRecord{}, badRequest("workflow Run frozen Human scope authority is incomplete")
	}
	scope, err := domain.NormalizeMessageScopeValue(candidate)
	if err != nil {
		return workflowRunCallbackScopeRecord{}, badRequest(fmt.Sprintf("workflow Run message scope is invalid: %v", err))
	}
	var record workflowRunCallbackScopeRecord
	if scope.Kind == domain.MessageScopeKindTask {
		// Task mutations use task-row -> scope-row lock order. Read the
		// candidate once without a lock so the shared Task discussion helper can
		// lock the Task first; after authorization, lock and revalidate the
		// scope before the dependent durable write.
		record, err = loadWorkflowRunCallbackScopeWithoutLock(ctx, tx, workspaceID, scope.ScopeID)
	} else {
		record, err = loadWorkflowRunCallbackScope(ctx, tx, workspaceID, scope.ScopeID)
	}
	if err != nil {
		return workflowRunCallbackScopeRecord{}, err
	}
	if record.Scope.Kind != scope.Kind {
		return workflowRunCallbackScopeRecord{}, badRequest(fmt.Sprintf(
			"workflow Run message scope kind %q does not match stored scope kind %q",
			scope.Kind,
			record.Scope.Kind,
		))
	}
	if err := store.authorizeWorkflowRunCallbackHumanScope(
		ctx,
		tx,
		workspaceID,
		ownerUserID,
		authorMemberID,
		record,
	); err != nil {
		return workflowRunCallbackScopeRecord{}, err
	}
	if scope.Kind == domain.MessageScopeKindTask {
		locked, lockErr := loadWorkflowRunCallbackScope(ctx, tx, workspaceID, scope.ScopeID)
		if lockErr != nil {
			return workflowRunCallbackScopeRecord{}, lockErr
		}
		if locked.Scope != record.Scope {
			return workflowRunCallbackScopeRecord{}, badRequest("workflow Run Task scope changed during authorization")
		}
		record = locked
	}
	return record, nil
}

func loadWorkflowRunCallbackScope(
	ctx context.Context,
	queryer coredb.QueryRower,
	workspaceID string,
	scopeID string,
) (workflowRunCallbackScopeRecord, error) {
	return loadWorkflowRunCallbackScopeRecord(ctx, queryer, workspaceID, scopeID, true)
}

func loadWorkflowRunCallbackScopeWithoutLock(
	ctx context.Context,
	queryer coredb.QueryRower,
	workspaceID string,
	scopeID string,
) (workflowRunCallbackScopeRecord, error) {
	return loadWorkflowRunCallbackScopeRecord(ctx, queryer, workspaceID, scopeID, false)
}

func loadWorkflowRunCallbackScopeRecord(
	ctx context.Context,
	queryer coredb.QueryRower,
	workspaceID string,
	scopeID string,
	forUpdate bool,
) (workflowRunCallbackScopeRecord, error) {
	var scopeType string
	var parentID string
	query := `
SELECT s.type, COALESCE(s.parent_scope_id, '')
FROM scopes s
WHERE s.workspace_id = $1
  AND s.id = $2
  AND s.status = 'active'
  AND s.archived_at IS NULL
LIMIT 1
`
	if forUpdate {
		query += "FOR UPDATE"
	}
	err := queryer.QueryRow(ctx, query, workspaceID, scopeID).Scan(&scopeType, &parentID)
	if coredb.IsNotFound(err) {
		return workflowRunCallbackScopeRecord{}, ErrNotFound
	}
	if err != nil {
		return workflowRunCallbackScopeRecord{}, fmt.Errorf("load workflow Run callback scope: %w", err)
	}
	kind, err := workflowRunCallbackScopeKind(scopeType)
	if err != nil {
		return workflowRunCallbackScopeRecord{}, err
	}
	return workflowRunCallbackScopeRecord{
		Scope: domain.MessageScopeValue{
			Kind:    kind,
			ScopeID: scopeID,
		},
		ParentID: strings.TrimSpace(parentID),
	}, nil
}

func workflowRunCallbackScopeKind(scopeType string) (domain.MessageScopeKind, error) {
	switch strings.TrimSpace(scopeType) {
	case string(domain.MessageScopeKindChannel):
		return domain.MessageScopeKindChannel, nil
	case string(domain.MessageScopeKindThread):
		return domain.MessageScopeKindThread, nil
	case string(domain.MessageScopeKindDM):
		return domain.MessageScopeKindDM, nil
	case string(domain.MessageScopeKindTask):
		return domain.MessageScopeKindTask, nil
	default:
		return "", badRequest(fmt.Sprintf("workflow Run callback scope type %q is unsupported", scopeType))
	}
}

func loadWorkflowRunCallbackAgent(
	ctx context.Context,
	queryer coredb.QueryRower,
	workspaceID string,
	agentID string,
	forUpdate bool,
) (agent.AuthenticatedAgent, error) {
	var authenticated agent.AuthenticatedAgent
	query := `
SELECT a.id, a.workspace_id, a.member_id, a.machine_id
FROM agents a
JOIN members m
  ON m.workspace_id = a.workspace_id
 AND m.id = a.member_id
 AND m.type = 'agent'
 AND m.status = 'active'
JOIN machines mac
  ON mac.workspace_id = a.workspace_id
 AND mac.id = a.machine_id
 AND mac.status = 'active'
WHERE a.workspace_id = $1
  AND a.id = $2
LIMIT 1
`
	if forUpdate {
		query += "FOR UPDATE"
	}
	err := queryer.QueryRow(ctx, query, workspaceID, agentID).Scan(
		&authenticated.AgentID,
		&authenticated.WorkspaceID,
		&authenticated.MemberID,
		&authenticated.MachineID,
	)
	if coredb.IsNotFound(err) {
		return agent.AuthenticatedAgent{}, ErrForbidden
	}
	if err != nil {
		return agent.AuthenticatedAgent{}, fmt.Errorf("load workflow Run callback Agent: %w", err)
	}
	return authenticated, nil
}

func (store *Store) authorizeWorkflowRunCallbackAgentScope(
	ctx context.Context,
	tx coredb.Tx,
	authenticated agent.AuthenticatedAgent,
	record workflowRunCallbackScopeRecord,
) error {
	switch record.Scope.Kind {
	case domain.MessageScopeKindChannel:
		_, err := loadAgentTargetChannelRosterBasis(ctx, tx, authenticated, record.Scope.ScopeID)
		return err
	case domain.MessageScopeKindThread:
		if record.ParentID == "" {
			return ErrNotFound
		}
		parent, err := loadWorkflowRunCallbackScopeWithoutLock(ctx, tx, authenticated.WorkspaceID, record.ParentID)
		if err != nil {
			return err
		}
		switch parent.Scope.Kind {
		case domain.MessageScopeKindChannel:
			_, err := loadAgentTargetChannelRosterBasis(ctx, tx, authenticated, parent.Scope.ScopeID)
			return err
		case domain.MessageScopeKindDM:
			// Ordinary Agent writes to a DM reply thread first resolve the
			// parent DM and require both active parent participants. A child
			// thread membership is only a read grant and must not preserve
			// callback write authority after parent-DM membership is revoked.
			_, err := message.ResolveDMThreadByIDForMember(
				ctx,
				tx,
				authenticated.MemberID,
				authenticated.WorkspaceID,
				parent.Scope.ScopeID,
			)
			if errors.Is(err, message.ErrNotFound) || errors.Is(err, message.ErrForbidden) {
				return ErrForbidden
			}
			if err != nil {
				return fmt.Errorf("authorize workflow Run callback parent DM: %w", err)
			}
			return nil
		default:
			return badRequest("workflow Run callback thread parent must be a channel or dm scope")
		}
	case domain.MessageScopeKindDM:
		return authorizeWorkflowRunCallbackVisibleScope(ctx, tx, authenticated.WorkspaceID, authenticated.MemberID, record.Scope.ScopeID)
	case domain.MessageScopeKindTask:
		err := taskboard.New(store.db).InTx(tx).AuthorizeAgentTaskScopeDiscussionWrite(
			ctx,
			taskboard.AgentActor{
				WorkspaceID: authenticated.WorkspaceID,
				MemberID:    authenticated.MemberID,
			},
			record.Scope.ScopeID,
		)
		return mapTaskScopeMessageError(err)
	default:
		return badRequest("workflow Run callback scope kind is unsupported")
	}
}

func (store *Store) authorizeWorkflowRunCallbackHumanScope(
	ctx context.Context,
	tx coredb.Tx,
	workspaceID string,
	ownerUserID string,
	authorMemberID string,
	record workflowRunCallbackScopeRecord,
) error {
	member, err := store.loadAuthorizedHumanWorkspaceMember(
		ctx,
		tx,
		ownerUserID,
		workspaceID,
		workspaceAccessAction,
	)
	if err != nil {
		return err
	}
	if strings.TrimSpace(member.Member.ID) != authorMemberID {
		return ErrForbidden
	}

	if record.Scope.Kind == domain.MessageScopeKindTask {
		return mapTaskScopeMessageError(
			taskboard.New(store.db).InTx(tx).AuthorizeUserTaskScopeDiscussionWrite(
				ctx,
				ownerUserID,
				workspaceID,
				authorMemberID,
				record.Scope.ScopeID,
			),
		)
	}
	authorizedMember, target, err := store.resolveAuthorizedHumanMessageTargetWithQueryer(
		ctx,
		tx,
		ownerUserID,
		workspaceID,
		record.Scope.ScopeID,
		channelPostAction,
		dmPostAction,
		threadPostAction,
	)
	if err != nil {
		return err
	}
	if strings.TrimSpace(authorizedMember.Member.ID) != authorMemberID ||
		message.ScopeIDForThreadAccess(target) != record.Scope.ScopeID {
		return ErrForbidden
	}
	return nil
}

func authorizeWorkflowRunCallbackVisibleScope(
	ctx context.Context,
	queryer coredb.Queryer,
	workspaceID string,
	memberID string,
	scopeID string,
) error {
	allowed, err := message.NewSQLVisibleScopeAuthorizer(queryer).CanReadVisibleScope(
		ctx,
		workspaceID,
		memberID,
		scopeID,
	)
	if err != nil {
		return fmt.Errorf("authorize workflow Run callback visible scope: %w", err)
	}
	if !allowed {
		return ErrForbidden
	}
	return nil
}
