package taskboard

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

	"github.com/tidbcloud/AgentTeam/internal/coredb"
)

const (
	defaultPrivateAgentMentionCandidateLimit     = 50
	maxPrivateAgentMentionCandidateLimit         = 50
	maxPrivateAgentMentionCandidateScanLimit     = 200
	privateAgentMentionCandidateDefaultOverfetch = 4
)

type privateAgentMentionCandidateSourceScope struct {
	ScopeType string
	ScopeID   string
}

func (txs TxStore) ListPrivateAgentMentionCandidates(ctx context.Context, request ListPrivateAgentMentionCandidatesRequest) (ListPrivateAgentMentionCandidatesResponse, error) {
	workspaceID, err := request.Context.requireWorkspaceID()
	if err != nil {
		return ListPrivateAgentMentionCandidatesResponse{}, err
	}
	userID, _, err := request.Context.requireUser()
	if err != nil {
		return ListPrivateAgentMentionCandidatesResponse{}, err
	}
	scopeType, err := normalizeTaskCollaboratorScope(request.Scope.ScopeType)
	if err != nil {
		return ListPrivateAgentMentionCandidatesResponse{}, err
	}
	if scopeType != taskCollaboratorScopeChannel && scopeType != taskCollaboratorScopeThread {
		return ListPrivateAgentMentionCandidatesResponse{}, badRequest("private agent mention candidates require channel or thread scope")
	}

	sourceTarget, viewerMemberID, err := txs.resolveTaskCollaboratorTargetForUser(ctx, txs.db, userID, workspaceID, TaskRef{}, request.Scope, taskReadChannelAction)
	if err != nil {
		return ListPrivateAgentMentionCandidatesResponse{}, err
	}
	hostChannelID, err := txs.privateAgentMentionCandidateHostChannelID(ctx, txs.db, sourceTarget)
	if err != nil {
		return ListPrivateAgentMentionCandidatesResponse{}, err
	}
	if strings.TrimSpace(hostChannelID) == "" {
		return ListPrivateAgentMentionCandidatesResponse{}, nil
	}

	returnLimit, scanLimit := normalizePrivateAgentMentionCandidateLimits(request.Limit, request.CandidateScanLimit)
	candidates, err := loadPrivateAgentMentionCandidates(ctx, txs.db, workspaceID, hostChannelID, request.Prefix, viewerMemberID, scanLimit)
	if err != nil || len(candidates) == 0 {
		return ListPrivateAgentMentionCandidatesResponse{}, err
	}

	nonOwnerCandidateOwners := privateAgentMentionCandidateNonOwnerIDs(candidates, viewerMemberID)
	grantedOwners, err := txs.loadPrivateAgentMentionCandidatePeopleGrantRelations(ctx, txs.db, workspaceID, viewerMemberID, sourceTarget, nonOwnerCandidateOwners, time.Now().UTC())
	if err != nil {
		return ListPrivateAgentMentionCandidatesResponse{}, err
	}

	filtered := make([]PrivateAgentMentionCandidate, 0, minInt(returnLimit, len(candidates)))
	for _, candidate := range candidates {
		ownerMemberID := strings.TrimSpace(candidate.OwnerMemberID)
		if ownerMemberID == "" {
			continue
		}
		if ownerMemberID == strings.TrimSpace(viewerMemberID) {
			candidate.Relation = "owner"
			candidate.RelationSource = &PrivateAgentContextRelationSource{Kind: "owner", ScopeType: sourceTarget.ScopeType, ScopeID: sourceTarget.ScopeID}
		} else {
			relation, ok := grantedOwners[ownerMemberID]
			if !ok {
				continue
			}
			candidate.Relation = "collaborator"
			candidate.RelationSource = &relation
		}
		filtered = append(filtered, candidate)
		if len(filtered) >= returnLimit {
			break
		}
	}

	return ListPrivateAgentMentionCandidatesResponse{Candidates: filtered}, nil
}

func normalizePrivateAgentMentionCandidateLimits(requestedLimit int, requestedScanLimit int) (int, int) {
	returnLimit := requestedLimit
	if returnLimit <= 0 {
		returnLimit = defaultPrivateAgentMentionCandidateLimit
	}
	if returnLimit > maxPrivateAgentMentionCandidateLimit {
		returnLimit = maxPrivateAgentMentionCandidateLimit
	}

	scanLimit := requestedScanLimit
	if scanLimit <= 0 {
		scanLimit = returnLimit * privateAgentMentionCandidateDefaultOverfetch
	}
	if scanLimit < returnLimit {
		scanLimit = returnLimit
	}
	if scanLimit > maxPrivateAgentMentionCandidateScanLimit {
		scanLimit = maxPrivateAgentMentionCandidateScanLimit
	}
	return returnLimit, scanLimit
}

func (txs TxStore) privateAgentMentionCandidateHostChannelID(ctx context.Context, queryer rowQueryer, target taskCollaboratorTarget) (string, error) {
	switch target.ScopeType {
	case taskCollaboratorScopeChannel, taskCollaboratorScopeTask:
		return strings.TrimSpace(target.ChannelID), nil
	case taskCollaboratorScopeThread:
		return loadTaskCollaboratorThreadParentChannelID(ctx, queryer, target.WorkspaceID, target.ThreadChannelID)
	default:
		return "", badRequest("private agent mention candidates require channel or thread scope")
	}
}

func loadPrivateAgentMentionCandidates(ctx context.Context, queryer coredb.Queryer, workspaceID string, hostChannelID string, prefix string, viewerMemberID string, scanLimit int) ([]PrivateAgentMentionCandidate, error) {
	normalizedPrefix := strings.ToLower(strings.TrimSpace(prefix))

	// Keep this in sync with the frontend mention candidate matcher: the lazy
	// private-agent query matches field starts and token-boundary prefixes, not
	// arbitrary substrings, so it stays bounded while finding names such as
	// "looper-devel-codex1" from "codex".
	rows, err := queryer.Query(ctx, `
SELECT
    a.id,
    a.name,
    COALESCE(NULLIF(agent_member.display_name, ''), NULLIF(a.name, ''), a.member_id),
    a.member_id,
    COALESCE(a.avatar_url, ''),
    owner_member.type,
    owner_member.id,
    owner_user.id,
    owner_user.display_name,
    owner_member.display_name,
    owner_user.name
FROM scope_memberships sm
JOIN scopes host_scope
  ON host_scope.id = sm.scope_id
 AND host_scope.workspace_id = $1
 AND host_scope.status = 'active'
JOIN agents a
  ON a.workspace_id = host_scope.workspace_id
 AND a.member_id = sm.member_id
 AND a.status <> 'archived'
JOIN machines mac
  ON mac.workspace_id = a.workspace_id
 AND mac.id = a.machine_id
 AND mac.status = 'active'
JOIN members agent_member
  ON agent_member.id = sm.member_id
JOIN members owner_member
  ON owner_member.id = sm.owner_member_id
LEFT JOIN users owner_user
  ON owner_user.id = owner_member.user_id
WHERE sm.scope_id = $2
  AND sm.visibility = 'private'
  AND sm.owner_member_id IS NOT NULL
  AND sm.removed_at IS NULL
  AND ($3 = ''
       OR LOCATE($3, LOWER(a.name)) = 1
       OR LOCATE(CONCAT('-', $3), LOWER(a.name)) > 0
       OR LOCATE(CONCAT('_', $3), LOWER(a.name)) > 0
       OR LOCATE(CONCAT(' ', $3), LOWER(a.name)) > 0
       OR LOCATE(CONCAT('.', $3), LOWER(a.name)) > 0
       OR LOCATE(CONCAT('/', $3), LOWER(a.name)) > 0
       OR LOCATE(CONCAT(':', $3), LOWER(a.name)) > 0
       OR LOCATE($3, LOWER(a.member_id)) = 1
       OR LOCATE(CONCAT('-', $3), LOWER(a.member_id)) > 0
       OR LOCATE(CONCAT('_', $3), LOWER(a.member_id)) > 0
       OR LOCATE(CONCAT(' ', $3), LOWER(a.member_id)) > 0
       OR LOCATE(CONCAT('.', $3), LOWER(a.member_id)) > 0
       OR LOCATE(CONCAT('/', $3), LOWER(a.member_id)) > 0
       OR LOCATE(CONCAT(':', $3), LOWER(a.member_id)) > 0
       OR LOCATE($3, LOWER(COALESCE(NULLIF(agent_member.display_name, ''), ''))) = 1
       OR LOCATE(CONCAT('-', $3), LOWER(COALESCE(NULLIF(agent_member.display_name, ''), ''))) > 0
       OR LOCATE(CONCAT('_', $3), LOWER(COALESCE(NULLIF(agent_member.display_name, ''), ''))) > 0
       OR LOCATE(CONCAT(' ', $3), LOWER(COALESCE(NULLIF(agent_member.display_name, ''), ''))) > 0
       OR LOCATE(CONCAT('.', $3), LOWER(COALESCE(NULLIF(agent_member.display_name, ''), ''))) > 0
       OR LOCATE(CONCAT('/', $3), LOWER(COALESCE(NULLIF(agent_member.display_name, ''), ''))) > 0
       OR LOCATE(CONCAT(':', $3), LOWER(COALESCE(NULLIF(agent_member.display_name, ''), ''))) > 0)
ORDER BY CASE WHEN sm.owner_member_id = $4 THEN 0 ELSE 1 END,
         sm.owner_member_id ASC,
         sm.created_at ASC,
         a.id ASC
LIMIT $5
`, strings.TrimSpace(workspaceID), strings.TrimSpace(hostChannelID), normalizedPrefix, strings.TrimSpace(viewerMemberID), scanLimit)
	if err != nil {
		return nil, fmt.Errorf("list private agent mention candidates: %w", err)
	}
	defer rows.Close()

	candidates := make([]PrivateAgentMentionCandidate, 0)
	for rows.Next() {
		var candidate PrivateAgentMentionCandidate
		var ownerProjection taskActorProjection
		if err := rows.Scan(
			&candidate.ID,
			&candidate.Name,
			&candidate.DisplayName,
			&candidate.MemberID,
			&candidate.AvatarURL,
			&ownerProjection.MemberType,
			&ownerProjection.MemberID,
			&ownerProjection.UserID,
			&ownerProjection.UserDisplayName,
			&ownerProjection.MemberDisplayName,
			&ownerProjection.UserName,
		); err != nil {
			return nil, fmt.Errorf("scan private agent mention candidate: %w", err)
		}
		ownerProfile, err := taskPrincipalProfileFromProjection("private agent mention owner", ownerProjection, true)
		if err != nil {
			return nil, fmt.Errorf("private agent mention owner profile: %w", err)
		}
		candidate.OwnerProfile = ownerProfile
		privateAgentMentionCandidateDeriveOwnerCompat(&candidate)
		candidates = append(candidates, candidate)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate private agent mention candidates: %w", err)
	}
	return candidates, nil
}

func privateAgentMentionCandidateDeriveOwnerCompat(candidate *PrivateAgentMentionCandidate) {
	if candidate == nil || candidate.OwnerProfile == nil {
		return
	}
	profile := candidate.OwnerProfile.Normalize()
	candidate.OwnerMemberID = firstTaskString(profile.MemberID, candidate.OwnerMemberID)
	candidate.OwnerUserID = firstTaskString(profile.PrincipalID, candidate.OwnerUserID)
	candidate.OwnerDisplayName = firstTaskString(profile.DisplayName, candidate.OwnerDisplayName)
	candidate.OwnerName = firstTaskString(profile.DisplayName, candidate.OwnerName)
}

func privateAgentMentionCandidateNonOwnerIDs(candidates []PrivateAgentMentionCandidate, viewerMemberID string) []string {
	trimmedViewerMemberID := strings.TrimSpace(viewerMemberID)
	seen := make(map[string]struct{}, len(candidates))
	ownerIDs := make([]string, 0, len(candidates))
	for _, candidate := range candidates {
		ownerMemberID := strings.TrimSpace(candidate.OwnerMemberID)
		if ownerMemberID == "" || ownerMemberID == trimmedViewerMemberID {
			continue
		}
		if _, ok := seen[ownerMemberID]; ok {
			continue
		}
		seen[ownerMemberID] = struct{}{}
		ownerIDs = append(ownerIDs, ownerMemberID)
	}
	return ownerIDs
}

func (txs TxStore) loadPrivateAgentMentionCandidatePeopleGrantRelations(ctx context.Context, queryer coredb.Queryer, workspaceID string, viewerMemberID string, sourceTarget taskCollaboratorTarget, ownerMemberIDs []string, now time.Time) (map[string]PrivateAgentContextRelationSource, error) {
	ownerMemberIDs = normalizeUniqueStringList(ownerMemberIDs)
	if len(ownerMemberIDs) == 0 {
		return map[string]PrivateAgentContextRelationSource{}, nil
	}
	sourceScopes, err := txs.privateAgentMentionCandidateSourceScopes(ctx, queryer, sourceTarget)
	if err != nil {
		return nil, err
	}
	if len(sourceScopes) == 0 {
		return map[string]PrivateAgentContextRelationSource{}, nil
	}

	ownersCTE, ownerArgs, nextParam, err := coredb.BuildStringValuesCTE("candidate_owners", "owner_member_id", ownerMemberIDs, 4)
	if err != nil {
		return nil, err
	}
	scopeTypes := make([]string, 0, len(sourceScopes))
	scopeIDs := make([]string, 0, len(sourceScopes))
	for _, scope := range sourceScopes {
		scopeTypes = append(scopeTypes, scope.ScopeType)
		scopeIDs = append(scopeIDs, scope.ScopeID)
	}
	scopesCTE, scopeArgs, _, err := coredb.BuildStringPairsCTE("source_scopes", "scope_type", "scope_id", scopeTypes, scopeIDs, nextParam)
	if err != nil {
		return nil, err
	}

	query := fmt.Sprintf(`
WITH %s,
%s
SELECT tse.owner_member_id, tse.scope_type, tse.scope_id, tspm.id
FROM task_scope_executions tse
JOIN candidate_owners co
  ON co.owner_member_id = tse.owner_member_id
JOIN source_scopes ss
  ON ss.scope_type = tse.scope_type
 AND ss.scope_id = tse.scope_id
JOIN task_scope_participant_metadata tspm
  ON tspm.owner_scope_execution_id = tse.id
 AND tspm.member_id = $2
 AND tspm.removed_at IS NULL
 AND tspm.left_at IS NULL
WHERE tse.workspace_id = $1
  AND (tspm.expires_at IS NULL OR tspm.expires_at > $3)
ORDER BY CASE tse.scope_type
    WHEN 'task' THEN 0
    WHEN 'thread' THEN 1
    WHEN 'channel' THEN 2
    ELSE 3
  END,
  tspm.added_at ASC,
  tspm.id ASC
`, ownersCTE, scopesCTE)
	args := make([]any, 0, 3+len(ownerArgs)+len(scopeArgs))
	args = append(args, strings.TrimSpace(workspaceID), strings.TrimSpace(viewerMemberID), now)
	args = append(args, ownerArgs...)
	args = append(args, scopeArgs...)

	rows, err := queryer.Query(ctx, query, args...)
	if err != nil {
		return nil, fmt.Errorf("load private agent mention candidate grants: %w", err)
	}
	defer rows.Close()

	granted := make(map[string]PrivateAgentContextRelationSource, len(ownerMemberIDs))
	for rows.Next() {
		var ownerMemberID string
		var scopeType string
		var scopeID string
		var relationID string
		if err := rows.Scan(&ownerMemberID, &scopeType, &scopeID, &relationID); err != nil {
			return nil, fmt.Errorf("scan private agent mention candidate grant: %w", err)
		}
		ownerMemberID = strings.TrimSpace(ownerMemberID)
		if ownerMemberID != "" {
			if _, exists := granted[ownerMemberID]; !exists {
				granted[ownerMemberID] = PrivateAgentContextRelationSource{
					Kind:       "people",
					ScopeType:  strings.TrimSpace(scopeType),
					ScopeID:    strings.TrimSpace(scopeID),
					RelationID: strings.TrimSpace(relationID),
				}
			}
		}
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate private agent mention candidate grants: %w", err)
	}
	return granted, nil
}

func (txs TxStore) loadPrivateAgentMentionCandidatePeopleRelation(ctx context.Context, queryer coredb.Queryer, workspaceID string, viewerMemberID string, sourceTarget taskCollaboratorTarget, ownerMemberID string, now time.Time) (*PrivateAgentContextRelationSource, error) {
	relations, err := txs.loadPrivateAgentMentionCandidatePeopleGrantRelations(ctx, queryer, workspaceID, viewerMemberID, sourceTarget, []string{ownerMemberID}, now)
	if err != nil {
		return nil, err
	}
	relation, ok := relations[strings.TrimSpace(ownerMemberID)]
	if !ok {
		return nil, nil
	}
	return &relation, nil
}

func (txs TxStore) privateAgentMentionCandidateSourceScopes(ctx context.Context, queryer rowQueryer, sourceTarget taskCollaboratorTarget) ([]privateAgentMentionCandidateSourceScope, error) {
	sourceTargets, err := txs.privateAgentContextSourceTargets(ctx, queryer, sourceTarget)
	if err != nil {
		return nil, err
	}
	seen := make(map[string]struct{}, len(sourceTargets))
	scopes := make([]privateAgentMentionCandidateSourceScope, 0, len(sourceTargets))
	for _, target := range sourceTargets {
		scopeType := strings.TrimSpace(target.ScopeType)
		scopeID := strings.TrimSpace(target.ScopeID)
		if scopeType == "" || scopeID == "" {
			continue
		}
		key := taskCollaboratorScopeKey(scopeType, scopeID)
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		scopes = append(scopes, privateAgentMentionCandidateSourceScope{
			ScopeType: scopeType,
			ScopeID:   scopeID,
		})
	}
	return scopes, nil
}

func minInt(left int, right int) int {
	if left < right {
		return left
	}
	return right
}
