package orchestrator

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

	"github.com/tidbcloud/AgentTeam/internal/core/domain"
)

const (
	ApprovalDecisionApproved        = "approved"
	ApprovalDecisionRejected        = "rejected"
	ApprovalDecisionCommentMaxRunes = 4000
)

var (
	ErrApprovalNotFound         = errors.New("workflow Run approval not found")
	ErrApprovalUnauthorized     = errors.New("workflow Run approval actor is not the assignee")
	ErrApprovalNotPending       = errors.New("workflow Run approval is no longer pending")
	ErrApprovalDecisionConflict = errors.New("workflow Run approval already has a different decision")
)

// ApprovalRequest is the immutable, fully rendered request dispatched from a
// request_approval effect. AssigneeMemberID is an exact active Human member,
// never an authoring alias to be resolved by the Workflow App.
type ApprovalRequest struct {
	WorkspaceID      string                    `json:"workspaceId"`
	PlanID           string                    `json:"planId"`
	PlanVersionID    string                    `json:"planVersionId"`
	RunID            string                    `json:"runId"`
	ActivationID     string                    `json:"activationId"`
	EffectID         string                    `json:"effectId"`
	NodeID           string                    `json:"nodeId"`
	AssigneeMemberID string                    `json:"assigneeMemberId"`
	Summary          string                    `json:"summary"`
	FeedbackScope    *domain.MessageScopeValue `json:"feedbackScope,omitempty"`
	CausedByMemberID string                    `json:"causedByMemberId"`
}

func (request ApprovalRequest) Normalize() ApprovalRequest {
	request.WorkspaceID = strings.TrimSpace(request.WorkspaceID)
	request.PlanID = strings.TrimSpace(request.PlanID)
	request.PlanVersionID = strings.TrimSpace(request.PlanVersionID)
	request.RunID = strings.TrimSpace(request.RunID)
	request.ActivationID = strings.TrimSpace(request.ActivationID)
	request.EffectID = strings.TrimSpace(request.EffectID)
	request.NodeID = strings.TrimSpace(request.NodeID)
	request.AssigneeMemberID = strings.TrimSpace(request.AssigneeMemberID)
	request.Summary = strings.TrimSpace(request.Summary)
	if request.FeedbackScope != nil {
		scope := *request.FeedbackScope
		request.FeedbackScope = &scope
	}
	request.CausedByMemberID = strings.TrimSpace(request.CausedByMemberID)
	return request
}

func (request ApprovalRequest) Validate() error {
	request = request.Normalize()
	required := map[string]string{
		"workspaceId": request.WorkspaceID, "planId": request.PlanID,
		"planVersionId": request.PlanVersionID, "runId": request.RunID,
		"activationId": request.ActivationID, "effectId": request.EffectID,
		"nodeId": request.NodeID, "assigneeMemberId": request.AssigneeMemberID,
		"causedByMemberId": request.CausedByMemberID,
	}
	for name, value := range required {
		if value == "" {
			return fmt.Errorf("workflow Run approval %s is required", name)
		}
	}
	if request.Summary == "" || len([]rune(request.Summary)) > 4000 {
		return fmt.Errorf("workflow Run approval summary must contain 1..4000 characters")
	}
	if request.FeedbackScope != nil {
		if err := domain.ValidateMessageScopeValue(*request.FeedbackScope); err != nil {
			return fmt.Errorf("workflow Run approval feedbackScope is invalid: %w", err)
		}
	}
	return nil
}

type ApprovalReceipt struct {
	NotificationID       string `json:"notificationId"`
	MessageID            string `json:"messageId"`
	NotificationRevision int64  `json:"notificationRevision"`
	IdempotentReplay     bool   `json:"idempotentReplay"`
}

// ApprovalWriter is the only App surface needed by the effect dispatcher.
// Durable effect attempt/retry/receipt ownership stays in run.Store.
type ApprovalWriter interface {
	WriteApproval(context.Context, ApprovalRequest) (ApprovalReceipt, error)
}

// ApprovalDecision carries trusted Human/action identity from the App action
// delivery. The sink must atomically load EffectID, verify exact assignee,
// and Run/effect liveness, and implement first-decision-wins.
type ApprovalDecision struct {
	WorkspaceID      string    `json:"workspaceId"`
	EffectID         string    `json:"effectId"`
	NotificationID   string    `json:"notificationId"`
	ActorMemberID    string    `json:"actorMemberId"`
	Decision         string    `json:"decision"`
	Comment          string    `json:"comment,omitempty"`
	CommentProvided  bool      `json:"-"`
	ClientDecisionID string    `json:"clientDecisionId"`
	DecidedAt        time.Time `json:"decidedAt"`
}

func (decision ApprovalDecision) Normalize() ApprovalDecision {
	decision.WorkspaceID = strings.TrimSpace(decision.WorkspaceID)
	decision.EffectID = strings.TrimSpace(decision.EffectID)
	decision.NotificationID = strings.TrimSpace(decision.NotificationID)
	decision.ActorMemberID = strings.TrimSpace(decision.ActorMemberID)
	decision.Decision = strings.TrimSpace(decision.Decision)
	decision.Comment = strings.TrimSpace(decision.Comment)
	decision.ClientDecisionID = strings.TrimSpace(decision.ClientDecisionID)
	if !decision.DecidedAt.IsZero() {
		decision.DecidedAt = decision.DecidedAt.UTC()
	}
	return decision
}

func (decision ApprovalDecision) Validate() error {
	decision = decision.Normalize()
	if decision.WorkspaceID == "" || decision.EffectID == "" || decision.NotificationID == "" ||
		decision.ActorMemberID == "" || decision.ClientDecisionID == "" || decision.DecidedAt.IsZero() {
		return fmt.Errorf("workflow Run approval decision identity and timestamp are required")
	}
	if decision.Decision != ApprovalDecisionApproved && decision.Decision != ApprovalDecisionRejected {
		return fmt.Errorf("workflow Run approval decision must be approved or rejected")
	}
	if len([]rune(decision.Comment)) > ApprovalDecisionCommentMaxRunes {
		return fmt.Errorf("workflow Run approval comment must be at most %d characters", ApprovalDecisionCommentMaxRunes)
	}
	return nil
}

// ApprovalDecisionResult is the authoritative result returned after the
// durable sink commits. It contains the frozen card snapshot required to
// converge the visible notification without re-reading mutable Plan data.
type ApprovalDecisionResult struct {
	WorkspaceID             string                    `json:"workspaceId"`
	PlanID                  string                    `json:"planId"`
	PlanVersionID           string                    `json:"planVersionId"`
	RunID                   string                    `json:"runId"`
	ActivationID            string                    `json:"activationId"`
	EffectID                string                    `json:"effectId"`
	NodeID                  string                    `json:"nodeId"`
	AssigneeMemberID        string                    `json:"assigneeMemberId"`
	Summary                 string                    `json:"summary"`
	FeedbackScope           *domain.MessageScopeValue `json:"feedbackScope,omitempty"`
	Comment                 string                    `json:"comment,omitempty"`
	FeedbackMessageID       string                    `json:"feedbackMessageId,omitempty"`
	FeedbackClientMessageID string                    `json:"feedbackClientMessageId,omitempty"`
	Decision                string                    `json:"decision"`
	DecidedByMemberID       string                    `json:"decidedByMemberId"`
	DecidedAt               time.Time                 `json:"decidedAt"`
	Replayed                bool                      `json:"replayed"`
}

func (result ApprovalDecisionResult) Validate() error {
	request := ApprovalRequest{
		WorkspaceID: result.WorkspaceID, PlanID: result.PlanID,
		PlanVersionID: result.PlanVersionID, RunID: result.RunID,
		ActivationID: result.ActivationID, EffectID: result.EffectID,
		NodeID: result.NodeID, AssigneeMemberID: result.AssigneeMemberID,
		Summary: result.Summary, FeedbackScope: result.FeedbackScope,
		CausedByMemberID: result.DecidedByMemberID,
	}
	if err := request.Validate(); err != nil {
		return err
	}
	if result.Decision != ApprovalDecisionApproved && result.Decision != ApprovalDecisionRejected {
		return fmt.Errorf("workflow Run approval result decision is invalid")
	}
	if result.FeedbackScope != nil {
		if err := domain.ValidateMessageScopeValue(*result.FeedbackScope); err != nil {
			return fmt.Errorf("workflow Run approval result feedbackScope is invalid: %w", err)
		}
	}
	if len([]rune(strings.TrimSpace(result.Comment))) > ApprovalDecisionCommentMaxRunes {
		return fmt.Errorf("workflow Run approval result comment is too long")
	}
	if strings.TrimSpace(result.Comment) != "" && result.FeedbackScope == nil {
		return fmt.Errorf("workflow Run approval result comment requires a frozen feedback scope")
	}
	if strings.TrimSpace(result.DecidedByMemberID) != strings.TrimSpace(result.AssigneeMemberID) || result.DecidedAt.IsZero() {
		return fmt.Errorf("workflow Run approval result actor/timestamp is invalid")
	}
	return nil
}

// DecisionSink is implemented by the durable Run runtime, not by the App.
type DecisionSink interface {
	DecideApproval(context.Context, ApprovalDecision) (ApprovalDecisionResult, error)
}
