package app

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

	"github.com/tidbcloud/AgentTeam/internal/core/catalog"
	"github.com/tidbcloud/AgentTeam/internal/core/identifiers"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/pinyinsearch"
	"github.com/tidbcloud/AgentTeam/internal/store/agent"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
	"github.com/tidbcloud/AgentTeam/internal/store/privateagent"
)

const (
	agentWorkspaceNameConflictMessage       = "an agent with that name already exists in this workspace"
	agentWorkspaceNameInvalidFormatMessage  = "name must start with a letter and contain only letters, numbers, underscores, or hyphens"
	agentWorkspaceNameMaxLength             = 32
	membersWorkspaceAgentDisplayNameKeyName = "members_workspace_agent_display_name_key"
	agentsWorkspaceNameKeyName              = "agents_workspace_name_key"
)

var errAgentDisplayNameConflict = errors.New("agent display name conflict")

// CreateAgentForUser materializes a browser-managed workspace agent after
// validating the caller can manage the target runtime machine.
func (store *Store) CreateAgentForUser(ctx context.Context, userID string, workspaceID string, machineID string, params agent.CreateAgentParams) (agent.AgentView, error) {
	if _, err := store.requireMachineManagement(ctx, userID, workspaceID, machineID); err != nil {
		return agent.AgentView{}, err
	}

	tx, err := store.db.BeginTx(ctx, coredb.TxOptions{})
	if err != nil {
		return agent.AgentView{}, fmt.Errorf("begin create agent tx: %w", err)
	}
	defer rollback(tx, ctx)

	agentView, memberEvents, err := store.createAgentInWorkspace(ctx, tx, userID, workspaceID, machineID, params)
	if err != nil {
		return agent.AgentView{}, err
	}

	if err := tx.Commit(ctx); err != nil {
		return agent.AgentView{}, fmt.Errorf("commit create agent: %w", err)
	}
	store.emitCreatedAgentChannelMemberEvents(ctx, workspaceID, memberEvents)
	return agentView, nil
}

// CreateWorkspaceAgentInTx materializes an agent inside a caller-owned
// transaction. It is the narrow org/service agent creation port; callers own
// any higher-level authorization before invoking it. The returned member events
// are transaction-bound handoffs: discard them on rollback, or publish each one
// through EmitCommittedMessageProjectionOnly only after the caller commits.
func (store *Store) CreateWorkspaceAgentInTx(ctx context.Context, tx coredb.Tx, workspaceID string, machineID string, params agent.CreateAgentParams) (agent.AgentView, []message.ChannelMemberEventResult, error) {
	return store.createAgentInWorkspace(ctx, tx, "", workspaceID, machineID, params)
}

func (store *Store) emitCreatedAgentChannelMemberEvents(ctx context.Context, workspaceID string, results []message.ChannelMemberEventResult) {
	if store == nil || len(results) == 0 {
		return
	}
	store.committedMessageFanoutMu.RLock()
	fanout := store.committedMessageFanout
	store.committedMessageFanoutMu.RUnlock()
	if fanout == nil {
		store.missingProjectionFanoutLogOnce.Do(func() {
			store.committedMessageLogger().Error(
				"committed message fanout is not configured",
				"path", "create_agent_channel_membership",
				"workspaceID", strings.TrimSpace(workspaceID),
			)
		})
		return
	}
	fanoutCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), committedMessagePostCommitTimeout)
	defer cancel()
	for _, result := range results {
		if !result.Changed {
			continue
		}
		fanout.EmitCommittedMessageProjectionOnly(fanoutCtx, workspaceID, result.Message, result.Delivery)
	}
}

func (store *Store) createAgentInWorkspace(ctx context.Context, tx coredb.Tx, actorUserID string, workspaceID string, machineID string, params agent.CreateAgentParams) (agent.AgentView, []message.ChannelMemberEventResult, error) {
	agentName := ""
	if strings.TrimSpace(params.Name) != "" {
		var err error
		agentName, err = normalizeAgentName(params.Name)
		if err != nil {
			return agent.AgentView{}, nil, err
		}
	}

	if err := store.requireMachineExists(ctx, tx, workspaceID, machineID); err != nil {
		return agent.AgentView{}, nil, err
	}
	ownerUserID, err := machineCreatedByUser(ctx, tx, workspaceID, machineID)
	if err != nil {
		return agent.AgentView{}, nil, err
	}
	eventActorUserID := strings.TrimSpace(actorUserID)
	if eventActorUserID == "" {
		eventActorUserID = ownerUserID
	}
	ownerWorkspaceMember, err := store.loadRuntimeWorkspaceMemberForUpdate(ctx, tx, ownerUserID, workspaceID)
	if err != nil {
		return agent.AgentView{}, nil, err
	}
	if err := store.checkWorkspaceAgentOwnerQuotaInTx(ctx, tx, workspaceID, ownerUserID, 1); err != nil {
		return agent.AgentView{}, nil, err
	}

	sourceKind := strings.TrimSpace(params.SourceKind)
	sourceRef := strings.TrimSpace(params.SourceRef)
	displayName := strings.TrimSpace(params.DisplayName)
	avatarURL := strings.TrimSpace(params.AvatarURL)
	description := strings.TrimSpace(params.Description)
	runtimeValue := strings.TrimSpace(params.Runtime)
	modelValue := strings.TrimSpace(params.Model)
	reasoningEffortValue := strings.TrimSpace(params.ReasoningEffort)
	autoWakeEnabled := true
	if params.AutoWakeEnabled != nil {
		autoWakeEnabled = *params.AutoWakeEnabled
	}

	if displayName == "" {
		return agent.AgentView{}, nil, badRequest("displayName is required")
	}
	if agentName == "" {
		var err error
		agentName, err = store.availableDerivedAgentName(ctx, tx, workspaceID, deriveAgentNameFromDisplayName(displayName))
		if err != nil {
			return agent.AgentView{}, nil, err
		}
	}
	if err := validateAgentRuntime(runtimeValue); err != nil {
		return agent.AgentView{}, nil, err
	}
	permissionMode := agent.AgentPermissionModeFullAccess
	if err := validateAgentPermissionModeForRuntime(runtimeValue, permissionMode); err != nil {
		return agent.AgentView{}, nil, err
	}
	reasoningEffortValue = agent.NormalizeReasoningEffortForRuntime(
		runtimeValue,
		reasoningEffortValue,
	)
	llmColumns, err := agent.EncodeStoredAgentLLMColumns(
		store.appCredentialStorageKey,
		agent.NewAgentLLM(params.LLMMode, params.BaseURL, params.APIKey, params.Headers, params.WireAPI),
	)
	if err != nil {
		return agent.AgentView{}, nil, err
	}
	agentID, err := identifiers.NewEntityID("agt")
	if err != nil {
		return agent.AgentView{}, nil, fmt.Errorf("generate agent id: %w", err)
	}
	now := time.Now().UTC()
	memberPinyinFull, memberPinyinInitials := pinyinsearch.Variants(displayName)
	agentPinyinFull, agentPinyinInitials := pinyinsearch.Variants(agentName)
	memberID, err := insertGeneratedMember(identifiers.NewMemberID, func(memberID string) error {
		_, err := tx.Exec(ctx, `
	INSERT INTO members (id, workspace_id, type, display_name, agent_display_name_key, display_name_pinyin, display_name_pinyin_initials, created_at, updated_at)
	VALUES ($1, $2, 'agent', $3, LOWER(TRIM($3)), $4, $5, $6, $6)
`, memberID, workspaceID, displayName, memberPinyinFull, memberPinyinInitials, now)
		if uniqueViolationMatches(err, membersWorkspaceAgentDisplayNameKeyName) {
			return errAgentDisplayNameConflict
		}
		return err
	}, "generate agent member id", "insert agent member")
	if err != nil {
		if errors.Is(err, errAgentDisplayNameConflict) {
			return agent.AgentView{}, nil, agent.MarkNameConflict(conflict(agentWorkspaceNameConflictMessage))
		}
		return agent.AgentView{}, nil, err
	}
	if _, err := tx.Exec(ctx, `
INSERT INTO agents (
    id,
    workspace_id,
    machine_id,
    member_id,
    name,
    name_pinyin,
    name_pinyin_initials,
    avatar_url,
    runtime,
    permission_mode,
    model,
    reasoning_effort,
    llm_mode,
    llm_base_url,
    llm_api_key_ciphertext,
    llm_headers_json,
    llm_wire_api,
    model_profile_id,
    description,
    source_kind,
    source_ref,
    auth_token_hash,
    auto_wake_enabled,
    visible_reply_contract_enabled,
    status,
    activity,
    activity_detail,
    created_at,
    updated_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, CAST(NULLIF($16, '') AS JSON), $17, NULLIF($18, ''), $19, $20, $21, NULL, $22, TRUE, 'inactive', 'offline', '', $23, $23)
`, agentID, workspaceID, machineID, memberID, agentName, agentPinyinFull, agentPinyinInitials, avatarURL, runtimeValue, permissionMode, modelValue, reasoningEffortValue, llmColumns.Mode, llmColumns.BaseURL, llmColumns.APIKeyCiphertext, llmColumns.HeadersJSON, llmColumns.WireAPI, strings.TrimSpace(params.ModelProfileID), description, sourceKind, sourceRef, autoWakeEnabled, now); err != nil {
		if uniqueViolationMatches(err, agentsWorkspaceNameKeyName) {
			return agent.AgentView{}, nil, agent.MarkNameConflict(conflict(agentWorkspaceNameConflictMessage))
		}
		return agent.AgentView{}, nil, fmt.Errorf("insert agent: %w", err)
	}

	allChannelID, err := lookupAllChannelID(ctx, tx, workspaceID)
	if err != nil {
		if err == ErrNotFound {
			return agent.AgentView{}, nil, fmt.Errorf("workspace is missing required #all channel")
		}
		return agent.AgentView{}, nil, err
	}

	agentOwner := agentEntryOwner{
		AgentID:       agentID,
		AgentMemberID: memberID,
		OwnerMemberID: ownerWorkspaceMember.Member.ID,
	}
	spaces := store.privateAgentProjectionStore()
	if err := spaces.EnsurePrivateChannelAgentEntry(ctx, tx, workspaceID, allChannelID, privateagent.AgentEntryOwner{
		AgentID:       agentOwner.AgentID,
		AgentMemberID: agentOwner.AgentMemberID,
		OwnerMemberID: agentOwner.OwnerMemberID,
	}, now); err != nil {
		return agent.AgentView{}, nil, fmt.Errorf("add agent to #all private roster: %w", err)
	}
	memberEvents, err := store.addAgentChannelMembershipsInTx(ctx, tx, eventActorUserID, workspaceID, agentOwner, allChannelID, params.ChannelIDs, now)
	if err != nil {
		return agent.AgentView{}, nil, err
	}
	if err := store.upsertBlankAgentRoleBinding(ctx, tx, ownerUserID, workspaceID, agentID, now); err != nil {
		return agent.AgentView{}, nil, err
	}
	if err := store.RunAgentAppCatalogChangeHook(ctx, tx, workspaceID, agentID, now); err != nil {
		return agent.AgentView{}, nil, err
	}

	agentView, err := store.agentViewByID(ctx, tx, workspaceID, agentID)
	if err != nil {
		return agent.AgentView{}, nil, err
	}
	return agentView, memberEvents, nil
}

func (store *Store) upsertBlankAgentRoleBinding(ctx context.Context, tx coredb.Tx, userID string, workspaceID string, agentID string, now time.Time) error {
	bindingID, err := identifiers.NewEntityID("arb")
	if err != nil {
		return fmt.Errorf("generate blank agent role binding id: %w", err)
	}
	_, err = tx.Exec(ctx, `
INSERT INTO agent_role_bindings (
    id, workspace_id, agent_id, binding_kind, role_id, role_version_id,
    source_preset_role_id, source_preset_version_id, active_definition_path,
    realization_id, created_by, updated_by, created_at, updated_at
) VALUES ($1, $2, $3, 'blank', '', '', '', '', '', '', $4, $4, $5, $5)
ON DUPLICATE KEY UPDATE
    binding_kind = VALUES(binding_kind),
    role_id = VALUES(role_id),
    role_version_id = VALUES(role_version_id),
    source_preset_role_id = VALUES(source_preset_role_id),
    source_preset_version_id = VALUES(source_preset_version_id),
    active_definition_path = VALUES(active_definition_path),
    realization_id = VALUES(realization_id),
    updated_by = VALUES(updated_by),
    updated_at = VALUES(updated_at)
`, bindingID, workspaceID, agentID, userID, now)
	if err != nil {
		return fmt.Errorf("upsert blank agent role binding: %w", err)
	}
	return nil
}

func nullIfEmptyString(value string) any {
	trimmed := strings.TrimSpace(value)
	if trimmed == "" {
		return nil
	}
	return trimmed
}

func (store *Store) requireMachineExists(ctx context.Context, queryer interface {
	QueryRow(context.Context, string, ...any) coredb.Row
}, workspaceID string, machineID string) error {
	row := queryer.QueryRow(ctx, `SELECT 1 FROM machines WHERE workspace_id = $1 AND id = $2 AND status = 'active'`, workspaceID, machineID)
	var marker int
	if err := row.Scan(&marker); err != nil {
		if coredb.IsNotFound(err) {
			return ErrNotFound
		}
		return fmt.Errorf("load machine: %w", err)
	}
	return nil
}

func (store *Store) addAgentChannelMembershipsInTx(ctx context.Context, tx coredb.Tx, actorUserID string, workspaceID string, agent agentEntryOwner, allChannelID string, channelIDs []string, createdAt time.Time) ([]message.ChannelMemberEventResult, error) {
	initialChannelIDs := normalizedInitialAgentChannelIDs(allChannelID, channelIDs)
	if len(initialChannelIDs) == 0 {
		return nil, nil
	}
	trimmedActorUserID := strings.TrimSpace(actorUserID)
	if trimmedActorUserID == "" {
		return nil, badRequest("userId is required for initial channel membership")
	}
	results := make([]message.ChannelMemberEventResult, 0, len(initialChannelIDs))
	for _, channelID := range initialChannelIDs {
		locked, err := loadLockedChannelMemberAuthority(ctx, tx, trimmedActorUserID, workspaceID, channelID)
		if err != nil {
			return nil, err
		}
		channelAccess := locked.Access
		if !channelAccess.Joined {
			return nil, ErrNotFound
		}
		if !channelAccess.CanInvite {
			return nil, ErrForbidden
		}
		if channelAccess.Channel.Type != "channel" {
			return nil, badRequest("thread channels do not have direct members")
		}
		mutation, err := store.addPrivateChannelAgentMutationInTx(ctx, tx, workspaceID, channelAccess, agent.AgentID, createdAt)
		if err != nil {
			return nil, err
		}
		if !mutation.Changed {
			continue
		}
		result, err := store.appendChannelMemberEventMessage(ctx, tx, workspaceID, channelAccess, channelAccess.WorkspaceMember.Member.ID, message.ChannelMemberEventActionAdded, mutation.Target)
		if err != nil {
			return nil, err
		}
		results = append(results, result)
	}
	return results, nil
}

func normalizedInitialAgentChannelIDs(allChannelID string, channelIDs []string) []string {
	seen := make(map[string]struct{}, len(channelIDs)+1)
	seen[strings.TrimSpace(allChannelID)] = struct{}{}
	normalized := make([]string, 0, len(channelIDs))
	for _, rawChannelID := range channelIDs {
		channelID := strings.TrimSpace(rawChannelID)
		if channelID == "" {
			continue
		}
		if _, ok := seen[channelID]; ok {
			continue
		}
		seen[channelID] = struct{}{}
		normalized = append(normalized, channelID)
	}
	sort.Strings(normalized)
	return normalized
}

// GetAgentForUser returns an agent after validating the caller manages it.
func (store *Store) GetAgentForUser(ctx context.Context, userID string, workspaceID string, agentID string) (agent.AgentView, error) {
	if _, err := store.requireAgentOperation(ctx, userID, workspaceID, agentID); err != nil {
		return agent.AgentView{}, err
	}
	return store.agentViewByID(ctx, store.db, workspaceID, agentID)
}

// UpdateAgentForUser updates browser-editable identity/runtime fields for one
// caller-managed agent.
func (store *Store) UpdateAgentForUser(ctx context.Context, userID string, workspaceID string, agentID string, params agent.UpdateAgentParams) (agent.AgentView, error) {
	if _, err := store.requireAgentOperation(ctx, userID, workspaceID, agentID); err != nil {
		return agent.AgentView{}, err
	}

	if params.Runtime != nil {
		if err := validateAgentRuntime(strings.TrimSpace(*params.Runtime)); err != nil {
			return agent.AgentView{}, err
		}
	}
	if params.PermissionMode != nil {
		permissionMode, ok := agent.NormalizeAgentPermissionMode(*params.PermissionMode)
		if !ok {
			return agent.AgentView{}, badRequest("permissionMode must be full_access")
		}
		params.PermissionMode = &permissionMode
	}
	if params.DeriveName && params.DisplayName == nil {
		return agent.AgentView{}, badRequest("displayName is required when deriveName is true")
	}
	var newName *string
	if params.Name != nil {
		normalizedName, err := normalizeAgentName(*params.Name)
		if err != nil {
			return agent.AgentView{}, err
		}
		newName = &normalizedName
	}

	tx, err := store.db.BeginTx(ctx, coredb.TxOptions{})
	if err != nil {
		return agent.AgentView{}, fmt.Errorf("begin update agent tx: %w", err)
	}
	defer rollback(tx, ctx)

	// Agent lifecycle operations use Agent -> agent member as their canonical
	// lock order. Invocation acceptance follows machine -> Agent -> agent
	// member, so taking the Agent row before loading or mutating its member
	// prevents an UpdateAgent/Invocation lock inversion. The subsequent view
	// read is intentionally performed after this lock so all derived update
	// values come from the row generation that this transaction owns.
	var lockedAgentID string
	if err := tx.QueryRow(ctx, `
SELECT id
FROM agents
WHERE workspace_id = $1 AND id = $2 AND status <> 'archived'
FOR UPDATE
`, workspaceID, agentID).Scan(&lockedAgentID); err != nil {
		if coredb.IsNotFound(err) {
			return agent.AgentView{}, ErrNotFound
		}
		return agent.AgentView{}, fmt.Errorf("lock agent for update: %w", err)
	}

	current, err := store.agentViewByID(ctx, tx, workspaceID, agentID)
	if err != nil {
		return agent.AgentView{}, err
	}

	displayName := current.DisplayName
	if params.DisplayName != nil {
		displayName = strings.TrimSpace(*params.DisplayName)
		if displayName == "" {
			return agent.AgentView{}, badRequest("displayName is required")
		}
	}
	if newName == nil && params.DeriveName {
		derivedName := deriveAgentNameFromDisplayName(displayName)
		newName = &derivedName
	}
	description := current.Description
	if params.Description != nil {
		description = strings.TrimSpace(*params.Description)
	}
	runtimeValue := current.Runtime
	if params.Runtime != nil {
		runtimeValue = strings.TrimSpace(*params.Runtime)
	}
	permissionMode := agent.DefaultAgentPermissionMode(current.PermissionMode)
	if params.PermissionMode != nil {
		permissionMode = agent.DefaultAgentPermissionMode(*params.PermissionMode)
	}
	if err := validateAgentPermissionModeForRuntime(runtimeValue, permissionMode); err != nil {
		return agent.AgentView{}, err
	}
	permissionModeTightened := strings.TrimSpace(current.PermissionMode) != agent.AgentPermissionModeWorkspaceWrite &&
		permissionMode == agent.AgentPermissionModeWorkspaceWrite
	modelValue := current.Model
	if params.Model != nil {
		modelValue = strings.TrimSpace(*params.Model)
	}
	currentReasoningEffort := agent.NormalizeReasoningEffortForRuntime(
		current.Runtime,
		current.ReasoningEffort,
	)
	reasoningEffortValue := currentReasoningEffort
	if params.ReasoningEffort != nil {
		reasoningEffortValue = strings.TrimSpace(*params.ReasoningEffort)
	}
	reasoningEffortValue = agent.NormalizeReasoningEffortForRuntime(
		runtimeValue,
		reasoningEffortValue,
	)
	currentLLM := agent.NewAgentLLM(current.LLMMode, current.BaseURL, current.APIKey, current.Headers, current.WireAPI)
	llmMode := current.LLMMode
	baseURL := current.BaseURL
	apiKey := current.APIKey
	headers := copyAgentHeaders(current.Headers)
	wireAPI := current.WireAPI
	if params.LLMSet {
		if params.LLMMode != nil {
			llmMode = strings.TrimSpace(*params.LLMMode)
		}
		if params.BaseURL != nil {
			baseURL = strings.TrimSpace(*params.BaseURL)
		}
		if params.APIKey != nil {
			apiKey = strings.TrimSpace(*params.APIKey)
		}
		if params.HeadersSet {
			headers = copyAgentHeaders(params.Headers)
		}
		if params.WireAPI != nil {
			wireAPI = strings.TrimSpace(*params.WireAPI)
		}
	}
	modelProfileID := current.ModelProfileID
	if params.ModelProfileSet {
		modelProfileID = ""
		if params.ModelProfileID != nil {
			modelProfileID = strings.TrimSpace(*params.ModelProfileID)
		}
	} else if params.LLMSet {
		modelProfileID = ""
	}
	updateLLMColumns := params.LLMSet || params.ModelProfileSet
	llmValue := agent.NewAgentLLM(llmMode, baseURL, apiKey, headers, wireAPI)
	llmColumns, err := agent.EncodeStoredAgentLLMColumns(store.appCredentialStorageKey, llmValue)
	if err != nil {
		return agent.AgentView{}, err
	}
	runtimeChanged := params.Runtime != nil && runtimeValue != current.Runtime
	permissionModeChanged := params.PermissionMode != nil && permissionMode != current.PermissionMode
	modelChanged := params.Model != nil && modelValue != current.Model
	reasoningEffortChanged := params.ReasoningEffort != nil && reasoningEffortValue != currentReasoningEffort
	llmChanged := updateLLMColumns && !agentLLMEqual(llmValue, currentLLM)
	runtimePolicyChanged := runtimeChanged || permissionModeChanged
	sessionInvalidationRequired := runtimeChanged || modelChanged || reasoningEffortChanged || llmChanged || permissionModeTightened
	var avatarURL *string
	if params.AvatarURL != nil {
		trimmed := strings.TrimSpace(*params.AvatarURL)
		avatarURL = &trimmed
	}

	memberPinyinFull, memberPinyinInitials := pinyinsearch.Variants(displayName)
	if _, err := tx.Exec(ctx, `
UPDATE members
SET display_name = $2,
    agent_display_name_key = LOWER(TRIM($2)),
    display_name_pinyin = $3,
    display_name_pinyin_initials = $4,
    updated_at = NOW()
WHERE id = $1
`, current.MemberID, displayName, memberPinyinFull, memberPinyinInitials); err != nil {
		if uniqueViolationMatches(err, membersWorkspaceAgentDisplayNameKeyName) {
			return agent.AgentView{}, agent.MarkNameConflict(conflict(agentWorkspaceNameConflictMessage))
		}
		return agent.AgentView{}, fmt.Errorf("update agent member display name: %w", err)
	}
	if newName != nil {
		agentPinyinFull, agentPinyinInitials := pinyinsearch.Variants(*newName)
		if _, err := tx.Exec(ctx, `
UPDATE agents
SET name = $2,
    name_pinyin = $4,
    name_pinyin_initials = $5,
    updated_at = NOW()
WHERE workspace_id = $1 AND id = $3
`, workspaceID, *newName, agentID, agentPinyinFull, agentPinyinInitials); err != nil {
			if uniqueViolationMatches(err, agentsWorkspaceNameKeyName) {
				return agent.AgentView{}, agent.MarkNameConflict(conflict(agentWorkspaceNameConflictMessage))
			}
			return agent.AgentView{}, fmt.Errorf("update agent name: %w", err)
		}
	}
	if _, err := tx.Exec(ctx, `
UPDATE agents
SET description = $2,
    runtime = $3,
    permission_mode = $4,
    auth_token_hash = CASE WHEN $17 THEN NULL ELSE auth_token_hash END,
    runtime_policy_generation = CASE WHEN $18 THEN runtime_policy_generation + 1 ELSE runtime_policy_generation END,
    model = $5,
    reasoning_effort = $6,
    llm_mode = CASE WHEN $16 THEN $7 ELSE llm_mode END,
    llm_base_url = CASE WHEN $16 THEN $8 ELSE llm_base_url END,
    llm_api_key_ciphertext = CASE WHEN $16 THEN $9 ELSE llm_api_key_ciphertext END,
    llm_headers_json = CASE WHEN $16 THEN CAST(NULLIF($10, '') AS JSON) ELSE llm_headers_json END,
    llm_wire_api = CASE WHEN $16 THEN $11 ELSE llm_wire_api END,
    model_profile_id = CASE WHEN $16 THEN NULLIF($12, '') ELSE model_profile_id END,
    avatar_url = COALESCE($13, avatar_url),
    session_id = CASE WHEN $15 THEN '' ELSE session_id END,
    updated_at = NOW()
WHERE workspace_id = $1 AND id = $14
`, workspaceID, description, runtimeValue, permissionMode, modelValue, reasoningEffortValue, llmColumns.Mode, llmColumns.BaseURL, llmColumns.APIKeyCiphertext, llmColumns.HeadersJSON, llmColumns.WireAPI, modelProfileID, avatarURL, agentID, sessionInvalidationRequired, updateLLMColumns, permissionModeTightened, runtimePolicyChanged); err != nil {
		return agent.AgentView{}, fmt.Errorf("update agent: %w", err)
	}
	if err := tx.Commit(ctx); err != nil {
		return agent.AgentView{}, fmt.Errorf("commit update agent: %w", err)
	}

	current.DisplayName = displayName
	current.Description = description
	current.Runtime = runtimeValue
	current.PermissionMode = permissionMode
	current.Model = modelValue
	current.ReasoningEffort = reasoningEffortValue
	current.LLMMode, current.BaseURL, current.APIKey, current.Headers, current.WireAPI = agent.AgentLLMFieldsFromStored(llmValue)
	current.ModelProfileID = modelProfileID
	if sessionInvalidationRequired {
		current.SessionID = ""
	}
	return current, nil
}

func normalizeAgentName(raw string) (string, error) {
	name := strings.ToLower(strings.TrimSpace(raw))
	if name == "" {
		return "", badRequest("name is required")
	}

	runeCount := 0
	for index, r := range name {
		runeCount++
		if runeCount > agentWorkspaceNameMaxLength {
			return "", badRequest("name must be at most 32 characters")
		}
		if index == 0 {
			if !unicode.IsLetter(r) {
				return "", badRequest(agentWorkspaceNameInvalidFormatMessage)
			}
			continue
		}
		if unicode.IsLetter(r) || unicode.IsNumber(r) || r == '_' || r == '-' {
			continue
		}
		return "", badRequest(agentWorkspaceNameInvalidFormatMessage)
	}
	return name, nil
}

func deriveAgentNameFromDisplayName(displayName string) string {
	normalized := strings.ToLower(strings.TrimSpace(displayName))
	var builder strings.Builder
	lastSeparator := false
	for _, r := range normalized {
		switch {
		case unicode.IsLetter(r), unicode.IsNumber(r):
			builder.WriteRune(r)
			lastSeparator = false
		case r == '_' || r == '-':
			if builder.Len() > 0 && !lastSeparator {
				builder.WriteRune(r)
				lastSeparator = true
			}
		default:
			if builder.Len() > 0 && !lastSeparator {
				builder.WriteRune('-')
				lastSeparator = true
			}
		}
	}

	candidate := strings.Trim(builder.String(), "-_")
	if candidate == "" {
		candidate = "agent"
	}
	for _, r := range candidate {
		if !unicode.IsLetter(r) {
			candidate = "agent-" + candidate
		}
		break
	}
	candidate = truncateAgentName(candidate, agentWorkspaceNameMaxLength)
	candidate = strings.TrimRight(candidate, "-_")
	if candidate == "" {
		return "agent"
	}
	if _, err := normalizeAgentName(candidate); err != nil {
		return "agent"
	}
	return candidate
}

func truncateAgentName(name string, maxRunes int) string {
	if maxRunes <= 0 {
		return ""
	}
	runes := make([]rune, 0, maxRunes)
	for _, r := range name {
		if len(runes) >= maxRunes {
			break
		}
		runes = append(runes, r)
	}
	return string(runes)
}

func agentNameWithNumericSuffix(base string, suffixNumber int) string {
	suffix := fmt.Sprintf("-%d", suffixNumber)
	maxBaseRunes := agentWorkspaceNameMaxLength - len([]rune(suffix))
	if maxBaseRunes < 1 {
		maxBaseRunes = 1
	}
	prefix := strings.TrimRight(truncateAgentName(base, maxBaseRunes), "-_")
	if prefix == "" {
		prefix = "agent"
	}
	return prefix + suffix
}

func (store *Store) availableDerivedAgentName(ctx context.Context, tx coredb.Tx, workspaceID string, base string) (string, error) {
	normalizedBase, err := normalizeAgentName(base)
	if err != nil {
		return "", err
	}
	for attempt := 0; attempt < 100; attempt++ {
		candidate := normalizedBase
		if attempt > 0 {
			candidate = agentNameWithNumericSuffix(normalizedBase, attempt+1)
		}
		var existingID string
		err := tx.QueryRow(ctx, `
SELECT id
FROM agents
WHERE workspace_id = $1 AND name = $2
LIMIT 1
`, workspaceID, candidate).Scan(&existingID)
		if coredb.IsNotFound(err) {
			return candidate, nil
		}
		if err != nil {
			return "", fmt.Errorf("check derived agent name availability: %w", err)
		}
	}
	return "", agent.MarkNameConflict(conflict(agentWorkspaceNameConflictMessage))
}

// RestoreAgentPermissionModeForUser restores permission state after a failed
// runtime permission tightening. The operation is bounded to the requested
// agent and only applies while the row still has the failed permission value.
func (store *Store) RestoreAgentPermissionModeForUser(ctx context.Context, userID string, workspaceID string, agentID string, permissionMode string, expectedPermissionMode string) error {
	if _, err := store.requireAgentOperation(ctx, userID, workspaceID, agentID); err != nil {
		return err
	}

	var ok bool
	permissionMode, ok = agent.NormalizeAgentPermissionMode(permissionMode)
	if !ok {
		return badRequest("permissionMode must be full_access")
	}
	expectedPermissionMode = strings.TrimSpace(expectedPermissionMode)
	if !agent.IsAgentPermissionMode(expectedPermissionMode) {
		return badRequest("permissionMode must be full_access")
	}

	commandTag, err := store.db.Exec(ctx, `
UPDATE agents
SET permission_mode = $3,
    runtime_policy_generation = runtime_policy_generation + 1,
    updated_at = NOW()
WHERE workspace_id = $1 AND id = $2 AND permission_mode = $4 AND status <> 'archived'
`, workspaceID, agentID, permissionMode, expectedPermissionMode)
	if err != nil {
		return fmt.Errorf("restore agent permission mode: %w", err)
	}
	if commandTag.RowsAffected() == 0 {
		return nil
	}
	return nil
}

func copyAgentHeaders(headers map[string]string) map[string]string {
	if len(headers) == 0 {
		return map[string]string{}
	}
	copied := make(map[string]string, len(headers))
	for key, value := range headers {
		copied[key] = value
	}
	return copied
}

func agentLLMEqual(left *agent.AgentLLM, right *agent.AgentLLM) bool {
	leftNormalized := left.Normalize()
	rightNormalized := right.Normalize()
	if leftNormalized == nil || rightNormalized == nil {
		return leftNormalized == nil && rightNormalized == nil
	}
	leftHeaders, leftErr := agent.EncodeAgentHeaders(leftNormalized.Headers)
	rightHeaders, rightErr := agent.EncodeAgentHeaders(rightNormalized.Headers)
	return leftErr == nil &&
		rightErr == nil &&
		leftNormalized.Mode == rightNormalized.Mode &&
		leftNormalized.BaseURL == rightNormalized.BaseURL &&
		leftNormalized.APIKey == rightNormalized.APIKey &&
		leftNormalized.WireAPI == rightNormalized.WireAPI &&
		leftHeaders == rightHeaders
}

// ValidateAgentRuntime validates the supported runtime enum for agent definitions.
func ValidateAgentRuntime(runtimeValue string) error {
	return validateAgentRuntime(runtimeValue)
}

func validateAgentRuntime(runtimeValue string) error {
	if catalog.IsSupportedAgentRuntime(runtimeValue) {
		return nil
	}
	return badRequest(catalog.UnsupportedAgentRuntimeMessage())
}

func validateAgentPermissionModeForRuntime(runtimeValue string, permissionMode string) error {
	_ = runtimeValue
	permissionMode, ok := agent.NormalizeAgentPermissionMode(permissionMode)
	if !ok || permissionMode != agent.AgentPermissionModeFullAccess {
		return badRequest("permissionMode must be full_access")
	}
	return nil
}
