package drivercontract

import (
	"context"
	"fmt"
	"io"
	"os/exec"
	"strings"

	"github.com/tidbcloud/AgentTeam/desktop/internal/agent/runtimeconfig"
	"github.com/tidbcloud/AgentTeam/desktop/internal/promptcompiler"
	"github.com/tidbcloud/AgentTeam/desktop/internal/types"
)

// ParsedEvent is a normalized runtime event parsed from CLI stdout.
type ParsedEvent struct {
	Kind           string
	Text           string
	ToolName       string
	ToolCallID     string
	ToolInput      interface{}
	PromptKind     string
	Runtime        string
	EventType      string
	SessionID      string
	RuntimeInputID string
	Message        string
	Title          string
	SectionID      string
	Metadata       map[string]any

	// StopReason is the LLM stop reason for consts.ParsedEventKindTurnEnd events, when available.
	// Common values: "end_turn", "max_tokens", "tool_use", "stop_sequence".
	StopReason string

	// Usage fields — populated when Kind == consts.ParsedEventKindUsage.
	InputTokens         uint64
	CachedInputTokens   uint64
	OutputTokens        uint64
	ActiveContextTokens uint64
	ContextWindowTokens uint64
	Model               string
	// ModelInvocationID is populated only when the runtime exposes a stable,
	// unique model-call identity. Usage notification count must not substitute.
	ModelInvocationID string
	// ContextExposureKnown is true only when this usage belongs to a model
	// invocation whose active AgentTeam-compiled Context is the Process Context.
	// Runtime-native child/subagent usage must leave it false.
	ContextExposureKnown bool
	// ContextExposureExcluded marks a known invocation that did not consume the
	// Process root Context, such as a runtime-native subagent or summary call.
	ContextExposureExcluded bool
	// ContextCompacted marks the first root invocation observed after a
	// runtime-native compaction boundary.
	ContextCompacted bool
}

// ProviderConfig holds the upstream LLM configuration to inject into the
// runtime process when an agent uses inline LLM settings.
type ProviderConfig struct {
	ID      string
	Name    string
	BaseURL string
	APIKey  string
	// MetricsAPIKey overrides the request-metrics key recorded by the daemon
	// when the runtime must use a short-lived local bridge credential instead
	// of a stable upstream secret.
	MetricsAPIKey  string
	CredentialKind string
	EnvKey         string
	Headers        map[string]string
	WireAPIs       []string
}

// ExecConfig carries the run-bound `agentteam exec` action transport. BaseURL and
// TokenFile are injected into the runtime. AuthorizationID remains daemon-only
// and identifies the process authorization revoked during teardown.
type ExecConfig struct {
	BaseURL         string
	AuthorizationID string
	TokenFile       string
}

// LaunchConfig contains the parameters required to launch an interactive agent runtime.
type LaunchConfig struct {
	// Identity
	AgentID     string
	RuntimeType runtimeconfig.RuntimeType

	// Session & workspace
	SessionID string
	LaunchID  string
	WorkDir   string
	// RuntimeStateDir is the stable per-agent root for AgentTeam-owned runtime
	// persistence. It must not vary with session, execution, or ephemeral
	// launch workdirs.
	RuntimeStateDir string
	// DisableRecovery prevents process-owned resume safety, context recovery,
	// idle compaction, and checkpoint side effects for non-resumable launches.
	DisableRecovery bool

	// Prompts & instructions. Semantic launch surfaces are available only through
	// the compiler-produced sealed carrier. Attachments remain typed transport
	// facts and are resolved before the carrier is compiled.
	CompiledPrompt    *promptcompiler.CompiledPrompt `json:"-"`
	PromptAttachments []types.RuntimeAttachment

	// Contract
	ContractCarrierFile string

	// Runtime configuration
	Binary              string
	PermissionMode      string
	Model               string
	SmallModel          string
	ReasoningEffort     string
	Env                 map[string]string
	Provider            *ProviderConfig
	Exec                *ExecConfig
	RuntimeDebugEnabled bool
	// DisableNativeTools removes provider-native shell, filesystem, browser,
	// app, plugin, memory, goal, image, and delegation tools. The model then
	// sees only the explicitly configured MCP tool set.
	DisableNativeTools bool
	// Tools, skills & delivery
	MCPs                     []types.NamedMCPConfig
	DisallowedTools          []string
	InitialDeliveryMessageID string
	// SkillDir is the global AgentTeam skills directory. When a runtime supports
	// native skill discovery, startup materializes its skills into the
	// runtime's workdir-specific skill location before spawn.
	SkillDir string
	// ManagedSkillDirs are absolute paths to locally cloned managed skill
	// vaults. They are merged with SkillDir at materialize time using a
	// priority-based scheme: lower priority number wins on name collisions.
	// The list is a snapshot taken at launch config construction; later
	// changes to the on-disk vault set do not affect the running process
	// until the next launch.
	ManagedSkillDirs []string
	// AnalysisToken is the daemon-issued, read-only self-scope credential that
	// must be revoked with the runtime process.
	AnalysisToken string
	// BuiltinSkills is a launch-scoped snapshot of daemon-provided skills
	// whose visibility is coupled to capabilities in this LaunchConfig.
	// Runtime materialization copies them and removes stale copies on a later
	// launch where the capability is absent.
	BuiltinSkills []BuiltinSkill
	// SkillPlan is the compiler-owned ordering/precedence/budget decision
	// consumed by runtime skill materialization.
	SkillPlan promptcompiler.SkillPlan
}

// BuiltinSkill is an immutable daemon-provided runtime skill snapshot.
type BuiltinSkill struct {
	Name    string
	Content string
}

// EffectivePermissionMode returns the launch permission mode, preserving
// historical full-access behavior when older local config has no explicit mode
// and coercing legacy workspace-only selections to full access.
func (c LaunchConfig) EffectivePermissionMode() (string, error) {
	switch types.NormalizeAgentPermissionMode(c.PermissionMode) {
	case types.AgentPermissionModeFullAccess:
		return types.AgentPermissionModeFullAccess, nil
	default:
		return "", fmt.Errorf("unsupported agent permission mode %q", c.PermissionMode)
	}
}

// RuntimeInput is one compiler-produced user-visible message prepared by the
// process layer for runtime delivery. It carries transport identity and
// attachments separately, while semantic text is available only through the
// sealed CompiledPrompt.
type RuntimeInput struct {
	// ID is an optional daemon delivery identity that lets runtimes echo a
	// correlated runtime_input_acked event when the runtime acknowledges
	// responsibility for the body at its supported input boundary.
	ID string
	// Resume selects the compiler's resume launch surface for an initial
	// runtime submission. It is transport/session state, not prompt text.
	Resume bool
	// CompiledPrompt is produced by promptcompiler for the delivery envelope;
	// callers cannot construct a valid semantic carrier themselves.
	CompiledPrompt *promptcompiler.CompiledPrompt `json:"compiledPrompt,omitempty"`
	Attachments    []types.RuntimeAttachment
}

// PromptText returns the compiler-produced delivery surface for transport
// serialization. Empty is returned for an invalid or non-delivery carrier.
func (i RuntimeInput) PromptText() string {
	if i.CompiledPrompt == nil || !i.CompiledPrompt.Valid() {
		return ""
	}
	if text := i.CompiledPrompt.SlotText("deliveryText"); text != "" {
		return text
	}
	if text := i.CompiledPrompt.SlotText("turnInput"); text != "" {
		return text
	}
	if i.CompiledPrompt.HasLaunchCarrier() {
		if i.Resume {
			if text := i.CompiledPrompt.LaunchSurface("resumePrompt"); text != "" {
				return text
			}
		} else if text := i.CompiledPrompt.LaunchSurface("freshPrompt"); text != "" {
			return text
		}
		return i.CompiledPrompt.LaunchSurface("prompt")
	}
	return ""
}

// Empty reports whether this input has no usable content.
func (i RuntimeInput) Empty() bool {
	return strings.TrimSpace(i.PromptText()) == "" && len(i.Attachments) == 0
}

// PreparedRecoveryPoint carries recovery point metadata returned by a driver
// after a successful spawn that prepared a recovery point for the initial message.
type PreparedRecoveryPoint struct {
	MessageID   string
	ThreadID    string
	Strategy    string
	RecoveryRef string
}

// SessionResumeFallback describes one authoritative provider-session resume
// rejection for which the driver created exactly one fresh session. It carries
// no provider/session identifiers; the Process layer turns it into bounded,
// structured runtime diagnostics.
type SessionResumeFallback struct {
	Reason    string
	Operation string
}

// SpawnResult contains the started command and its relevant pipes.
type SpawnResult struct {
	Cmd            *exec.Cmd
	Stdin          io.WriteCloser
	Stdout         io.ReadCloser
	StderrTail     func() string
	Wait           func() error
	Terminate      func()
	SessionID      string
	SessionResumed bool
	// FreshSessionFallback is set at most once when runtime authority rejected
	// the requested provider session and the driver already cut over to one
	// fresh session. It must never be used for ambiguous transport outcomes.
	FreshSessionFallback  *SessionResumeFallback
	PreparedRecoveryPoint *PreparedRecoveryPoint
	Cleanup               func()
}

// Driver is the runtime-specific adapter for interactive agent execution.
type Driver interface {
	Name() string
	SupportsStdinNotification() bool
	MCPToolPrefix() string
	Spawn(ctx context.Context, cfg LaunchConfig) (SpawnResult, error)
	ParseLine(line string) []ParsedEvent
	EncodeStdinMessage(text, sessionID string) []byte
}

// RecoveryDriver is an optional runtime capability for preparing and restoring
// rewind recovery points without relying on the one-shot CLI path.
type RecoveryDriver interface {
	PrepareRecoveryPoint() (strategy string, recoveryRef string, supported bool, err error)
	RestoreRecoveryPoint(ctx context.Context, strategy string, recoveryRef string) error
}

// MessageBoundRecoveryDriver can prepare a runtime recovery point that must be
// bound to the exact AgentTeam message being delivered. Most runtimes can prepare a
// recovery point from their current state, but some native rewind APIs (for
// example OpenCode's session revert) restore by the runtime-side user message
// id. Those drivers must derive and later reuse the same runtime message id for
// the delivery corresponding to messageID.
type MessageBoundRecoveryDriver interface {
	PrepareRecoveryPointForMessage(messageID string) (strategy string, recoveryRef string, supported bool, err error)
}

// DetachedRecoveryDriver can restore a recovery point without an already-running
// agent process. The driver is responsible for any runtime-specific bootstrap.
type DetachedRecoveryDriver interface {
	RestoreRecoveryPointDetached(ctx context.Context, cfg LaunchConfig, strategy string, recoveryRef string) error
}

// BusyStdinDeliveryDriver reports whether the current active turn can accept
// real routed-message input as runtime-visible delivery instead of falling back
// to the post-turn continuation path. Transport-only nudges or refresh hints do
// not satisfy this capability.
type BusyStdinDeliveryDriver interface {
	SupportsBusyStdinDelivery() bool
}

// BusyStdinMessageDriver can accept a compiler-produced routed message through
// a runtime-specific delivery API. Semantic text never crosses this boundary
// as a caller-supplied string.
type BusyStdinMessageDriver interface {
	DeliverBusyStdinInput(ctx context.Context, input RuntimeInput, sessionID string) error
}

// RuntimeInputDeliveryDriver can accept a structured runtime input instead of a
// text-only stdin delivery. Drivers that do not implement it keep the text-only
// path.
type RuntimeInputDeliveryDriver interface {
	DeliverRuntimeInput(ctx context.Context, input RuntimeInput, sessionID string) error
}

// CorrelatedRuntimeInputDeliveryDriver reports that DeliverRuntimeInput uses
// RuntimeInput.ID and later emits ParsedEventKindRuntimeInputAcked for the
// same id. This capability is independent from read-only reconciliation: a
// runtime may provide a positive streamed acknowledgement without having any
// authoritative operation that can prove an input absent after a crash.
type CorrelatedRuntimeInputDeliveryDriver interface {
	SupportsCorrelatedRuntimeInputDelivery() bool
}

// AckedBusyRuntimeInputDriver can enqueue a correlated runtime input while the
// process is busy and later emits a ParsedEventKindRuntimeInputAcked event
// with the same RuntimeInput.ID when the runtime acknowledges responsibility
// for applying the body at its supported input boundary.
// SupportsAckedBusyRuntimeInputDelivery reports structural driver capability,
// not momentary turn readiness; implementations reconcile transient runtime
// state inside DeliverAckedBusyRuntimeInput. The process layer records transport
// success as pending-only and keeps freshness observed frontier unchanged until
// that acknowledgement.
type AckedBusyRuntimeInputDriver interface {
	SupportsAckedBusyRuntimeInputDelivery() bool
	DeliverAckedBusyRuntimeInput(ctx context.Context, input RuntimeInput, sessionID string) error
}

// ExplicitTurnLifecycleDriver reports that root liveness is authoritative only
// at explicit turn-start/turn-end boundaries. Activity events from such a
// runtime remain visible but must not independently make an idle Process busy.
// This is intentionally optional so Claude/OpenCode keep their existing
// activity-driven fallback semantics.
type ExplicitTurnLifecycleDriver interface {
	UsesExplicitTurnLifecycle() bool
}

// InterruptingRuntimeInputDriver can explicitly interrupt the current runtime
// turn/run and then deliver the supplied runtime-visible input as the next
// authoritative request without tearing down the whole process.
type InterruptingRuntimeInputDriver interface {
	DeliverInterruptingRuntimeInput(ctx context.Context, input RuntimeInput, sessionID string) error
}

// ActiveRunInterruptDriver can explicitly interrupt the current runtime
// turn/run without delivering a replacement input or tearing down the process.
type ActiveRunInterruptDriver interface {
	InterruptActiveRun(ctx context.Context, sessionID string) error
}

// ActiveTurnIDProvider exposes the runtime's current active turn identity when
// the runtime delivery model tracks live deliveries against a concrete in-progress
// turn. Delivery compilation uses this to bind prompt_input activity rows to
// the exact runtime turn they were compiled for.
type ActiveTurnIDProvider interface {
	CurrentActiveTurnID() string
}

// IdleCompactionDriver can start runtime-owned context compaction only after
// confirming there is no queued continuation message to flush.
type IdleCompactionDriver interface {
	StartIdleCompaction(ctx context.Context, sessionID string) (bool, error)
}

// ProactiveIdleCompactionDriver starts runtime-owned context compaction
// proactively at an idle boundary from the runtime's own context-usage signal,
// before any context-window exhaustion occurs. It runs between turns: the
// runtime context is compacted in place and the next user turn continues
// naturally, with no continuation retry injected. A driver returns started=true
// with a non-nil error when the mutation outcome is ambiguous, so Process keeps
// the session non-idle until an authoritative boundary reconciles it. Contrast
// ContextCompactionDriver, which is tied to an overflow event and retries the
// interrupted work.
type ProactiveIdleCompactionDriver interface {
	StartProactiveIdleCompaction(ctx context.Context, sessionID string) (bool, error)
}

// ContextCompactionDriver lets runtimes with native/session-local compaction
// queue a compaction specifically for a context-window exhaustion event. The
// Process layer starts the queued compaction at the next safe idle boundary and
// then injects a bounded continuation prompt to retry the interrupted work.
type ContextCompactionDriver interface {
	QueueContextCompaction(ev ParsedEvent) bool
}

// ContextWindowExceededDriver can detect whether a ParsedEvent indicates that
// the LLM context window has been exceeded, requiring recovery (compaction or
// session restart). Drivers that have built-in context management should not
// implement this interface.
type ContextWindowExceededDriver interface {
	IsContextWindowExceeded(ev ParsedEvent) bool
}

// OutputTruncationDriver detects terminal events where the model exhausted its
// output-token budget while the current session context remains resumable.
// Process uses this signal to continue in the same session instead of
// discarding context through context-window recovery.
type OutputTruncationDriver interface {
	IsOutputTruncated(ev ParsedEvent) bool
}
