// This file implements the headless / stdio run modes (US-020, FR-18): a
// non-interactive driver that runs the agent loop over a single prompt for
// scripting and CI. Two output modes are supported, mirroring pi's print-mode
// and rpc/stream-json protocols:
//
//   - PrintMode: run the loop to completion and write only the final assistant
//     text to the output (the "-p / --print" mode).
//   - StreamJSONMode: serialize every AgentEvent as a line-delimited JSON object
//     as it is emitted (the "--output-format stream-json" mode), so a parent
//     process can consume the run incrementally.
//
// The run's success/failure is reported as a returned error so the CLI can map
// it to a process exit code: a run whose final assistant message carries
// stopReason error/aborted, or whose stream result errors, is a failure.
package runtime

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"os"
	"strings"

	"github.com/smallnest/pigo/internal/agentcore"
)

// HeadlessMode selects how a headless run reports its progress and result.
type HeadlessMode int

const (
	// PrintMode runs the loop to completion and writes only the final assistant
	// text to the output writer.
	PrintMode HeadlessMode = iota
	// StreamJSONMode writes each AgentEvent as a line-delimited JSON object as it
	// is emitted.
	StreamJSONMode
)

// HeadlessConfig configures a headless run.
type HeadlessConfig struct {
	// Run is the loop configuration (provider stream, tools, hooks).
	Run RunConfig
	// Mode selects print vs stream-json output. Defaults to PrintMode.
	Mode HeadlessMode
	// Out receives the run output (final text or JSON lines). Required.
	Out io.Writer
	// OnEvent, when non-nil, is invoked for every AgentEvent before output
	// handling. It is the seam plugin lifecycle-event delivery (US-017, #133)
	// hooks into, independent of the output mode. It must not block.
	OnEvent func(ev agentcore.AgentEvent)
	// Progress receives human-readable sub-agent progress lines
	// (SubAgentProgressEvent). Defaults to os.Stderr when nil. Progress is
	// stderr-only by contract: it is never serialised onto Out (stdout), so it
	// cannot pollute the final result text or the stream-json envelope stream.
	Progress io.Writer
}

// ErrRunFailed is the sentinel returned by RunHeadless when the agent run ended
// in a failure state (stopReason error/aborted). The CLI maps a non-nil error
// to a non-zero exit code.
type ErrRunFailed struct {
	// Reason is the stopReason (or message) that marked the run as failed.
	Reason string
}

func (e *ErrRunFailed) Error() string {
	if e.Reason == "" {
		return "agent run failed"
	}
	return "agent run failed: " + e.Reason
}

// RunHeadless runs the agent loop for the already-assembled agentCtx and drives
// output per cfg.Mode. It blocks until the run ends and returns nil on success
// or an error describing the failure (for exit-code mapping). It never returns
// before the stream is fully drained, so no goroutine is leaked.
func RunHeadless(ctx context.Context, agentCtx *agentcore.AgentContext, cfg HeadlessConfig) error {
	if cfg.Out == nil {
		return fmt.Errorf("headless: nil output writer")
	}
	stream := agentLoop(ctx, agentCtx, cfg.Run)

	// writeErr holds the first stream-json write failure. We keep draining after
	// it (DrainStream never returns early) so the loop's producer goroutine never
	// blocks on a synchronous Emit — honoring the no-leak contract on a broken
	// pipe. In stream-json mode every event is serialised; print mode only needs
	// the final message, which DrainStream returns.
	var writeErr error
	h := StreamHandler{}
	// Compose the output-mode serialiser (stream-json only) with the optional
	// external OnEvent (plugin lifecycle delivery, US-017). Both observe every
	// event; the serialiser runs first so a write failure is recorded even when a
	// plugin observer is also wired.
	//
	// SubAgentProgressEvent (D-9) is special-cased: it is written as a
	// human-readable line to the progress writer (stderr) and is deliberately
	// excluded from the stream-json stdout path so it never pollutes the result
	// output or the machine-readable envelope stream (progress is stderr-only).
	progress := cfg.Progress
	if progress == nil {
		progress = os.Stderr
	}
	streamJSON := cfg.Mode == StreamJSONMode
	h.OnEvent = func(ev agentcore.AgentEvent) {
		if pe, ok := ev.(agentcore.SubAgentProgressEvent); ok {
			writeProgressLine(progress, pe)
			if cfg.OnEvent != nil {
				cfg.OnEvent(ev)
			}
			return
		}
		if streamJSON && writeErr == nil {
			if err := writeEventJSON(cfg.Out, ev); err != nil {
				writeErr = err
			}
		}
		if cfg.OnEvent != nil {
			cfg.OnEvent(ev)
		}
	}
	lastAssistant, resErr := DrainStream(ctx, stream, h)
	if writeErr != nil {
		return writeErr
	}
	if resErr != nil {
		return resErr
	}

	if cfg.Mode == PrintMode {
		text := ""
		if lastAssistant != nil {
			text = agentcore.ContentToText(lastAssistant.Content)
		}
		if _, err := io.WriteString(cfg.Out, text); err != nil {
			return err
		}
		if text != "" && !strings.HasSuffix(text, "\n") {
			if _, err := io.WriteString(cfg.Out, "\n"); err != nil {
				return err
			}
		}
	}

	if lastAssistant != nil {
		switch lastAssistant.StopReason {
		case agentcore.StopReasonError:
			reason := lastAssistant.ErrorMessage
			if reason == "" {
				reason = "error"
			}
			return &ErrRunFailed{Reason: reason}
		case agentcore.StopReasonAborted:
			return &ErrRunFailed{Reason: "aborted"}
		}
	}
	return nil
}

// writeProgressLine renders one SubAgentProgressEvent as a human-readable line
// on w (stderr by contract). When the task supplied a description it is shown
// alongside the activity; otherwise the line degrades to the activity alone
// (Description MAY be empty, Activity never is). Write errors are ignored:
// progress is a non-critical, best-effort side channel.
func writeProgressLine(w io.Writer, ev agentcore.SubAgentProgressEvent) {
	if ev.Description != "" {
		fmt.Fprintf(w, "  ⏺ %s · %s\n", ev.Description, ev.Activity)
		return
	}
	fmt.Fprintf(w, "  ⏺ %s\n", ev.Activity)
}

// writeEventJSON serializes one AgentEvent as a single line of JSON, terminated
// by a newline, onto w. The envelope always carries a "type" discriminant so a
// consumer can dispatch without positional knowledge.
func writeEventJSON(w io.Writer, ev agentcore.AgentEvent) error {
	env := eventEnvelope(ev)
	b, err := json.Marshal(env)
	if err != nil {
		return fmt.Errorf("headless: marshal event: %w", err)
	}
	b = append(b, '\n')
	_, err = w.Write(b)
	return err
}

// eventEnvelope maps an AgentEvent onto a JSON-serializable object with a
// "type" discriminant plus the event's observable payload. Only fields that are
// safe and useful over the wire are included (assistant text, tool ids/names,
// stop reasons) — never secrets.
func eventEnvelope(ev agentcore.AgentEvent) map[string]any {
	env := map[string]any{"type": ev.EventType()}
	switch e := ev.(type) {
	case agentcore.AgentStartEvent:
		// The first event carries the backing session id (mirrors pi/Claude Code),
		// so a consumer can associate the run's output with a session and resume
		// it later. Omitted only when the run has no backing session (SessionID
		// unset), which the envelope treats as "not resumable".
		if e.SessionID != "" {
			env["sessionId"] = e.SessionID
		}
	case agentcore.AgentEndEvent:
		env["messageCount"] = len(e.Messages)
	case agentcore.TurnEndEvent:
		env["stopReason"] = e.Message.StopReason
		if text := agentcore.ContentToText(e.Message.Content); text != "" {
			env["text"] = text
		}
		if calls := e.Message.ToolCalls(); len(calls) > 0 {
			names := make([]string, len(calls))
			for i, c := range calls {
				names[i] = c.Name
			}
			env["toolCalls"] = names
		}
	case agentcore.MessageUpdateEvent:
		if a, ok := e.Message.(agentcore.AssistantMessage); ok {
			if text := agentcore.ContentToText(a.Content); text != "" {
				env["text"] = text
			}
		}
	case agentcore.ToolExecutionStartEvent:
		env["toolCallId"] = e.ToolCallID
		env["toolName"] = e.ToolName
	case agentcore.ToolExecutionEndEvent:
		env["toolCallId"] = e.ToolCallID
		env["toolName"] = e.ToolName
		env["isError"] = e.IsError
	case agentcore.CompactionStartEvent:
		env["reason"] = e.Reason
		env["tokensBefore"] = e.TokensBefore
	case agentcore.CompactionEvent:
		env["reason"] = e.Reason
		env["tokensBefore"] = e.TokensBefore
		env["tokensAfter"] = e.TokensAfter
		env["summarizedCount"] = e.SummarizedCount
		env["keptCount"] = e.KeptCount
		if e.ErrorMessage != "" {
			env["error"] = e.ErrorMessage
		}
	case agentcore.TelemetryEvent:
		// The run-end telemetry summary: structured metrics a script can read
		// directly from the stream-json output (observability -- structured telemetry collection). Per-tool
		// timings are flattened into a name→{count,totalMs} object so a JSON
		// consumer can index by tool name.
		env["turns"] = e.Turns
		env["truncationCount"] = e.TruncationCount
		env["compactionCount"] = e.CompactionCount
		env["contextUtilization"] = e.ContextUtilization
		env["contextTokens"] = e.ContextTokens
		env["contextWindow"] = e.ContextWindow
		tools := make(map[string]map[string]any, len(e.ToolDurationsMs))
		for name, t := range e.ToolDurationsMs {
			tools[name] = map[string]any{"count": t.Count, "totalMs": t.TotalMs}
		}
		env["toolDurationsMs"] = tools
	}
	return env
}
