package webagent

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"reflect"
	"strconv"
	"strings"

	"github.com/tidbcloud/AgentTeam/internal/api/dto"
	"github.com/tidbcloud/AgentTeam/internal/service/agentapproval"
	"github.com/tidbcloud/AgentTeam/internal/store/agent"
	"github.com/tidbcloud/AgentTeam/internal/store/agentops"
	"github.com/tidbcloud/AgentTeam/internal/store/identity"
)

type operationAgentService interface {
	CreateAgent(ctx context.Context, userID, workspaceID string, input CreateAgentInput) (agent.AppAgent, error)
	UpdateAgent(ctx context.Context, userID, workspaceID string, input UpdateAgentInput) (agent.AppAgent, error)
	DeleteAgent(ctx context.Context, userID, workspaceID, agentID string) error
}

type operationDuplicateAgentService interface {
	DuplicateAgent(ctx context.Context, userID, workspaceID, sourceAgentID string, input DuplicateAgentInput) (agent.AppAgent, *dto.WebAgentResourceCopyResponse, error)
}

type operationAgentInventory interface {
	ListOwnedAgentsForWorkspace(ctx context.Context, userID, workspaceID string) ([]agent.AppAgent, error)
}

type operationOwnerIdentityFinder interface {
	FindUserByID(ctx context.Context, userID string) (identity.User, error)
}

// OperationExecutor applies approved structural Agent operation items through
// the shared AgentService boundary.
type OperationExecutor struct {
	agents        operationAgentService
	inventory     operationAgentInventory
	ownerIdentity operationOwnerIdentityFinder
}

type OperationExecutorOption func(*OperationExecutor)

func WithOperationAgentInventory(inventory operationAgentInventory) OperationExecutorOption {
	return func(executor *OperationExecutor) {
		executor.inventory = inventory
	}
}

func WithOperationOwnerIdentityFinder(finder operationOwnerIdentityFinder) OperationExecutorOption {
	return func(executor *OperationExecutor) {
		executor.ownerIdentity = finder
	}
}

func NewOperationExecutor(agents operationAgentService, options ...OperationExecutorOption) *OperationExecutor {
	executor := &OperationExecutor{agents: agents}
	for _, option := range options {
		if option != nil {
			option(executor)
		}
	}
	return executor
}

// ExecuteItem verifies the frozen operation identity before decoding or
// applying its credential-free item. Successful results contain only agentId.
func (executor *OperationExecutor) ExecuteItem(ctx context.Context, op *agentops.Operation, item agentops.OperationItem) (json.RawMessage, error) {
	if executor == nil || executor.agents == nil {
		return nil, errors.New("agent operation executor is not configured")
	}
	if op == nil {
		return nil, errors.New("agent operation is missing")
	}

	var payload agentapproval.Payload
	if err := decodeCredentialFreeJSON(op.Payload, &payload); err != nil {
		return nil, errors.New("agent operation frozen payload is invalid")
	}
	action := strings.TrimSpace(op.Action)
	if strings.TrimSpace(payload.Action) != action {
		return nil, errors.New("agent operation action does not match frozen payload")
	}
	if strings.TrimSpace(payload.Owner.UserID) == "" ||
		strings.TrimSpace(payload.Owner.UserID) != strings.TrimSpace(op.OwnerUserID) {
		return nil, errors.New("agent operation owner does not match frozen payload")
	}
	if err := agentapproval.ValidatePayload(payload); err != nil {
		return nil, errors.New("agent operation frozen payload is invalid")
	}

	switch action {
	case agentapproval.ActionCreate:
		return executor.executeCreate(ctx, op, payload, item)
	case agentapproval.ActionUpdate:
		return executor.executeUpdate(ctx, op, payload, item)
	case agentapproval.ActionDelete:
		return executor.executeDelete(ctx, op, payload, item)
	case agentapproval.ActionDuplicate:
		return executor.executeDuplicate(ctx, op, payload, item)
	default:
		return nil, fmt.Errorf("agent operation action %q is unsupported", action)
	}
}

func (executor *OperationExecutor) executeDuplicate(ctx context.Context, op *agentops.Operation, payload agentapproval.Payload, item agentops.OperationItem) (json.RawMessage, error) {
	var proposal agentapproval.DuplicateProposal
	if payload.Duplicate == nil ||
		item.ItemIndex < 0 ||
		item.ItemIndex >= payload.Duplicate.Count ||
		decodeCredentialFreeJSON(item.Input, &proposal) != nil ||
		!reflect.DeepEqual(proposal, *payload.Duplicate) {
		return nil, errors.New("agent operation duplicate item is invalid")
	}

	duplicateService, ok := executor.agents.(operationDuplicateAgentService)
	if !ok || executor.inventory == nil {
		return nil, errors.New("agent operation duplicate executor is not configured")
	}
	ownedAgents, err := executor.inventory.ListOwnedAgentsForWorkspace(ctx, op.OwnerUserID, op.WorkspaceID)
	if err != nil {
		return nil, safeOperationError{action: agentapproval.ActionDuplicate}
	}
	source, found := operationDuplicateSource(ownedAgents, proposal.SourceAgentID)
	if !found {
		return nil, safeOperationError{action: agentapproval.ActionDuplicate}
	}
	name, displayName := operationDuplicateNames(source, ownedAgents, item.ItemIndex)

	input := DuplicateAgentInput{
		Mode:        proposal.Mode,
		Name:        name,
		DisplayName: displayName,
		MachineID:   proposal.MachineID,
		Runtime:     proposal.Runtime,
		ChannelIDs:  proposal.ChannelIDs,
	}
	if strings.TrimSpace(proposal.Mode) == agentDuplicateModeWithResource {
		if executor.ownerIdentity == nil {
			return nil, errors.New("agent operation duplicate owner identity is not configured")
		}
		owner, err := executor.ownerIdentity.FindUserByID(ctx, op.OwnerUserID)
		if err != nil || strings.TrimSpace(owner.ID) != strings.TrimSpace(op.OwnerUserID) {
			return nil, safeOperationError{action: agentapproval.ActionDuplicate}
		}
		input.Identity = NewApprovedAgentManagementResourceCopyIdentity(owner)
	}

	created, _, err := duplicateService.DuplicateAgent(ctx, op.OwnerUserID, op.WorkspaceID, proposal.SourceAgentID, input)
	if err != nil {
		return nil, safeOperationError{action: agentapproval.ActionDuplicate}
	}
	return operationAgentIDResult(created.ID)
}

func operationDuplicateSource(agents []agent.AppAgent, sourceAgentID string) (agent.AppAgent, bool) {
	for _, candidate := range agents {
		if candidate.ID == sourceAgentID {
			return candidate, true
		}
	}
	return agent.AppAgent{}, false
}

func operationDuplicateNames(source agent.AppAgent, existing []agent.AppAgent, itemIndex int) (string, string) {
	nameBase := strings.TrimSpace(source.Name)
	if nameBase == "" {
		nameBase = "agent"
	}
	displayBase := strings.TrimSpace(source.DisplayName)
	if displayBase == "" {
		displayBase = nameBase
	}
	usedNames := make(map[string]struct{}, len(existing))
	usedDisplayNames := make(map[string]struct{}, len(existing))
	for _, candidate := range existing {
		usedNames[strings.ToLower(strings.TrimSpace(candidate.Name))] = struct{}{}
		usedDisplayNames[strings.ToLower(strings.TrimSpace(candidate.DisplayName))] = struct{}{}
	}

	baseOrdinal := itemIndex + 1
	for collisionAttempt := 0; ; collisionAttempt++ {
		suffix := "-copy-" + strconv.Itoa(baseOrdinal)
		displaySuffix := " copy " + strconv.Itoa(baseOrdinal)
		if collisionAttempt > 0 {
			suffix += "-" + strconv.Itoa(collisionAttempt+1)
			displaySuffix += " " + strconv.Itoa(collisionAttempt+1)
		}
		candidateName := truncateOperationDuplicateName(nameBase, suffix)
		candidateDisplayName := truncateOperationDuplicateName(displayBase, displaySuffix)
		if _, exists := usedNames[strings.ToLower(candidateName)]; exists {
			continue
		}
		if _, exists := usedDisplayNames[strings.ToLower(candidateDisplayName)]; exists {
			continue
		}
		return candidateName, candidateDisplayName
	}
}

func truncateOperationDuplicateName(base, suffix string) string {
	const maxRunes = 32
	suffixRunes := []rune(suffix)
	maxBaseRunes := maxRunes - len(suffixRunes)
	if maxBaseRunes < 0 {
		maxBaseRunes = 0
	}
	baseRunes := []rune(strings.TrimSpace(base))
	if len(baseRunes) > maxBaseRunes {
		baseRunes = baseRunes[:maxBaseRunes]
	}
	return string(baseRunes) + suffix
}

func (executor *OperationExecutor) executeCreate(ctx context.Context, op *agentops.Operation, payload agentapproval.Payload, item agentops.OperationItem) (json.RawMessage, error) {
	if item.ItemIndex != 0 {
		return nil, errors.New("agent operation create item is invalid")
	}
	var proposal agentapproval.CreateProposal
	if err := decodeCredentialFreeJSON(item.Input, &proposal); err != nil || payload.Create == nil || !reflect.DeepEqual(proposal, *payload.Create) {
		return nil, errors.New("agent operation create item is invalid")
	}

	llmMode := trustedLLMMode(proposal.Model, proposal.ModelProfileID)
	created, err := executor.agents.CreateAgent(ctx, op.OwnerUserID, op.WorkspaceID, CreateAgentInput{
		Name:            proposal.Name,
		Description:     proposal.Description,
		MachineID:       proposal.MachineID,
		Runtime:         proposal.Runtime,
		Model:           proposal.Model,
		ReasoningEffort: proposal.ReasoningEffort,
		LLMMode:         llmMode,
		ModelProfileID:  proposal.ModelProfileID,
		ChannelIDs:      proposal.ChannelIDs,
	})
	if err != nil {
		return nil, safeOperationError{action: agentapproval.ActionCreate}
	}
	return operationAgentIDResult(created.ID)
}

func (executor *OperationExecutor) executeUpdate(ctx context.Context, op *agentops.Operation, payload agentapproval.Payload, item agentops.OperationItem) (json.RawMessage, error) {
	if item.ItemIndex != 0 {
		return nil, errors.New("agent operation update item is invalid")
	}
	var proposal agentapproval.UpdateProposal
	if err := decodeCredentialFreeJSON(item.Input, &proposal); err != nil || payload.Update == nil || !reflect.DeepEqual(proposal, *payload.Update) {
		return nil, errors.New("agent operation update item is invalid")
	}

	updated, err := executor.agents.UpdateAgent(ctx, op.OwnerUserID, op.WorkspaceID, UpdateAgentInput{
		AgentID:           proposal.AgentID,
		Description:       frozenOptionalString(proposal.Description),
		Model:             frozenOptionalString(proposal.Model),
		ReasoningEffort:   frozenOptionalString(proposal.ReasoningEffort),
		ModelProfileID:    frozenOptionalString(proposal.ModelProfileID),
		ResolveTrustedLLM: proposal.Model != nil || proposal.ModelProfileID != nil,
	})
	if err != nil {
		return nil, safeOperationError{action: agentapproval.ActionUpdate}
	}
	return operationAgentIDResult(updated.ID)
}

func (executor *OperationExecutor) executeDelete(ctx context.Context, op *agentops.Operation, payload agentapproval.Payload, item agentops.OperationItem) (json.RawMessage, error) {
	var input struct {
		AgentID string `json:"agentId"`
	}
	if err := decodeCredentialFreeJSON(item.Input, &input); err != nil ||
		payload.Delete == nil ||
		item.ItemIndex < 0 ||
		item.ItemIndex >= len(payload.Delete.AgentIDs) ||
		payload.Delete.AgentIDs[item.ItemIndex] != input.AgentID {
		return nil, errors.New("agent operation delete item is invalid")
	}
	if err := executor.agents.DeleteAgent(ctx, op.OwnerUserID, op.WorkspaceID, input.AgentID); err != nil {
		return nil, safeOperationError{action: agentapproval.ActionDelete}
	}
	return operationAgentIDResult(input.AgentID)
}

func trustedLLMMode(model, profileID string) string {
	if strings.TrimSpace(profileID) != "" {
		return agent.AgentLLMModeLocal
	}
	if strings.HasPrefix(strings.TrimSpace(model), "agentteam/") {
		return agent.AgentLLMModeAgentTeamHosted
	}
	return agent.AgentLLMModeAgentTeamProxy
}

func frozenOptionalString(value *string) dto.OptionalString {
	if value == nil {
		return dto.OptionalString{}
	}
	copied := *value
	return dto.OptionalString{Set: true, Value: &copied}
}

func decodeCredentialFreeJSON(raw json.RawMessage, target any) error {
	decoder := json.NewDecoder(bytes.NewReader(raw))
	decoder.DisallowUnknownFields()
	if err := decoder.Decode(target); err != nil {
		return err
	}
	var extra any
	if err := decoder.Decode(&extra); err == nil {
		return errors.New("multiple JSON values")
	} else if !errors.Is(err, io.EOF) {
		return err
	}
	return nil
}

func operationAgentIDResult(agentID string) (json.RawMessage, error) {
	result, err := json.Marshal(struct {
		AgentID string `json:"agentId"`
	}{AgentID: agentID})
	if err != nil {
		return nil, errors.New("encode agent operation result")
	}
	return result, nil
}

type safeOperationError struct {
	action string
}

func (err safeOperationError) Error() string {
	return "agent operation " + err.action + " failed"
}

var _ agentops.OperationExecutor = (*OperationExecutor)(nil)
