package run

import (
	"context"
	"encoding/json"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/workflow/invocation"
	"github.com/tidbcloud/AgentTeam/internal/workflow/plan"
	"github.com/tidbcloud/AgentTeam/internal/workflow/runartifact"
)

const (
	RunStatusQueued    = "queued"
	RunStatusRunning   = "running"
	RunStatusWaiting   = "waiting"
	RunStatusSucceeded = "succeeded"
	RunStatusFailed    = "failed"
	RunStatusCanceled  = "canceled"

	ActivationStatusReady     = "ready"
	ActivationStatusWaiting   = "waiting"
	ActivationStatusSucceeded = "succeeded"
	ActivationStatusFailed    = "failed"
	ActivationStatusCanceled  = "canceled"

	EffectStatusPending        = "pending"
	EffectStatusDispatching    = "dispatching"
	EffectStatusDelivered      = "delivered"
	EffectStatusAwaiting       = "awaiting"
	EffectStatusQueuedForAgent = "queued_for_agent"
	// EffectStatusWaitingForResponder is a durably parked callback that declared
	// an ordered candidate responder pool (responder.selectOne) and every
	// candidate's callback capacity is currently occupied. The effect is NOT
	// bound to any single candidate and does NOT occupy any candidate's FIFO
	// queue; it waits for any candidate to free, then is promoted (select+claim
	// +freeze) by PromoteCallbackResponderWaiters.
	EffectStatusWaitingForResponder = "waiting_for_responder"
	EffectStatusCompleted           = "completed"
	EffectStatusFailed              = "failed"
	EffectStatusCanceled            = "canceled"

	EffectKindMessage  = "send_message"
	EffectKindApproval = "request_approval"

	MutationInsert = "insert"
	MutationUpdate = "update"

	// EffectParkStateNone is the durable park FSM's terminal-inactive state: the
	// callback is not parked and the ordinary recovery schedule applies.
	EffectParkStateNone = ""
	// EffectParkStateArmOnIdle is set by the public callback `park` action: the
	// effect stays awaiting, no recovery send fires, and the FSM waits for the
	// first authoritative known idle observation before confirming parked.
	EffectParkStateArmOnIdle = "arm_on_idle"
	// EffectParkStateParked is confirmed when the first authoritative known idle
	// observation arrives after arming. No recovery send fires while parked; the
	// first authoritative known working observation clears the park and resets
	// the recovery schedule atomically.
	EffectParkStateParked = "parked"
)

// AgentPrincipal is the authenticated Agent identity carried by Agent-facing
// Run read/control operations. Both AgentID and MemberID are required; neither
// is inferred from artifact coordinates or request JSON.
type AgentPrincipal struct {
	WorkspaceID string
	AgentID     string
	MemberID    string
}

// Run is the durable mutable half of an immutable Invocation. All identity and
// authoring references are frozen snapshots copied at Invocation acceptance.
type Run struct {
	ID           string
	WorkspaceID  string
	InvocationID string
	// SourceKind and InvokedBy are immutable acceptance provenance retained for
	// audit and response projection. Agent Run-read authority is the current
	// machine-owner match against PrincipalMemberID, never this provenance.
	SourceKind    string
	InvokedBy     invocation.Actor
	PlanID        string
	PlanVersionID string
	// TeamID is the server-authorized Team frozen at Invocation acceptance from
	// the approved, activated Plan/Team binding or the invoking Agent's explicit
	// teamId. It is NULL only for a dynamic Run whose caller omitted teamId. It
	// is authoritative context only: it is never derived from visible message
	// text or a callback request.
	TeamID               string
	WorkflowNameSnapshot string
	PlanKey              string
	PlanDisplayName      string
	BindingID            string
	BindingKey           string
	BindingDisplayName   string
	Status               string
	InputJSON            json.RawMessage
	InputDigest          string
	StateJSON            json.RawMessage
	OutputJSON           json.RawMessage
	ErrorCode            string
	ErrorJSON            json.RawMessage
	Identity             invocation.ExecutionIdentitySnapshot
	QueuedAt             time.Time
	StartedAt            *time.Time
	CompletedAt          *time.Time
	CanceledAt           *time.Time
	Revision             int64
	// LastEventSeq is the last event sequence already allocated. Invocation
	// acceptance initializes it to the run_accepted event at sequence 1;
	// appenders increment it while holding the Run lock.
	LastEventSeq int64
	RunnableAt   time.Time
	LeaseOwner   string
	LeaseToken   string
	LeaseExpires *time.Time
	CreatedAt    time.Time
	UpdatedAt    time.Time
}

func (run Run) Terminal() bool {
	switch run.Status {
	case RunStatusSucceeded, RunStatusFailed, RunStatusCanceled:
		return true
	default:
		return false
	}
}

// Activation is one logical visit to one compiled artifact node. Reducer
// scope/parallel/loop state crosses this package only as deterministic JSON.
type Activation struct {
	ID                 string
	WorkspaceID        string
	RunID              string
	LogicalKey         string
	ParentActivationID string
	NodeID             string
	Opcode             string
	Status             string
	InputJSON          json.RawMessage
	OutputJSON         json.RawMessage
	ScopeContextJSON   json.RawMessage
	SelectedPort       string
	AttemptCount       int64
	RetryAt            *time.Time
	WakeAt             *time.Time
	DeadlineAt         *time.Time
	ErrorCode          string
	ErrorJSON          json.RawMessage
	StartedAt          *time.Time
	CompletedAt        *time.Time
	CreatedAt          time.Time
	UpdatedAt          time.Time
}

// Effect is a durable external-work outbox item. IdempotencyKey is semantic
// and stable across infrastructure lease recovery.
type Effect struct {
	ID                       string
	WorkspaceID              string
	RunID                    string
	ActivationID             string
	ApprovalAssigneeMemberID string
	CallbackResponderAgentID string
	MessageID                string
	Kind                     string
	IdempotencyKey           string
	Status                   string
	RequestJSON              json.RawMessage
	ResultJSON               json.RawMessage
	AttemptCount             int64
	RetryAt                  *time.Time
	DeadlineAt               *time.Time
	LeaseOwner               string
	LeaseToken               string
	LeaseExpiresAt           *time.Time
	ErrorCode                string
	ErrorJSON                json.RawMessage
	DeliveredAt              *time.Time
	CompletedAt              *time.Time
	QueuedAt                 *time.Time
	// RecoveryAttemptCount is the durable, public compatibility history of
	// automatic recovery sends for this callback. It is incremented only when a
	// recovery send wins a tick and is never reset by Agent success, epoch
	// rebasing, pause, or park.
	RecoveryAttemptCount int
	// FirstNonWorkingAt and NextRecoveryAt are the legacy compatibility
	// snapshot. Recovery scheduling never reads or rewrites them; terminal and
	// pause lifecycle transitions may still clear the legacy next timestamp as
	// they did before the current-cycle projection existed.
	FirstNonWorkingAt *time.Time
	NextRecoveryAt    *time.Time
	// RecoveryCycleAttemptCount/First/Next are the authoritative schedule for
	// the current Agent recovery epoch. They may reset when a successful Agent
	// action advances the epoch or when park is cleared; the public legacy count
	// above must not be reset with them.
	RecoveryCycleAttemptCount      int
	RecoveryCycleFirstNonWorkingAt *time.Time
	RecoveryCycleNextRecoveryAt    *time.Time
	// RecoveryPausedAt is set by the non-terminal callback `pause` action: the
	// effect stays awaiting but automatic recovery is stopped without any resume
	// trigger. Only callback complete/fail, cancel, or terminal lifecycle
	// ends/cleans it; later Agent activity never unpauses it.
	RecoveryPausedAt *time.Time
	// RecoveryEpoch identifies the Agent recovery cycle whose legacy per-effect
	// schedule is currently materialized. It is a fencing marker; the
	// Agent-scoped recovery state is authoritative for reset/cap semantics.
	RecoveryEpoch int64
	// ParkState is the durable park FSM state (see EffectParkState*): "" (none),
	// "arm_on_idle", or "parked". It is a separate FSM from RecoveryPausedAt;
	// the park is cleared and the recovery schedule reset atomically by the first
	// authoritative known working observation after parked.
	ParkState string
	// ParkArmedAt is when the public `park` action armed the FSM (arm_on_idle).
	ParkArmedAt *time.Time
	// ParkParkedAt is when the first authoritative known idle observation
	// confirmed parked.
	ParkParkedAt *time.Time
	// ParkClearedAt is when the first authoritative known working observation
	// cleared the park and reset the recovery schedule.
	ParkClearedAt *time.Time
	CreatedAt     time.Time
	UpdatedAt     time.Time
}

type Event struct {
	ID          string
	WorkspaceID string
	RunID       string
	Seq         int64
	Type        string
	PayloadJSON json.RawMessage
	CreatedAt   time.Time
}

// RunnableCandidate carries a DB-issued workspace-placement generation. The
// private fence prevents callers from inventing a candidate and bypassing
// placement ownership checks.
type RunnableCandidate struct {
	runID          string
	workspaceID    string
	workspaceGroup string
	placementState string
	placementEpoch int64
}

func (candidate RunnableCandidate) RunID() string       { return candidate.runID }
func (candidate RunnableCandidate) WorkspaceID() string { return candidate.workspaceID }

type ClaimRunnableParams struct {
	WorkerID string
	Now      time.Time
	LeaseTTL time.Duration
	Limit    int
}

type ClaimedRun struct {
	Run   Run
	Fence TurnFence
}

type TurnFence struct {
	WorkspaceID string
	RunID       string
	LeaseToken  string
}

type ListRunsParams struct {
	WorkspaceID     string
	PlanID          string
	Status          string
	BeforeStartedAt *time.Time
	BeforeID        string
	Limit           int
}

type ListChildrenParams struct {
	WorkspaceID string
	RunID       string
	Status      string
	AfterID     string
	Limit       int
}

type ListEventsParams struct {
	WorkspaceID string
	RunID       string
	AfterSeq    int64
	Limit       int
}

type ActivationMutation struct {
	Operation      string
	Value          Activation
	ExpectedStatus string
}

type EffectMutation struct {
	Operation      string
	Value          Effect
	ExpectedStatus string
}

type EventDraft struct {
	Type        string
	PayloadJSON json.RawMessage
}

// SaveTurnParams is the generic reducer persistence boundary. The Run snapshot
// is complete rather than a reducer-specific patch, while activation/effect
// mutations and events are committed in the same transaction and revision.
type SaveTurnParams struct {
	Fence            TurnFence
	ExpectedRevision int64
	Status           string
	StateJSON        json.RawMessage
	// Artifacts are immutable oversized aggregate writes prepared by the
	// reducer projector. They are inserted in the same transaction before any
	// child mutation or Run snapshot update.
	Artifacts    []runartifact.Draft
	OutputJSON   json.RawMessage
	ErrorCode    string
	ErrorJSON    json.RawMessage
	RunnableAt   time.Time
	StartedAt    *time.Time
	CompletedAt  *time.Time
	CanceledAt   *time.Time
	ReleaseLease bool
	Activations  []ActivationMutation
	Effects      []EffectMutation
	Events       []EventDraft
	Now          time.Time
}

type SaveTurnResult struct {
	Run    Run
	Events []Event
}

// ArtifactMaintenanceResult reports one bounded production retention pass.
// Assignment happens only after a Run is terminal and has no live children;
// deletion is limited to already-expired terminal artifacts.
type ArtifactMaintenanceResult struct {
	AssignedExpiry int64
	Deleted        int64
	OrphanedBlobs  int64
}

// FailClaimedRunParams terminals a deterministically poisoned reducer turn.
// Unlike SaveTurn, the store owns child cleanup and the dispatch drain barrier
// because a corrupt/missing executable artifact may make normal projection
// impossible.
type FailClaimedRunParams struct {
	Fence            TurnFence
	ExpectedRevision int64
	StateJSON        json.RawMessage
	ErrorCode        string
	ErrorJSON        json.RawMessage
	Now              time.Time
}

type FailClaimedRunResult struct {
	Run      Run
	Event    *Event
	Draining bool
}

type ListPendingEffectsParams struct {
	WorkspaceGroupID string
	Now              time.Time
	AfterID          string
	Limit            int
}

type EffectCandidate struct {
	EffectID       string
	WorkspaceID    string
	runID          string
	workspaceGroup string
	placementState string
	placementEpoch int64
}

type ClaimEffectParams struct {
	Candidate EffectCandidate
	WorkerID  string
	Now       time.Time
	LeaseTTL  time.Duration
}

// ResolveEffectParams acknowledges one fenced dispatch. Status may be
// delivered/awaiting/completed/failed; retryable infrastructure failures use
// pending with RetryAt. The owning Run is woken and optional events are
// appended under its event-sequence lock.
type ResolveEffectParams struct {
	WorkspaceID              string
	RunID                    string
	EffectID                 string
	LeaseToken               string
	Status                   string
	CallbackResponderAgentID string
	ResultJSON               json.RawMessage
	ErrorCode                string
	ErrorJSON                json.RawMessage
	RetryAt                  *time.Time
	DeliveredAt              *time.Time
	CompletedAt              *time.Time
	RunnableAt               time.Time
	Events                   []EventDraft
	Now                      time.Time
}

type ListAwaitingCallbackEffectsParams struct {
	WorkspaceID string
	AgentID     string
	MemberID    string
	AfterID     string
	Limit       int
}

// ResponderCandidateWaiter is the durable FIFO waiter position of one
// waiting_for_responder selectOne callback effect. It records the frozen
// ordered candidate pool and the FIFO position within (workspace, pool), so a
// candidate freeing capacity can promote the OLDEST waiter for the same pool.
type ResponderCandidateWaiter struct {
	ID                string
	WorkspaceID       string
	RunID             string
	ActivationID      string
	EffectID          string
	NodeID            string
	CandidatePoolKey  string
	CandidateAgentIDs []string
	// SelectOneStrategy is the durable frozen selectOne strategy ("" or
	// "ordered" = legacy strict priority, "random" = injectable unbiased
	// randomization). It is persisted on the waiter row so promotion and
	// restart/recovery re-select with the same replay-safe strategy and random
	// ordering as the original arrival.
	SelectOneStrategy plan.SelectOneStrategy
	WaiterSeq         int64
	// GlobalSeq is the workspace-monotonic arrival sequence used by the
	// promotion scan as its stable total order. It is monotonic per workspace
	// (tie-break effect_id) so the scan advances past unsatisfiable waiters
	// instead of re-reading the oldest page from the head.
	GlobalSeq int64
	Status    string
	CreatedAt time.Time
	UpdatedAt time.Time
}

const (
	// ResponderWaiterStatus* are the durable candidate-pool waiter lifecycle
	// statuses. waiting means the selectOne effect is parked; selected means it
	// was promoted and its responder frozen; canceled/completed are terminal.
	ResponderWaiterStatusWaiting   = "waiting"
	ResponderWaiterStatusSelected  = "selected"
	ResponderWaiterStatusCanceled  = "canceled"
	ResponderWaiterStatusCompleted = "completed"
)

type CancelRunParams struct {
	WorkspaceID string
	RunID       string
	ReasonJSON  json.RawMessage
	Now         time.Time
}

// ResolveAwaitingEffectPreCommitGuard performs authorization that must share
// the first-result-wins transaction with an awaiting-effect completion. The
// guard runs only for the first still-resolvable receipt, after the Run,
// Activation, and Effect have been locked and validated, and immediately
// before any durable completion write. It does not run for exact completed
// replays. Implementations must be database-local and free of network calls or
// business side effects.
type ResolveAwaitingEffectPreCommitGuard func(
	context.Context,
	coredb.Tx,
	Run,
	Activation,
	Effect,
) error

// ResolveAwaitingEffectPreCommitMutation performs durable writes that must
// commit with an awaiting-effect completion. Unlike the authorization-only
// guard above, this hook may write transaction-local rows (message, outbox,
// and idempotency records) and may update the result JSON before the effect is
// completed. It must not perform network or other external side effects.
type ResolveAwaitingEffectPreCommitMutation func(
	context.Context,
	coredb.Tx,
	Run,
	Activation,
	Effect,
	*json.RawMessage,
) error

// ResolveAwaitingEffectParams is shared by authenticated Agent callbacks and
// Workflow app approval decisions. The caller owns principal/assignee checks;
// the store owns first-result-wins idempotency and graph wakeup atomically.
type ResolveAwaitingEffectParams struct {
	WorkspaceID          string
	RunID                string
	ActivationID         string
	EffectID             string
	EffectResultJSON     json.RawMessage
	ActivationOutputJSON json.RawMessage
	SelectedPort         string
	ScopeContextJSON     json.RawMessage
	RunnableAt           time.Time
	Events               []EventDraft
	Now                  time.Time
	// AgentID and MemberID are populated only by authenticated callback callers.
	// Approval callers leave them empty; callback completion rechecks this
	// frozen grant after locking Run, Activation, and Effect.
	AgentID           string
	MemberID          string
	PreCommitGuard    ResolveAwaitingEffectPreCommitGuard
	PreCommitMutation ResolveAwaitingEffectPreCommitMutation
}

type ResolveAwaitingEffectResult struct {
	Run        Run
	Activation Activation
	Effect     Effect
	Events     []Event
	Replayed   bool
}
