package app

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

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

// ResolveWorkflowRunMessageTargetScope maps the author-facing message target
// frozen in a Workflow Run effect to its typed canonical scope. A non-empty
// clientMessageID also freezes that scope in a durable reservation before any
// message is written. The returned kind is loaded from the committed scope so
// concurrent reservations with different authored targets still dispatch the
// single winning destination through the correct canonical writer.
func (store *Store) ResolveWorkflowRunMessageTargetScope(ctx context.Context, workspaceID string, ownerUserID string, authorMemberID string, targetRef string, clientMessageID string) (domain.ResolvedMessageTarget, error) {
	workspaceID = strings.TrimSpace(workspaceID)
	ownerUserID = strings.TrimSpace(ownerUserID)
	authorMemberID = strings.TrimSpace(authorMemberID)
	targetRef = strings.TrimSpace(targetRef)
	if workspaceID == "" || ownerUserID == "" || authorMemberID == "" || targetRef == "" {
		return domain.ResolvedMessageTarget{}, badRequest("workspaceId, ownerUserId, authorMemberId, and target are required")
	}
	parsed, err := domain.ParseMessageTargetReference(targetRef)
	if err != nil {
		return domain.ResolvedMessageTarget{}, mapMessagingError(err)
	}
	return store.resolveWorkflowRunMessageDestination(
		ctx, workspaceID, ownerUserID, authorMemberID, targetRef, clientMessageID,
		func(ctx context.Context, queryer coredb.Tx, workspaceID, authorMemberID string) (domain.ResolvedMessageTarget, error) {
			return resolveUnreservedWorkflowRunMessageTargetScope(ctx, queryer, workspaceID, authorMemberID, parsed)
		},
	)
}

// ResolveWorkflowRunMessageScope validates a first-class canonical scope
// destination for a Workflow Run effect. It does not reinterpret scope ids as
// author-facing targets or materialize topology: the scope must already exist,
// be active, belong to the frozen workspace, and have the declared kind.
func (store *Store) ResolveWorkflowRunMessageScope(ctx context.Context, workspaceID string, ownerUserID string, authorMemberID string, scope domain.MessageScopeValue, clientMessageID string) (domain.ResolvedMessageTarget, error) {
	if err := domain.ValidateMessageScopeValue(scope); err != nil {
		return domain.ResolvedMessageTarget{}, badRequest(fmt.Sprintf("workflow Run message scope is invalid: %v", err))
	}
	reservationRef := "scope:" + string(scope.Kind) + ":" + scope.ScopeID
	return store.resolveWorkflowRunMessageDestination(
		ctx, workspaceID, ownerUserID, authorMemberID, reservationRef, clientMessageID,
		func(ctx context.Context, queryer coredb.Tx, workspaceID, _ string) (domain.ResolvedMessageTarget, error) {
			if _, err := store.authorizeWorkflowRunScopeForFrozenHuman(
				ctx,
				queryer,
				workspaceID,
				ownerUserID,
				authorMemberID,
				scope,
			); err != nil {
				return domain.ResolvedMessageTarget{}, err
			}
			resolved, err := loadWorkflowRunResolvedMessageTarget(ctx, queryer, workspaceID, scope.ScopeID)
			if err != nil {
				return domain.ResolvedMessageTarget{}, err
			}
			actualScope, err := domain.MessageScopeValueFromResolvedTarget(resolved)
			if err != nil {
				return domain.ResolvedMessageTarget{}, err
			}
			if actualScope.Kind != scope.Kind {
				return domain.ResolvedMessageTarget{}, badRequest(fmt.Sprintf(
					"workflow Run message scope kind %q does not match stored scope kind %q",
					scope.Kind, actualScope.Kind,
				))
			}
			return resolved, nil
		},
	)
}

type workflowRunMessageDestinationResolver func(context.Context, coredb.Tx, string, string) (domain.ResolvedMessageTarget, error)

func (store *Store) resolveWorkflowRunMessageDestination(
	ctx context.Context,
	workspaceID string,
	ownerUserID string,
	authorMemberID string,
	reservationRef string,
	clientMessageID string,
	resolveCandidate workflowRunMessageDestinationResolver,
) (domain.ResolvedMessageTarget, error) {
	workspaceID = strings.TrimSpace(workspaceID)
	ownerUserID = strings.TrimSpace(ownerUserID)
	authorMemberID = strings.TrimSpace(authorMemberID)
	reservationRef = strings.TrimSpace(reservationRef)
	if workspaceID == "" || ownerUserID == "" || authorMemberID == "" || reservationRef == "" || resolveCandidate == nil {
		return domain.ResolvedMessageTarget{}, badRequest("workspaceId, ownerUserId, authorMemberId, and message destination are required")
	}
	clientMessageID, err := normalizeClientMessageID(clientMessageID)
	if err != nil {
		return domain.ResolvedMessageTarget{}, err
	}
	if clientMessageID == "" {
		var resolved domain.ResolvedMessageTarget
		err := coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
			if err := store.authorizeWorkflowRunMessageFrozenAuthor(ctx, tx, workspaceID, ownerUserID, authorMemberID); err != nil {
				return err
			}
			var resolveErr error
			resolved, resolveErr = resolveCandidate(ctx, tx, workspaceID, authorMemberID)
			return resolveErr
		})
		return resolved, err
	}

	var reservedTarget domain.ResolvedMessageTarget
	err = coordinator.New(store.db).Run(ctx, "reserve workflow Run message target", func(tx coredb.Tx) error {
		if err := store.authorizeWorkflowRunMessageFrozenAuthor(ctx, tx, workspaceID, ownerUserID, authorMemberID); err != nil {
			return err
		}
		if existingScopeID, found, err := loadWorkflowRunMessageTargetReservation(ctx, tx, workspaceID, authorMemberID, clientMessageID); err != nil {
			return err
		} else if found {
			reservedTarget, err = loadWorkflowRunResolvedMessageTarget(ctx, tx, workspaceID, existingScopeID)
			if err != nil {
				return err
			}
			return nil
		}

		candidateScopeID, found, err := loadWorkflowRunMessageReplayScope(ctx, tx, workspaceID, authorMemberID, clientMessageID)
		if err != nil {
			return err
		}
		if !found {
			candidate, resolveErr := resolveCandidate(ctx, tx, workspaceID, authorMemberID)
			if resolveErr != nil {
				return resolveErr
			}
			candidateScopeID = candidate.ScopeID
		} else if _, err := loadWorkflowRunResolvedMessageTarget(ctx, tx, workspaceID, candidateScopeID); err != nil {
			return err
		}

		if strings.TrimSpace(candidateScopeID) == "" {
			return fmt.Errorf("resolved workflow Run message target has no canonical scope id")
		}

		if _, err := tx.Exec(ctx, `
INSERT INTO workflow_run_message_target_reservations (
  workspace_id,author_member_id,client_message_id,target_scope_id,target_ref,created_at,updated_at
) VALUES ($1,$2,$3,$4,$5,CURRENT_TIMESTAMP(6),CURRENT_TIMESTAMP(6))
ON DUPLICATE KEY UPDATE
  target_scope_id=target_scope_id
`, workspaceID, authorMemberID, clientMessageID, candidateScopeID, reservationRef); err != nil {
			return fmt.Errorf("reserve workflow Run message target: %w", err)
		}

		// A concurrent resolver may have won the unique-key race with a different
		// mutable target. Always return the committed winner, never our candidate.
		winnerScopeID, found, err := loadWorkflowRunMessageTargetReservation(ctx, tx, workspaceID, authorMemberID, clientMessageID)
		if err != nil {
			return err
		}
		if !found {
			return fmt.Errorf("workflow Run message target reservation disappeared")
		}
		reservedTarget, err = loadWorkflowRunResolvedMessageTarget(ctx, tx, workspaceID, winnerScopeID)
		return err
	})
	if err != nil {
		return domain.ResolvedMessageTarget{}, err
	}
	return reservedTarget, nil
}

func (store *Store) authorizeWorkflowRunMessageFrozenAuthor(ctx context.Context, tx coredb.Tx, workspaceID string, ownerUserID string, authorMemberID string) error {
	membership, err := store.loadAuthorizedHumanWorkspaceMemberForUpdate(ctx, tx, ownerUserID, workspaceID, workspaceAccessAction)
	if err != nil {
		if errors.Is(err, ErrForbidden) {
			return conflict("workflow Run frozen author member is unavailable")
		}
		return err
	}
	if strings.TrimSpace(membership.Member.ID) != strings.TrimSpace(authorMemberID) {
		return conflict("workflow Run frozen author member is unavailable")
	}
	return nil
}

func resolveUnreservedWorkflowRunMessageTargetScope(ctx context.Context, queryer coredb.QueryExecer, workspaceID string, authorMemberID string, parsed domain.MessageTargetReference) (domain.ResolvedMessageTarget, error) {
	switch parsed.Kind {
	case domain.MessageTargetKindTask:
		resolved, err := loadWorkflowRunResolvedMessageTarget(ctx, queryer, workspaceID, parsed.TaskScopeID)
		if err != nil {
			return domain.ResolvedMessageTarget{}, err
		}
		if resolved.Kind != domain.MessageTargetKindTask {
			return domain.ResolvedMessageTarget{}, badRequest("workflow Run task target did not resolve to a task scope")
		}
		return resolved, nil
	case domain.MessageTargetKindTeam:
		host, err := loadAgentTargetChannelByTeamID(ctx, queryer, workspaceID, parsed.Team.TeamID)
		if err != nil {
			return domain.ResolvedMessageTarget{}, err
		}
		if err := authorizeWorkflowRunMessageTeamHost(ctx, queryer, workspaceID, authorMemberID, host.ChannelID); err != nil {
			return domain.ResolvedMessageTarget{}, err
		}
		scopeID := strings.TrimSpace(host.ChannelID)
		if strings.TrimSpace(parsed.Team.ShortID) != "" {
			threadTarget, resolveErr := message.ResolveOrEnsureChannelReplyThreadTarget(
				ctx, queryer, workspaceID, host.ChannelID, host.Slug, parsed.Team.ShortID,
			)
			if resolveErr != nil {
				return domain.ResolvedMessageTarget{}, mapMessagingError(resolveErr)
			}
			scopeID = strings.TrimSpace(message.ScopeIDForThreadAccess(threadTarget))
		}
		return loadWorkflowRunResolvedMessageTarget(ctx, queryer, workspaceID, scopeID)
	case domain.MessageTargetKindChannel, domain.MessageTargetKindReplyThread, domain.MessageTargetKindDM:
	default:
		return domain.ResolvedMessageTarget{}, badRequest("unsupported workflow Run message target")
	}

	actor := message.MemberActor{
		WorkspaceID: workspaceID,
		MemberID:    authorMemberID,
	}
	var target message.ThreadAccess
	var err error
	if parsed.Kind == domain.MessageTargetKindDM && strings.TrimSpace(parsed.Thread.ShortID) == "" {
		// Match the ordinary Agent send boundary: a first send to dm:@peer may
		// create the canonical DM scope. The frozen Human is reauthorized by the
		// eventual message transaction before any visible effect is committed.
		target, err = message.EnsureDMThread(ctx, queryer, actor, parsed.Thread.PeerMemberID)
	} else {
		target, err = message.ResolveSendTarget(ctx, queryer, actor, parsed.String(), "")
	}
	if err != nil {
		return domain.ResolvedMessageTarget{}, mapMessagingError(err)
	}
	scopeID := strings.TrimSpace(message.ScopeIDForThreadAccess(target))
	if scopeID == "" {
		return domain.ResolvedMessageTarget{}, fmt.Errorf("resolved workflow Run message target has no canonical scope id")
	}
	return loadWorkflowRunResolvedMessageTarget(ctx, queryer, workspaceID, scopeID)
}

// authorizeWorkflowRunMessageTeamHost enforces the precondition of
// ResolveOrEnsureChannelReplyThreadTarget: the frozen author must already be
// an active member of the Team host before target resolution may materialize a
// reply-thread scope. The message transaction reauthorizes the Human again.
func authorizeWorkflowRunMessageTeamHost(ctx context.Context, queryer coredb.QueryRower, workspaceID string, authorMemberID string, channelID string) error {
	var marker int
	err := queryer.QueryRow(ctx, `
SELECT 1
FROM scopes s
JOIN members m
  ON m.workspace_id = s.workspace_id
 AND m.id = $2
 AND m.type = 'human'
 AND m.status = 'active'
JOIN scope_memberships sm
  ON sm.scope_id = s.id
 AND sm.member_id = m.id
 AND sm.removed_at IS NULL
 AND sm.archived_at IS NULL
 AND sm.effective_to_seq IS NULL
WHERE s.workspace_id = $1
  AND s.id = $3
  AND s.type = 'channel'
  AND s.status = 'active'
  AND s.archived_at IS NULL
LIMIT 1
FOR UPDATE
`, strings.TrimSpace(workspaceID), strings.TrimSpace(authorMemberID), strings.TrimSpace(channelID)).Scan(&marker)
	if coredb.IsNotFound(err) {
		return ErrForbidden
	}
	if err != nil {
		return fmt.Errorf("authorize workflow Run Team message host: %w", err)
	}
	return nil
}

func loadWorkflowRunResolvedMessageTarget(ctx context.Context, queryer coredb.QueryRower, workspaceID string, scopeID string) (domain.ResolvedMessageTarget, error) {
	workspaceID = strings.TrimSpace(workspaceID)
	scopeID = strings.TrimSpace(scopeID)
	if workspaceID == "" || scopeID == "" {
		return domain.ResolvedMessageTarget{}, badRequest("workflow Run message scope is required")
	}
	var scopeType string
	err := queryer.QueryRow(ctx, `
SELECT type
FROM scopes
WHERE workspace_id=$1
  AND id=$2
  AND status='active'
  AND archived_at IS NULL
LIMIT 1
`, workspaceID, scopeID).Scan(&scopeType)
	if coredb.IsNotFound(err) {
		return domain.ResolvedMessageTarget{}, ErrNotFound
	}
	if err != nil {
		return domain.ResolvedMessageTarget{}, fmt.Errorf("load workflow Run message target scope: %w", err)
	}
	var kind domain.MessageTargetKind
	switch strings.TrimSpace(scopeType) {
	case "channel":
		kind = domain.MessageTargetKindChannel
	case "thread":
		kind = domain.MessageTargetKindReplyThread
	case "dm":
		kind = domain.MessageTargetKindDM
	case "task":
		kind = domain.MessageTargetKindTask
	default:
		return domain.ResolvedMessageTarget{}, badRequest(fmt.Sprintf("workflow Run message target scope type %q is unsupported", scopeType))
	}
	return domain.ResolvedMessageTarget{Kind: kind, ScopeID: scopeID}, nil
}

func loadWorkflowRunMessageTargetReservation(ctx context.Context, queryer coredb.QueryRower, workspaceID string, authorMemberID string, clientMessageID string) (string, bool, error) {
	var scopeID string
	err := queryer.QueryRow(ctx, `
SELECT target_scope_id
FROM workflow_run_message_target_reservations
WHERE workspace_id=$1
  AND author_member_id=$2
  AND client_message_id=$3
FOR UPDATE
`, workspaceID, authorMemberID, clientMessageID).Scan(&scopeID)
	if coredb.IsNotFound(err) {
		return "", false, nil
	}
	if err != nil {
		return "", false, fmt.Errorf("load workflow Run message target reservation: %w", err)
	}
	if scopeID = strings.TrimSpace(scopeID); scopeID == "" {
		return "", false, fmt.Errorf("workflow Run message target reservation has no canonical scope id")
	}
	return scopeID, true, nil
}

func loadWorkflowRunMessageReplayScope(ctx context.Context, queryer coredb.Queryer, workspaceID string, authorMemberID string, clientMessageID string) (string, bool, error) {
	rows, err := queryer.Query(ctx, `
SELECT scope_id
FROM scope_messages
WHERE workspace_id = $1
  AND author_member_id = $2
  AND idempotency_subject_member_id = $2
  AND client_message_id = $3
ORDER BY scope_id ASC
LIMIT 2
FOR UPDATE
`, workspaceID, authorMemberID, clientMessageID)
	if err != nil {
		return "", false, fmt.Errorf("load workflow Run message replay scope: %w", err)
	}
	defer rows.Close()

	var scopeIDs []string
	for rows.Next() {
		var scopeID string
		if err := rows.Scan(&scopeID); err != nil {
			return "", false, fmt.Errorf("scan workflow Run message replay scope: %w", err)
		}
		if scopeID = strings.TrimSpace(scopeID); scopeID != "" {
			scopeIDs = append(scopeIDs, scopeID)
		}
	}
	if err := rows.Err(); err != nil {
		return "", false, fmt.Errorf("iterate workflow Run message replay scopes: %w", err)
	}
	switch len(scopeIDs) {
	case 0:
		return "", false, nil
	case 1:
		return scopeIDs[0], true, nil
	default:
		return "", false, fmt.Errorf("workflow Run clientMessageId resolves to multiple message scopes")
	}
}
