package app

import (
	"context"
	"fmt"
	"strings"

	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/pinyinsearch"
	"github.com/tidbcloud/AgentTeam/internal/store/agent"
)

const workflowAgentEditProfileLimit = 50

// WorkflowAgentEditAgent is the exact canonical Agent identity used by the
// Workflow Builder product adapter after the ordinary workspace Agent
// directory has supplied candidates.
type WorkflowAgentEditAgent struct {
	ID              string
	Name            string
	MemberID        string
	DisplayName     string
	AvatarURL       string
	Description     string
	MachineID       string
	AutoWakeEnabled bool
}

type WorkflowAgentEditProfile struct {
	ID          string
	Name        string
	DisplayName string
	AvatarURL   string
}

// LoadOwnedWorkflowAgentEditAgent resolves the POST-selected Agent by exact ID
// inside the caller's Plan transaction and revalidates that the authenticated
// Human owns its backing Machine. It must not authorize by scanning an
// autocomplete page.
func (store *Store) LoadOwnedWorkflowAgentEditAgent(ctx context.Context, queryer coredb.Queryer, userID, workspaceID, agentID string) (WorkflowAgentEditAgent, error) {
	if store == nil || queryer == nil {
		return WorkflowAgentEditAgent{}, fmt.Errorf("workflow Agent resolver is not configured")
	}
	var value WorkflowAgentEditAgent
	err := queryer.QueryRow(ctx, `
SELECT a.id, a.name, a.member_id, m.display_name, a.avatar_url, a.description,
       a.machine_id, a.auto_wake_enabled
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
  AND mac.created_by_user_id = $3
  AND a.status <> 'archived'
FOR UPDATE
`, strings.TrimSpace(workspaceID), strings.TrimSpace(agentID), strings.TrimSpace(userID)).Scan(
		&value.ID, &value.Name, &value.MemberID, &value.DisplayName,
		&value.AvatarURL, &value.Description, &value.MachineID, &value.AutoWakeEnabled,
	)
	if coredb.IsNotFound(err) {
		return WorkflowAgentEditAgent{}, ErrNotFound
	}
	if err != nil {
		return WorkflowAgentEditAgent{}, fmt.Errorf("load workflow Agent edit target: %w", err)
	}
	return value, nil
}

// LoadWorkflowAgentReplyAuthor resolves one authenticated Agent's current
// presentation without imposing candidate-picker Machine availability. The
// caller separately proves that this exact member participates in the
// Workflow Scope and owns the active request.
func (store *Store) LoadWorkflowAgentReplyAuthor(
	ctx context.Context,
	queryer coredb.Queryer,
	workspaceID, agentID, memberID string,
) (WorkflowAgentEditAgent, error) {
	if store == nil || queryer == nil {
		return WorkflowAgentEditAgent{}, fmt.Errorf("workflow Agent reply resolver is not configured")
	}
	var value WorkflowAgentEditAgent
	err := queryer.QueryRow(ctx, `
SELECT a.id, a.name, a.member_id, m.display_name, a.avatar_url, a.description,
       a.machine_id, a.auto_wake_enabled
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'
WHERE a.workspace_id = $1
  AND a.id = $2
  AND a.member_id = $3
  AND a.status <> 'archived'
`, strings.TrimSpace(workspaceID), strings.TrimSpace(agentID), strings.TrimSpace(memberID)).Scan(
		&value.ID, &value.Name, &value.MemberID, &value.DisplayName,
		&value.AvatarURL, &value.Description, &value.MachineID, &value.AutoWakeEnabled,
	)
	if coredb.IsNotFound(err) {
		return WorkflowAgentEditAgent{}, ErrNotFound
	}
	if err != nil {
		return WorkflowAgentEditAgent{}, fmt.Errorf("load workflow Agent reply author: %w", err)
	}
	return value, nil
}

// LoadWorkflowAgentEditProfiles hydrates only the bounded Agent IDs present in
// one Workflow message page. Historical profiles remain readable after an
// Agent or Machine becomes inactive; this method does not grant selection or
// delivery eligibility.
func (store *Store) LoadWorkflowAgentEditProfiles(ctx context.Context, workspaceID string, agentIDs []string) (map[string]WorkflowAgentEditProfile, error) {
	if store == nil || store.db == nil {
		return nil, fmt.Errorf("workflow Agent profile resolver is not configured")
	}
	workspaceID = strings.TrimSpace(workspaceID)
	uniqueIDs := make([]string, 0, len(agentIDs))
	seen := make(map[string]struct{}, len(agentIDs))
	for _, agentID := range agentIDs {
		agentID = strings.TrimSpace(agentID)
		if agentID == "" {
			continue
		}
		if _, exists := seen[agentID]; exists {
			continue
		}
		seen[agentID] = struct{}{}
		uniqueIDs = append(uniqueIDs, agentID)
	}
	if workspaceID == "" || len(uniqueIDs) == 0 {
		return map[string]WorkflowAgentEditProfile{}, nil
	}
	if len(uniqueIDs) > workflowAgentEditProfileLimit {
		return nil, fmt.Errorf("workflow Agent profile page exceeds %d ids", workflowAgentEditProfileLimit)
	}
	args := make([]any, 0, len(uniqueIDs)+1)
	args = append(args, workspaceID)
	placeholders := make([]string, 0, len(uniqueIDs))
	for _, agentID := range uniqueIDs {
		args = append(args, agentID)
		placeholders = append(placeholders, fmt.Sprintf("$%d", len(args)))
	}
	rows, err := store.db.Query(ctx, `
SELECT a.id, a.name, COALESCE(NULLIF(m.display_name, ''), a.name), a.avatar_url
FROM agents a
JOIN members m
  ON m.workspace_id = a.workspace_id
 AND m.id = a.member_id
 AND m.type = 'agent'
WHERE a.workspace_id = $1
  AND a.id IN (`+strings.Join(placeholders, ", ")+`)
`, args...)
	if err != nil {
		return nil, fmt.Errorf("load workflow Agent edit profiles: %w", err)
	}
	defer rows.Close()
	profiles := make(map[string]WorkflowAgentEditProfile, len(uniqueIDs))
	for rows.Next() {
		var profile WorkflowAgentEditProfile
		if err := rows.Scan(&profile.ID, &profile.Name, &profile.DisplayName, &profile.AvatarURL); err != nil {
			return nil, fmt.Errorf("scan workflow Agent edit profile: %w", err)
		}
		profiles[profile.ID] = profile
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate workflow Agent edit profiles: %w", err)
	}
	return profiles, nil
}

func (store *Store) ListAgentDirectoryForWorkspace(ctx context.Context, userID string, workspaceID string, options agent.AgentDirectoryOptions) (agent.AgentDirectoryPage, error) {
	workspaceMember, err := store.loadRuntimeWorkspaceMember(ctx, store.db, userID, workspaceID)
	if err != nil {
		return agent.AgentDirectoryPage{}, err
	}

	normalized, err := normalizeAgentDirectoryOptions(options)
	if err != nil {
		return agent.AgentDirectoryPage{}, err
	}
	cursor, err := decodeAgentDirectoryCursor(normalized.Cursor)
	if err != nil {
		return agent.AgentDirectoryPage{}, err
	}

	scopeFilter := ""
	if normalized.Scope == "owned" || normalized.Scope == "manageable" {
		scopeFilter = "  AND mac.created_by_user_id = $3\n"
	}
	queryPrefix := ""
	pinyinPattern := ""
	if normalized.Query != "" {
		queryPrefix = strings.ToLower(normalized.Query) + "%"
		if normalizedQuery := pinyinsearch.NormalizeQuery(normalized.Query); normalizedQuery != "" {
			pinyinPattern = normalizedQuery + "%"
		}
	}
	teamAssignmentSQL := agentDirectoryTeamAssignmentSQL(normalized.IncludeTeamAssignment)

	rows, err := store.db.Query(ctx, `
SELECT
       a.id,
       a.name,
       m.id,
       m.display_name,
       a.avatar_url,
       mac.created_by_user_id,
       a.machine_id,
       mac.created_by_user_id = $3 AS can_manage,
       `+agentDirectoryReviewSelect(normalized.IncludeReview)+`,
       `+teamAssignmentSQL.selectClause+`
FROM agents a
JOIN members m
  ON m.id = a.member_id
 AND m.workspace_id = a.workspace_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
`+teamAssignmentSQL.joinClause+`
WHERE a.workspace_id = $1
  AND a.status <> 'archived'
  AND mac.status = 'active'
  AND (($5 = '' AND $6 = '') OR (m.display_name > $5 OR (m.display_name = $5 AND a.id > $6)))
  AND ($7 = '' OR LOWER(a.name) LIKE $7 OR LOWER(m.display_name) LIKE $7
       OR ($8 <> '' AND (LOWER(a.name_pinyin) LIKE $8 OR LOWER(a.name_pinyin_initials) LIKE $8
       OR LOWER(m.display_name_pinyin) LIKE $8 OR LOWER(m.display_name_pinyin_initials) LIKE $8)))
`+scopeFilter+`
ORDER BY m.display_name ASC, a.id ASC
LIMIT $4
`,
		strings.TrimSpace(workspaceID),
		workspaceMember.Member.ID,
		strings.TrimSpace(userID),
		normalized.Limit+1,
		cursor.DisplayName,
		cursor.AgentID,
		queryPrefix,
		pinyinPattern,
	)
	if err != nil {
		return agent.AgentDirectoryPage{}, fmt.Errorf("list agent directory: %w", err)
	}
	defer rows.Close()

	items := make([]agent.AgentDirectoryItem, 0, normalized.Limit+1)
	for rows.Next() {
		var item agent.AgentDirectoryItem
		if err := rows.Scan(
			&item.ID,
			&item.Name,
			&item.MemberID,
			&item.DisplayName,
			&item.AvatarURL,
			&item.OwnerUserID,
			&item.MachineID,
			&item.CanManage,
			&item.CanReview,
			&item.TeamID,
			&item.TeamDisplayName,
		); err != nil {
			return agent.AgentDirectoryPage{}, fmt.Errorf("scan agent directory: %w", err)
		}
		items = append(items, item)
	}
	if err := rows.Err(); err != nil {
		return agent.AgentDirectoryPage{}, fmt.Errorf("iterate agent directory: %w", err)
	}
	if normalized.IncludeCurrentRoleBinding {
		agentIDs := make([]string, 0, len(items))
		for _, item := range items {
			agentIDs = append(agentIDs, item.ID)
		}
		bindings, err := store.loadAgentRoleBindingSummaries(ctx, workspaceID, agentIDs)
		if err != nil {
			return agent.AgentDirectoryPage{}, err
		}
		for index := range items {
			items[index].CurrentRoleBinding = bindings[strings.TrimSpace(items[index].ID)]
		}
	}

	hasMore := len(items) > normalized.Limit
	if hasMore {
		items = items[:normalized.Limit]
	}
	nextCursor := ""
	if hasMore && len(items) > 0 {
		last := items[len(items)-1]
		nextCursor, err = encodeAgentDirectoryCursor(agentDirectoryCursor{
			DisplayName: last.DisplayName,
			AgentID:     last.ID,
		})
		if err != nil {
			return agent.AgentDirectoryPage{}, err
		}
	}
	return agent.AgentDirectoryPage{
		Agents:     items,
		Limit:      normalized.Limit,
		Cursor:     normalized.Cursor,
		HasMore:    hasMore,
		NextCursor: nextCursor,
	}, nil
}

// LookupAgentDirectoryForWorkspace resolves a bounded set of exact Agent IDs
// through the same active Workspace directory contract used by the picker.
// It is presentation-only: archived Agents, inactive members, and Agents on
// inactive Machines remain unresolved rather than becoming selectable again.
func (store *Store) LookupAgentDirectoryForWorkspace(
	ctx context.Context,
	userID string,
	workspaceID string,
	agentIDs []string,
) ([]agent.AgentDirectoryItem, error) {
	if _, err := store.loadRuntimeWorkspaceMember(ctx, store.db, userID, workspaceID); err != nil {
		return nil, err
	}
	uniqueIDs := make([]string, 0, len(agentIDs))
	seen := make(map[string]struct{}, len(agentIDs))
	for _, agentID := range agentIDs {
		agentID = strings.TrimSpace(agentID)
		if agentID == "" {
			continue
		}
		if _, exists := seen[agentID]; exists {
			continue
		}
		seen[agentID] = struct{}{}
		uniqueIDs = append(uniqueIDs, agentID)
	}
	if len(uniqueIDs) == 0 {
		return []agent.AgentDirectoryItem{}, nil
	}
	if len(uniqueIDs) > agent.MaxAgentDirectoryLookupIDs {
		return nil, badRequest(fmt.Sprintf(
			"agent directory lookup must not exceed %d ids",
			agent.MaxAgentDirectoryLookupIDs,
		))
	}
	requestedAgentsCTE, requestedAgentArgs, _, err := coredb.BuildIndexedStringValuesCTE(
		"requested_agents",
		"agent_id",
		"ordinality",
		uniqueIDs,
		2,
	)
	if err != nil {
		return nil, err
	}
	args := make([]any, 0, len(requestedAgentArgs)+1)
	args = append(args, strings.TrimSpace(workspaceID))
	args = append(args, requestedAgentArgs...)
	rows, err := store.db.Query(ctx, `
WITH `+requestedAgentsCTE+`
SELECT a.id, a.name, m.id, m.display_name, a.avatar_url, a.machine_id
FROM requested_agents requested
JOIN agents a
  ON a.workspace_id = $1
 AND a.id = requested.agent_id
 AND a.status <> 'archived'
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'
ORDER BY requested.ordinality
`, args...)
	if err != nil {
		return nil, fmt.Errorf("lookup agent directory: %w", err)
	}
	defer rows.Close()

	items := make([]agent.AgentDirectoryItem, 0, len(uniqueIDs))
	for rows.Next() {
		var item agent.AgentDirectoryItem
		if err := rows.Scan(
			&item.ID,
			&item.Name,
			&item.MemberID,
			&item.DisplayName,
			&item.AvatarURL,
			&item.MachineID,
		); err != nil {
			return nil, fmt.Errorf("scan agent directory lookup: %w", err)
		}
		items = append(items, item)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate agent directory lookup: %w", err)
	}
	return items, nil
}

func agentDirectoryReviewSelect(includeReview bool) string {
	if !includeReview {
		return "FALSE AS can_review"
	}
	return `(mac.created_by_user_id = $3) AS can_review`
}

type agentDirectoryTeamAssignmentSQLParts struct {
	selectClause string
	joinClause   string
}

func agentDirectoryTeamAssignmentSQL(includeTeamAssignment bool) agentDirectoryTeamAssignmentSQLParts {
	if !includeTeamAssignment {
		return agentDirectoryTeamAssignmentSQLParts{
			selectClause: "'' AS team_id, '' AS team_display_name",
		}
	}
	return agentDirectoryTeamAssignmentSQLParts{
		selectClause: `COALESCE(team_assignment.team_id, '') AS team_id,
       COALESCE(team_assignment.team_display_name, '') AS team_display_name`,
		joinClause: `
LEFT JOIN (
    SELECT tm.workspace_id, tm.actor_id, t.id AS team_id,
           COALESCE(NULLIF(t.display_name, ''), t.handle, '') AS team_display_name
    FROM workspace_team_members tm
    JOIN workspace_teams t
      ON t.workspace_id = tm.workspace_id
     AND t.id = tm.team_id
     AND t.status = 'active'
    WHERE tm.actor_kind = 'agent'
      AND tm.removed_at IS NULL
) team_assignment
  ON team_assignment.workspace_id = a.workspace_id
 AND team_assignment.actor_id = m.id`,
	}
}

func normalizeAgentDirectoryOptions(options agent.AgentDirectoryOptions) (agent.AgentDirectoryOptions, error) {
	scope := strings.ToLower(strings.TrimSpace(options.Scope))
	if scope == "" {
		scope = "workspace"
	}
	switch scope {
	case "owned", "manageable", "workspace":
	default:
		return agent.AgentDirectoryOptions{}, badRequest("invalid agent directory scope")
	}

	query := strings.TrimSpace(options.Query)
	defaultLimit := agent.DefaultAgentDirectoryLimit
	maxLimit := agent.MaxAgentDirectoryLimit
	if query != "" {
		defaultLimit = agent.DefaultAgentSearchLimit
		maxLimit = agent.MaxAgentSearchLimit
	}
	limit := options.Limit
	switch {
	case limit <= 0:
		limit = defaultLimit
	case limit > maxLimit:
		limit = maxLimit
	}

	return agent.AgentDirectoryOptions{
		Scope:                     scope,
		Limit:                     limit,
		Cursor:                    strings.TrimSpace(options.Cursor),
		Query:                     query,
		IncludeReview:             options.IncludeReview,
		IncludeTeamAssignment:     options.IncludeTeamAssignment,
		IncludeCurrentRoleBinding: options.IncludeCurrentRoleBinding,
	}, nil
}
