package agentapproval

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

	"github.com/tidbcloud/AgentTeam/internal/core/identifiers"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/access"
	"github.com/tidbcloud/AgentTeam/internal/store/agent"
	"github.com/tidbcloud/AgentTeam/internal/store/agentops"
	"github.com/tidbcloud/AgentTeam/internal/store/approval"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
)

// Service implements the agent_management Approval subject. Submit freezes a
// credential-free proposal and opens an approval whose sole approver is the
// Human that owns the applicant Agent's machine. Decide enqueues a queued
// operation (+ the action's ordered items) on approve and enqueues nothing on
// reject. The worker executor (Phase 3) consumes the operation; this service
// only submits and enqueues.
type Service struct {
	db         coredb.DB
	approvals  *approval.Store
	operations *agentops.Store
}

// New creates an agent_management service over the given stores.
func New(db coredb.DB, approvals *approval.Store, operations *agentops.Store) *Service {
	return &Service{db: db, approvals: approvals, operations: operations}
}

func (service *Service) validate() error {
	if service == nil || service.db == nil || service.approvals == nil || service.operations == nil {
		return fmt.Errorf("agent management service is not configured")
	}
	return nil
}

// requireAuthenticatedAgent authoritatively binds the submitted principal to one
// active Agent row: the agent must exist, be non-archived, and its member_id
// must equal the authenticated MemberID (which must itself be an active agent
// member). This closes the gap left by the generic Approval AllowAgentApplicant
// check (which verifies the member is an active agent but does not bind it to
// the supplied agentID). A mismatched principal is rejected before any owner
// resolution or proposal freezing.
func requireAuthenticatedAgent(ctx context.Context, q coredb.QueryRower, workspaceID, agentID, memberID string) error {
	var count int
	if err := q.QueryRow(ctx, `SELECT COUNT(*) FROM agents a JOIN members m ON m.workspace_id=a.workspace_id AND m.id=a.member_id WHERE a.workspace_id=$1 AND a.id=$2 AND a.member_id=$3 AND a.status<>'archived' AND m.type='agent' AND m.status='active'`, workspaceID, agentID, memberID).Scan(&count); err != nil {
		return fmt.Errorf("validate authenticated agent management applicant: %w", err)
	}
	if count != 1 {
		return storeerr.Forbidden("authenticated agent does not match an active agent member")
	}
	return nil
}

// SubmitInput is the transport-free proposal. Principal identity comes from the
// authenticated Agent; the payload never carries credentials.
type SubmitInput struct {
	ClientRequestID string
	CorrelationID   string
	Now             time.Time
	Payload         Payload
}

// Result is one Submit outcome (newly created or replayed).
type Result struct {
	Approval    approval.Instance
	Assignments []approval.Assignment
	Created     bool
}

// LookupReplay returns an existing approval for the same authenticated
// applicant, action, request id, and canonical payload without consulting any
// mutable target Agent or machine. The frozen owner is loaded from the existing
// payload and used only to reconstruct the original digest. A miss is reported
// as found=false; a reused request id with different payload returns conflict.
func (service *Service) LookupReplay(ctx context.Context, authenticated agent.AuthenticatedAgent, input SubmitInput) (result Result, found bool, err error) {
	if err := service.validate(); err != nil {
		return Result{}, false, err
	}
	prepared, err := prepareSubmit(authenticated, input)
	if err != nil {
		return Result{}, false, err
	}
	subjectRef := agentManagementSubjectRef(prepared.workspaceID, prepared.agentID, prepared.clientRequestID)
	idempotencyKey := agentManagementIdempotencyKey(prepared.workspaceID, subjectRef)
	existing, err := service.approvals.LoadByIdempotencyKey(ctx, prepared.workspaceID, prepared.memberID, idempotencyKey)
	if errors.Is(err, storeerr.ErrNotFound) {
		legacyRef := legacyAgentManagementSubjectRef(prepared.workspaceID, prepared.agentID, prepared.payload.Action, prepared.clientRequestID)
		existing, err = service.approvals.LoadByIdempotencyKey(ctx, prepared.workspaceID, prepared.memberID, agentManagementIdempotencyKey(prepared.workspaceID, legacyRef))
		if errors.Is(err, storeerr.ErrNotFound) {
			return Result{}, false, nil
		}
	}
	if err != nil {
		return Result{}, false, err
	}
	frozen, err := decodePayload(existing.Payload, existing.PayloadDigest)
	if err != nil {
		return Result{}, false, err
	}
	prepared.payload.Owner = frozen.Owner
	prepared.payload.Presentation = frozen.Presentation
	_, _, payloadDigest, err := canonicalPayload(prepared.payload)
	if err != nil {
		return Result{}, false, err
	}
	if err := requireReplayMatch(existing, []string{
		subjectRef,
		legacyAgentManagementSubjectRef(prepared.workspaceID, prepared.agentID, prepared.payload.Action, prepared.clientRequestID),
	}, payloadDigest); err != nil {
		return Result{}, false, err
	}
	assignments, err := service.approvals.ListAssignments(ctx, existing.WorkspaceID, existing.ID)
	if err != nil {
		return Result{}, false, err
	}
	return Result{Approval: existing, Assignments: assignments, Created: false}, true, nil
}

// Submit creates or replays one agent_management approval. The same
// applicant+requestId+payload replays; a different payload conflicts. The owner
// Human (machine creator) is the sole approver.
func (service *Service) Submit(ctx context.Context, authenticated agent.AuthenticatedAgent, input SubmitInput) (Result, error) {
	if err := service.validate(); err != nil {
		return Result{}, err
	}
	prepared, err := prepareSubmit(authenticated, input)
	if err != nil {
		return Result{}, err
	}
	var result Result
	err = coredb.WithTx(ctx, service.db, func(tx coredb.Tx) error {
		if err := requireAuthenticatedAgent(ctx, tx, prepared.workspaceID, prepared.agentID, prepared.memberID); err != nil {
			return err
		}
		ownerUserID, err := access.AgentManagedByUser(ctx, tx, prepared.workspaceID, prepared.agentID)
		if err != nil {
			return err
		}
		owner, err := access.LoadHumanWorkspaceMember(ctx, tx, ownerUserID, prepared.workspaceID)
		if err != nil {
			return err
		}
		ownerMemberID := owner.Member.ID
		prepared.payload.Owner = OwnerRef{UserID: ownerUserID, MemberID: ownerMemberID}
		presentation, err := loadAgentPresentationSnapshot(ctx, tx, prepared.workspaceID, prepared.payload)
		if err != nil {
			return err
		}
		prepared.payload.Presentation = presentation
		normalized, snapshot, payloadDigest, err := canonicalPayload(prepared.payload)
		if err != nil {
			return err
		}
		prepared.payload = normalized
		subjectRef := agentManagementSubjectRef(prepared.workspaceID, prepared.payload.Applicant.AgentID, prepared.clientRequestID)
		idempotencyKey := agentManagementIdempotencyKey(prepared.workspaceID, subjectRef)
		existing, loadErr := service.approvals.InTx(tx).LoadByIdempotencyKey(ctx, prepared.workspaceID, prepared.memberID, idempotencyKey)
		if errors.Is(loadErr, storeerr.ErrNotFound) {
			legacyRef := legacyAgentManagementSubjectRef(prepared.workspaceID, prepared.payload.Applicant.AgentID, prepared.payload.Action, prepared.clientRequestID)
			existing, loadErr = service.approvals.InTx(tx).LoadByIdempotencyKey(ctx, prepared.workspaceID, prepared.memberID, agentManagementIdempotencyKey(prepared.workspaceID, legacyRef))
		}
		if loadErr == nil {
			frozen, err := decodePayload(existing.Payload, existing.PayloadDigest)
			if err != nil {
				return err
			}
			// Display identities are immutable presentation facts of the original
			// request. A later Agent rename must not turn an idempotent replay into
			// a conflict.
			prepared.payload.Presentation = frozen.Presentation
			_, _, payloadDigest, err = canonicalPayload(prepared.payload)
			if err != nil {
				return err
			}
			if err := requireReplayMatch(existing, []string{
				subjectRef,
				legacyAgentManagementSubjectRef(prepared.workspaceID, prepared.payload.Applicant.AgentID, prepared.payload.Action, prepared.clientRequestID),
			}, payloadDigest); err != nil {
				return err
			}
			result = Result{Approval: existing, Created: false}
			return service.loadReplayAssignments(ctx, tx, existing, &result)
		} else if !errors.Is(loadErr, storeerr.ErrNotFound) {
			return loadErr
		}

		approvalID, err := identifiers.NewEntityID("appr")
		if err != nil {
			return fmt.Errorf("generate agent management approval id: %w", err)
		}
		instance, assignments, created, err := service.approvals.InTx(tx).Create(ctx, approval.CreateParams{
			ID: approvalID, WorkspaceID: prepared.workspaceID, SubjectType: SubjectType,
			SubjectRef: subjectRef, SubjectVersion: 1,
			Payload: snapshot, PayloadDigest: payloadDigest,
			IdempotencyKey: idempotencyKey, PendingKey: "",
			ApplicantMemberID: prepared.memberID, AllowAgentApplicant: true,
			ApproverMemberIDs: []string{ownerMemberID},
			CorrelationID:     prepared.correlationID, Now: prepared.now,
		})
		if err != nil {
			return err
		}
		result = Result{Approval: instance, Assignments: assignments, Created: created}
		return nil
	})
	return result, err
}

func loadAgentPresentationSnapshot(ctx context.Context, queryer coredb.Queryer, workspaceID string, payload Payload) (PresentationSnapshot, error) {
	ids := make([]string, 0, 10)
	switch payload.Action {
	case ActionUpdate:
		ids = append(ids, payload.Update.AgentID)
	case ActionDelete:
		ids = append(ids, payload.Delete.AgentIDs...)
	case ActionDuplicate:
		ids = append(ids, payload.Duplicate.SourceAgentID)
	case ActionCreate:
		return PresentationSnapshot{Agents: []AgentIdentitySnapshot{{DisplayName: payload.Create.Name}}}, nil
	}
	if len(ids) == 0 {
		return PresentationSnapshot{}, nil
	}
	args := []any{strings.TrimSpace(workspaceID)}
	placeholders := make([]string, 0, len(ids))
	for _, id := range ids {
		args = append(args, strings.TrimSpace(id))
		placeholders = append(placeholders, fmt.Sprintf("$%d", len(args)))
	}
	rows, err := queryer.Query(ctx, `
SELECT a.id, COALESCE(NULLIF(a.name, ''), NULLIF(m.display_name, ''), a.id)
FROM agents a
JOIN members m ON m.id = a.member_id AND m.workspace_id = a.workspace_id
WHERE a.workspace_id = $1
  AND a.id IN (`+strings.Join(placeholders, ", ")+`)
  AND a.status <> 'archived'
`, args...)
	if err != nil {
		return PresentationSnapshot{}, fmt.Errorf("load agent management presentation: %w", err)
	}
	defer rows.Close()
	byID := make(map[string]string, len(ids))
	for rows.Next() {
		var id, displayName string
		if err := rows.Scan(&id, &displayName); err != nil {
			return PresentationSnapshot{}, fmt.Errorf("scan agent management presentation: %w", err)
		}
		byID[strings.TrimSpace(id)] = strings.TrimSpace(displayName)
	}
	if err := rows.Err(); err != nil {
		return PresentationSnapshot{}, fmt.Errorf("iterate agent management presentation: %w", err)
	}
	seen := make(map[string]struct{}, len(ids))
	agents := make([]AgentIdentitySnapshot, 0, len(ids))
	for _, rawID := range ids {
		id := strings.TrimSpace(rawID)
		if _, ok := seen[id]; ok {
			continue
		}
		seen[id] = struct{}{}
		agents = append(agents, AgentIdentitySnapshot{ID: id, DisplayName: byID[id]})
	}
	return PresentationSnapshot{Agents: agents}, nil
}

// DecisionResult is one Decide outcome.
type DecisionResult struct {
	Instance    approval.Instance
	OperationID int64
	Replay      bool
}

// Decide settles the approval. On approve it enqueues a queued operation (with
// the action's ordered items) inside the decision tx; on reject it enqueues
// nothing. The worker (Phase 3) later consumes the operation.
func (service *Service) Decide(ctx context.Context, params approval.DecideParams) (DecisionResult, error) {
	if err := service.validate(); err != nil {
		return DecisionResult{}, err
	}
	var operationID int64
	decision, err := service.approvals.DecideWithEffect(ctx, params, func(ctx context.Context, tx coredb.Tx, instance approval.Instance, requested string) (approval.DecisionEffectResult, error) {
		if instance.SubjectType != SubjectType {
			return approval.DecisionEffectResult{}, storeerr.Conflict("Approval is not an agent management request")
		}
		payload, err := decodePayload(instance.Payload, instance.PayloadDigest)
		if err != nil {
			return approval.DecisionEffectResult{}, err
		}
		if err := verifyDecide(instance, payload, params.ActorMemberID); err != nil {
			return approval.DecisionEffectResult{}, err
		}
		if requested == approval.DecisionRejected {
			return approval.DecisionEffectResult{}, nil
		}
		items, err := payload.itemInputs()
		if err != nil {
			return approval.DecisionEffectResult{}, err
		}
		operationID, err = service.operations.InTx(tx).EnqueueOperationInTx(ctx, agentops.EnqueueInput{
			WorkspaceID: instance.WorkspaceID, ApplicantAgentID: payload.Applicant.AgentID,
			OwnerUserID: payload.Owner.UserID, Action: payload.Action, Payload: instance.Payload,
			ApprovalID: instance.ID, RequestID: payload.ClientRequestID, Items: items,
		})
		if err != nil {
			return approval.DecisionEffectResult{}, err
		}
		return approval.DecisionEffectResult{}, nil
	})
	if err != nil {
		return DecisionResult{}, err
	}
	if decision.Instance.State == approval.StateApproved && operationID == 0 {
		operation, err := service.operations.LookupOperationByApproval(
			ctx, decision.Instance.WorkspaceID, decision.Instance.ID,
		)
		if err != nil {
			return DecisionResult{}, err
		}
		operationID = operation.ID
	}
	return DecisionResult{Instance: decision.Instance, OperationID: operationID, Replay: decision.AlreadyApplied}, nil
}

type preparedSubmit struct {
	workspaceID, agentID, memberID, clientRequestID, correlationID string
	payload                                                        Payload
	now                                                            time.Time
}

func prepareSubmit(authenticated agent.AuthenticatedAgent, input SubmitInput) (preparedSubmit, error) {
	p := preparedSubmit{
		workspaceID:     strings.TrimSpace(authenticated.WorkspaceID),
		agentID:         strings.TrimSpace(authenticated.AgentID),
		memberID:        strings.TrimSpace(authenticated.MemberID),
		clientRequestID: strings.TrimSpace(input.ClientRequestID),
		correlationID:   strings.TrimSpace(input.CorrelationID),
		payload:         input.Payload,
		now:             normalizedNow(input.Now),
	}
	if p.workspaceID == "" || p.agentID == "" || p.memberID == "" {
		return preparedSubmit{}, storeerr.BadRequest("authenticated agent workspace, agent, and member are required")
	}
	if p.clientRequestID == "" || len(p.clientRequestID) > 128 {
		return preparedSubmit{}, storeerr.BadRequest("agent management clientRequestId is required (1-128 chars)")
	}
	p.payload.ClientRequestID = p.clientRequestID
	p.payload.Applicant = Applicant{AgentID: p.agentID, MemberID: p.memberID}
	if err := ValidatePayload(p.payload); err != nil {
		return preparedSubmit{}, err
	}
	return p, nil
}

// validatePayload enforces the service-level invariants that hold regardless of
// entry point (Web route or approval subject): the action matches exactly one
// proposal, and delete/duplicate carry 1-10 items. Routes may add transport
// specifics, but the SubjectHandler is a second entry point, so these must hold
// in the service itself.
func ValidatePayload(payload Payload) error {
	proposals := 0
	for _, present := range []bool{payload.Create != nil, payload.Update != nil, payload.Delete != nil, payload.Duplicate != nil} {
		if present {
			proposals++
		}
	}
	if proposals != 1 {
		return storeerr.BadRequest("agent management proposal must have exactly one action proposal")
	}
	switch payload.Action {
	case ActionCreate:
		if payload.Create == nil {
			return storeerr.BadRequest("agent management create action requires a create proposal")
		}
		if strings.TrimSpace(payload.Create.Name) == "" || strings.TrimSpace(payload.Create.MachineID) == "" || strings.TrimSpace(payload.Create.Runtime) == "" {
			return storeerr.BadRequest("agent management create requires name, machineId, and runtime")
		}
	case ActionUpdate:
		if payload.Update == nil {
			return storeerr.BadRequest("agent management update action requires an update proposal")
		}
		if strings.TrimSpace(payload.Update.AgentID) == "" {
			return storeerr.BadRequest("agent management update requires agentId")
		}
		if payload.Update.Description == nil && payload.Update.Model == nil &&
			payload.Update.ReasoningEffort == nil && payload.Update.ModelProfileID == nil {
			return storeerr.BadRequest("agent management update requires at least one change field")
		}
	case ActionDelete:
		if payload.Delete == nil {
			return storeerr.BadRequest("agent management delete action requires a delete proposal")
		}
		if len(payload.Delete.AgentIDs) < 1 || len(payload.Delete.AgentIDs) > 10 {
			return storeerr.BadRequest("agent management delete requires 1-10 agent ids")
		}
		for _, id := range payload.Delete.AgentIDs {
			if strings.TrimSpace(id) == "" {
				return storeerr.BadRequest("agent management delete agent ids must be non-empty")
			}
		}
	case ActionDuplicate:
		if payload.Duplicate == nil {
			return storeerr.BadRequest("agent management duplicate action requires a duplicate proposal")
		}
		if strings.TrimSpace(payload.Duplicate.SourceAgentID) == "" {
			return storeerr.BadRequest("agent management duplicate requires sourceAgentId")
		}
		if payload.Duplicate.Count < 1 || payload.Duplicate.Count > 10 {
			return storeerr.BadRequest("agent management duplicate count must be 1-10")
		}
		switch strings.TrimSpace(payload.Duplicate.Mode) {
		case "", "config_only", "with_resources":
		default:
			return storeerr.BadRequest("agent management duplicate mode must be empty, config_only, or with_resources")
		}
	default:
		return storeerr.BadRequest("agent management action is invalid")
	}
	return nil
}

func agentManagementSubjectRef(workspaceID, agentID, requestID string) string {
	// v2 deliberately excludes action: requestId is the selector for the
	// applicant's management receipt, so reusing it for another action must
	// conflict rather than create a second approval.
	return digestStrings("agent-management-subject-v2", workspaceID, agentID, requestID)
}

func legacyAgentManagementSubjectRef(workspaceID, agentID, action, requestID string) string {
	return digestStrings("agent-management-subject-v1", workspaceID, agentID, action, requestID)
}

func agentManagementIdempotencyKey(workspaceID, subjectRef string) string {
	return digestStrings("approval-submission-v1", workspaceID, SubjectType, subjectRef)
}

func matchesAnySubjectRef(actual string, expected []string) bool {
	for _, candidate := range expected {
		if actual == candidate {
			return true
		}
	}
	return false
}

func requireReplayMatch(existing approval.Instance, subjectRefs []string, payloadDigest string) error {
	if existing.SubjectType != SubjectType || !matchesAnySubjectRef(existing.SubjectRef, subjectRefs) || existing.PayloadDigest != payloadDigest {
		return storeerr.Conflict("agent management client request id was reused with another payload")
	}
	return nil
}

func (service *Service) loadReplayAssignments(ctx context.Context, tx coredb.Tx, instance approval.Instance, result *Result) error {
	assignments, err := service.approvals.InTx(tx).ListAssignments(ctx, instance.WorkspaceID, instance.ID)
	if err != nil {
		return err
	}
	result.Assignments = assignments
	return nil
}

// verifyDecide ensures the decision applies to the exact frozen request: the
// applicant + subject reference must match the instance, and the deciding actor
// must be the frozen owner (the sole approver). Identity is taken from the
// frozen payload, not re-resolved, so an owner change after submit cannot turn
// into an unapproved new identity at approve time.
func verifyDecide(instance approval.Instance, payload Payload, actorMemberID string) error {
	if payload.Applicant.MemberID != instance.ApplicantMemberID {
		return storeerr.Conflict("agent management applicant does not match the approval")
	}
	if !matchesAnySubjectRef(instance.SubjectRef, []string{
		agentManagementSubjectRef(instance.WorkspaceID, payload.Applicant.AgentID, payload.ClientRequestID),
		legacyAgentManagementSubjectRef(instance.WorkspaceID, payload.Applicant.AgentID, payload.Action, payload.ClientRequestID),
	}) {
		return storeerr.Conflict("agent management subject reference does not match the approval")
	}
	if payload.Owner.MemberID == "" || payload.Owner.MemberID != actorMemberID {
		return storeerr.Forbidden("only the frozen owner Human may approve an agent management request")
	}
	return nil
}

func normalizedNow(value time.Time) time.Time {
	if value.IsZero() {
		return time.Now().UTC()
	}
	return value.UTC()
}
