package orchestrator

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"strings"

	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
	runstore "github.com/tidbcloud/AgentTeam/internal/store/workflow/run"
	runkernel "github.com/tidbcloud/AgentTeam/internal/workflow/run"
)

// ApprovalDecisionPreCommitGuard is evaluated inside the same first-result-wins
// database transaction that completes an approval effect. It must only perform
// transaction-local validation/locking; side effects belong to the caller's
// normal delivery path after the durable decision commit.
type ApprovalDecisionPreCommitGuard = runstore.ResolveAwaitingEffectPreCommitGuard

// PreCommitDecisionSink is an optional extension used when an approval carries
// a side effect whose authorization must be fenced with first-decision-wins.
type PreCommitDecisionSink interface {
	DecideApprovalWithPreCommit(context.Context, ApprovalDecision, ApprovalDecisionPreCommitGuard) (ApprovalDecisionResult, error)
}

// ApprovalDecisionPreCommitMutation writes transaction-local approval side
// effects before the terminal effect result is committed. The mutation may
// enrich the durable result JSON with the resulting receipt identity.
type ApprovalDecisionPreCommitMutation = runstore.ResolveAwaitingEffectPreCommitMutation

// PreCommitMessageDecisionSink is the atomic approval path for decisions whose
// comment must be durable before first-decision-wins can commit.
type PreCommitMessageDecisionSink interface {
	DecideApprovalWithPreCommitWrite(context.Context, ApprovalDecision, ApprovalDecisionPreCommitMutation) (ApprovalDecisionResult, error)
}

// ApprovalDecisionReceiptResolver recovers a completed approval's immutable
// result after a post-commit App projection failure. It is deliberately a
// receipt-only read: callers must use the returned frozen result for card
// convergence and must not re-authorize or rewrite the ordinary comment.
type ApprovalDecisionReceiptResolver interface {
	LoadCompletedApproval(context.Context, string, string, string, string) (ApprovalDecisionResult, error)
}

type ApprovalDecisionRepository interface {
	LoadEffectByID(context.Context, string, string) (runstore.Effect, error)
	LoadRun(context.Context, string, string) (runstore.Run, error)
	LoadActivation(context.Context, string, string, string) (runstore.Activation, error)
	ResolveAwaitingEffect(context.Context, runstore.ResolveAwaitingEffectParams) (runstore.ResolveAwaitingEffectResult, error)
}

// ApprovalDecisionService is the durable sink behind the dedicated Workflow
// App. App action authorization is defense in depth; this service repeats the
// exact frozen-assignee, notification, and liveness checks before
// committing first-decision-wins with the graph wakeup in one transaction.
type ApprovalDecisionService struct {
	repository ApprovalDecisionRepository
}

func NewApprovalDecisionService(repository ApprovalDecisionRepository) (*ApprovalDecisionService, error) {
	if repository == nil {
		return nil, fmt.Errorf("workflow Run approval decision repository is required")
	}
	return &ApprovalDecisionService{repository: repository}, nil
}

type approvalDecisionSnapshot struct {
	run        runstore.Run
	activation runstore.Activation
	effect     runstore.Effect
	request    EffectRequest
	receipt    EffectResult
}

func (service *ApprovalDecisionService) DecideApproval(ctx context.Context, decision ApprovalDecision) (ApprovalDecisionResult, error) {
	return service.decideApproval(ctx, decision, nil, nil)
}

// LoadCompletedApproval returns the immutable terminal approval receipt for a
// post-commit projection retry. The identity fence remains server-owned and
// the result is sourced only from the completed effect row; no mutable target,
// author, or comment input is consulted.
func (service *ApprovalDecisionService) LoadCompletedApproval(
	ctx context.Context,
	workspaceID, effectID, notificationID, actorMemberID string,
) (ApprovalDecisionResult, error) {
	if service == nil || service.repository == nil {
		return ApprovalDecisionResult{}, fmt.Errorf("workflow Run approval decision service is not configured")
	}
	workspaceID = strings.TrimSpace(workspaceID)
	effectID = strings.TrimSpace(effectID)
	notificationID = strings.TrimSpace(notificationID)
	actorMemberID = strings.TrimSpace(actorMemberID)
	if workspaceID == "" || effectID == "" || notificationID == "" || actorMemberID == "" {
		return ApprovalDecisionResult{}, fmt.Errorf("workflow Run completed approval receipt identity is required")
	}
	snapshot, err := service.loadApprovalDecisionSnapshot(ctx, workspaceID, effectID)
	if err != nil {
		return ApprovalDecisionResult{}, err
	}
	if snapshot.effect.Status != runstore.EffectStatusCompleted {
		return ApprovalDecisionResult{}, ErrApprovalNotPending
	}
	if snapshot.effect.ApprovalAssigneeMemberID != actorMemberID ||
		snapshot.receipt.AssigneeMemberID != actorMemberID ||
		snapshot.receipt.DecidedByMemberID != actorMemberID ||
		snapshot.receipt.NotificationID != notificationID {
		return ApprovalDecisionResult{}, ErrApprovalUnauthorized
	}
	decidedAt := snapshot.receipt.CompletedAt
	if decidedAt == nil {
		decidedAt = snapshot.effect.CompletedAt
	}
	if decidedAt == nil {
		return ApprovalDecisionResult{}, fmt.Errorf("workflow Run completed approval receipt has no completion time")
	}
	return approvalDecisionResult(snapshot, ApprovalDecision{
		WorkspaceID: workspaceID, EffectID: effectID, NotificationID: notificationID,
		ActorMemberID: actorMemberID, Decision: snapshot.receipt.Port,
		Comment: snapshot.receipt.ApprovalComment, ClientDecisionID: snapshot.receipt.IdempotencyKey,
		DecidedAt: *decidedAt,
	}, true), nil
}

// DecideApprovalWithPreCommit is the trusted Workflow App path for approval
// decisions that carry a side effect which must be authorized against the same
// locked Run/Activation/Effect snapshot as first-decision-wins. The guard is
// invoked only for a first pending decision; exact replays do not repeat it.
// Implementations must keep the guard database-local and side-effect free.
func (service *ApprovalDecisionService) DecideApprovalWithPreCommit(
	ctx context.Context,
	decision ApprovalDecision,
	guard ApprovalDecisionPreCommitGuard,
) (ApprovalDecisionResult, error) {
	if guard == nil {
		return ApprovalDecisionResult{}, fmt.Errorf("workflow Run approval pre-commit guard is required")
	}
	return service.decideApproval(ctx, decision, guard, nil)
}

// DecideApprovalWithPreCommitWrite commits the durable ordinary Human message
// in the same transaction as first-decision-wins. The callback must not perform
// network or post-commit side effects; it may only write through the supplied
// transaction and enrich the effect result receipt.
func (service *ApprovalDecisionService) DecideApprovalWithPreCommitWrite(
	ctx context.Context,
	decision ApprovalDecision,
	mutation ApprovalDecisionPreCommitMutation,
) (ApprovalDecisionResult, error) {
	if mutation == nil {
		return ApprovalDecisionResult{}, fmt.Errorf("workflow Run approval pre-commit message mutation is required")
	}
	return service.decideApproval(ctx, decision, nil, mutation)
}

func (service *ApprovalDecisionService) decideApproval(
	ctx context.Context,
	decision ApprovalDecision,
	guard ApprovalDecisionPreCommitGuard,
	mutation ApprovalDecisionPreCommitMutation,
) (ApprovalDecisionResult, error) {
	if service == nil || service.repository == nil {
		return ApprovalDecisionResult{}, fmt.Errorf("workflow Run approval decision service is not configured")
	}
	decision = decision.Normalize()
	if err := decision.Validate(); err != nil {
		return ApprovalDecisionResult{}, err
	}
	snapshot, err := service.loadApprovalDecisionSnapshot(ctx, decision.WorkspaceID, decision.EffectID)
	if err != nil {
		return ApprovalDecisionResult{}, err
	}
	if snapshot.effect.ApprovalAssigneeMemberID != decision.ActorMemberID ||
		snapshot.receipt.AssigneeMemberID != decision.ActorMemberID {
		return ApprovalDecisionResult{}, ErrApprovalUnauthorized
	}
	if snapshot.receipt.NotificationID != decision.NotificationID {
		return ApprovalDecisionResult{}, ErrApprovalUnauthorized
	}
	if (decision.CommentProvided || decision.Comment != "") && snapshot.request.Intent.FeedbackScope == nil {
		return ApprovalDecisionResult{}, fmt.Errorf("workflow Run approval comment requires a frozen feedback scope")
	}
	expected := approvalTerminalEffectResult(snapshot.receipt, decision)
	if snapshot.effect.Status == runstore.EffectStatusCompleted {
		if !approvalCompletionMatches(snapshot.receipt, expected) {
			return ApprovalDecisionResult{}, ErrApprovalDecisionConflict
		}
		return approvalDecisionResult(snapshot, decision, true), nil
	}
	if snapshot.run.Terminal() || snapshot.effect.Status != runstore.EffectStatusAwaiting ||
		snapshot.activation.Status != runstore.ActivationStatusWaiting {
		return ApprovalDecisionResult{}, ErrApprovalNotPending
	}

	effectResultJSON, err := EncodeEffectResult(expected)
	if err != nil {
		return ApprovalDecisionResult{}, err
	}
	activationOutputJSON := json.RawMessage(`{}`)
	eventPayload, err := json.Marshal(map[string]any{
		"effectId": snapshot.effect.ID, "activationId": snapshot.activation.LogicalKey,
		"nodeId": snapshot.activation.NodeID, "notificationId": decision.NotificationID,
		"decision": decision.Decision, "decidedByMemberId": decision.ActorMemberID,
	})
	if err != nil {
		return ApprovalDecisionResult{}, fmt.Errorf("encode workflow approval decision event: %w", err)
	}
	resolved, err := service.repository.ResolveAwaitingEffect(ctx, runstore.ResolveAwaitingEffectParams{
		WorkspaceID: decision.WorkspaceID, RunID: snapshot.run.ID,
		ActivationID: snapshot.activation.ID, EffectID: snapshot.effect.ID,
		EffectResultJSON: effectResultJSON, ActivationOutputJSON: activationOutputJSON,
		SelectedPort: decision.Decision, RunnableAt: decision.DecidedAt,
		Events:            []runstore.EventDraft{{Type: "approval_decided", PayloadJSON: eventPayload}},
		Now:               decision.DecidedAt,
		PreCommitGuard:    guard,
		PreCommitMutation: mutation,
	})
	if err != nil {
		if errors.Is(err, storeerr.ErrConflict) {
			return service.resolveConcurrentApproval(ctx, decision, expected)
		}
		if errors.Is(err, storeerr.ErrNotFound) {
			return ApprovalDecisionResult{}, ErrApprovalNotFound
		}
		return ApprovalDecisionResult{}, err
	}
	snapshot.run, snapshot.activation, snapshot.effect = resolved.Run, resolved.Activation, resolved.Effect
	committedReceipt, decodeErr := DecodeEffectResult(resolved.Effect.ResultJSON)
	if decodeErr != nil {
		return ApprovalDecisionResult{}, fmt.Errorf("decode committed workflow approval receipt: %w", decodeErr)
	}
	snapshot.receipt = committedReceipt
	return approvalDecisionResult(snapshot, decision, resolved.Replayed), nil
}

func (service *ApprovalDecisionService) resolveConcurrentApproval(ctx context.Context, decision ApprovalDecision, expected EffectResult) (ApprovalDecisionResult, error) {
	snapshot, err := service.loadApprovalDecisionSnapshot(ctx, decision.WorkspaceID, decision.EffectID)
	if err != nil {
		return ApprovalDecisionResult{}, err
	}
	if snapshot.effect.ApprovalAssigneeMemberID != decision.ActorMemberID || snapshot.receipt.AssigneeMemberID != decision.ActorMemberID ||
		snapshot.receipt.NotificationID != decision.NotificationID {
		return ApprovalDecisionResult{}, ErrApprovalUnauthorized
	}
	if snapshot.effect.Status == runstore.EffectStatusCompleted {
		if !approvalCompletionMatches(snapshot.receipt, expected) {
			return ApprovalDecisionResult{}, ErrApprovalDecisionConflict
		}
		return approvalDecisionResult(snapshot, decision, true), nil
	}
	return ApprovalDecisionResult{}, ErrApprovalNotPending
}

func (service *ApprovalDecisionService) loadApprovalDecisionSnapshot(ctx context.Context, workspaceID, effectID string) (approvalDecisionSnapshot, error) {
	effect, err := service.repository.LoadEffectByID(ctx, workspaceID, effectID)
	if err != nil {
		return approvalDecisionSnapshot{}, mapApprovalReadError(err)
	}
	run, err := service.repository.LoadRun(ctx, workspaceID, effect.RunID)
	if err != nil {
		return approvalDecisionSnapshot{}, mapApprovalReadError(err)
	}
	activation, err := service.repository.LoadActivation(ctx, workspaceID, effect.RunID, effect.ActivationID)
	if err != nil {
		return approvalDecisionSnapshot{}, mapApprovalReadError(err)
	}
	request, err := DecodeEffectRequest(effect.RequestJSON)
	if err != nil {
		return approvalDecisionSnapshot{}, fmt.Errorf("decode workflow approval request: %w", err)
	}
	if effect.Kind != EffectKindApproval || request.Intent.Kind != runkernel.IntentApproval ||
		request.Intent.ActivationID != activation.LogicalKey || request.Intent.NodeID != activation.NodeID ||
		strings.TrimSpace(effect.ApprovalAssigneeMemberID) == "" {
		return approvalDecisionSnapshot{}, ErrApprovalNotFound
	}
	if len(bytes.TrimSpace(effect.ResultJSON)) == 0 {
		return approvalDecisionSnapshot{}, ErrApprovalNotPending
	}
	receipt, err := DecodeEffectResult(effect.ResultJSON)
	if err != nil {
		return approvalDecisionSnapshot{}, fmt.Errorf("decode workflow approval receipt: %w", err)
	}
	if receipt.NotificationID == "" || receipt.MessageID == "" || receipt.AssigneeMemberID == "" {
		return approvalDecisionSnapshot{}, fmt.Errorf("workflow approval receipt is incomplete")
	}
	return approvalDecisionSnapshot{run: run, activation: activation, effect: effect, request: request, receipt: receipt}, nil
}

func approvalTerminalEffectResult(receipt EffectResult, decision ApprovalDecision) EffectResult {
	result := receipt.Normalize()
	result.Port = decision.Decision
	result.Output = runkernel.Values{}
	result.DecidedByMemberID = decision.ActorMemberID
	result.IdempotencyKey = decision.ClientDecisionID
	result.ApprovalComment = decision.Comment
	result.CompletedAt = &decision.DecidedAt
	result.FailureCode, result.FailureMessage = "", ""
	return result.Normalize()
}

func approvalCompletionMatches(actual, expected EffectResult) bool {
	actual, expected = actual.Normalize(), expected.Normalize()
	return actual.Port == expected.Port && actual.IdempotencyKey == expected.IdempotencyKey &&
		actual.DecidedByMemberID == expected.DecidedByMemberID && actual.NotificationID == expected.NotificationID &&
		actual.AssigneeMemberID == expected.AssigneeMemberID && actual.ApprovalComment == expected.ApprovalComment
}

func approvalDecisionResult(snapshot approvalDecisionSnapshot, decision ApprovalDecision, replayed bool) ApprovalDecisionResult {
	decidedAt := decision.DecidedAt
	decidedBy := decision.ActorMemberID
	committedDecision := decision.Decision
	if snapshot.effect.Status == runstore.EffectStatusCompleted {
		if snapshot.receipt.CompletedAt != nil {
			decidedAt = *snapshot.receipt.CompletedAt
		}
		if snapshot.receipt.DecidedByMemberID != "" {
			decidedBy = snapshot.receipt.DecidedByMemberID
		}
		if snapshot.receipt.Port == ApprovalDecisionApproved || snapshot.receipt.Port == ApprovalDecisionRejected {
			committedDecision = snapshot.receipt.Port
		}
	}
	return ApprovalDecisionResult{
		WorkspaceID: snapshot.run.WorkspaceID, PlanID: snapshot.run.PlanID,
		PlanVersionID: snapshot.run.PlanVersionID, RunID: snapshot.run.ID,
		ActivationID: snapshot.activation.LogicalKey, EffectID: snapshot.effect.ID,
		NodeID: snapshot.activation.NodeID, AssigneeMemberID: snapshot.effect.ApprovalAssigneeMemberID,
		Summary: snapshot.request.Intent.Summary, FeedbackScope: cloneMessageScopeValue(snapshot.request.Intent.FeedbackScope),
		Comment: snapshot.receipt.ApprovalComment, FeedbackMessageID: snapshot.receipt.ApprovalFeedbackMessageID,
		FeedbackClientMessageID: snapshot.receipt.ApprovalFeedbackClientMessageID,
		Decision:                committedDecision, DecidedByMemberID: decidedBy, DecidedAt: decidedAt,
		Replayed: replayed,
	}
}

func mapApprovalReadError(err error) error {
	if errors.Is(err, storeerr.ErrNotFound) {
		return ErrApprovalNotFound
	}
	return err
}

var _ DecisionSink = (*ApprovalDecisionService)(nil)
var _ PreCommitDecisionSink = (*ApprovalDecisionService)(nil)
var _ PreCommitMessageDecisionSink = (*ApprovalDecisionService)(nil)
var _ ApprovalDecisionReceiptResolver = (*ApprovalDecisionService)(nil)
