// This file implements the /btw command (mirrors Claude Code's /btw and the pi
// agent extension @narumitw/pi-btw): a throwaway "side thread" for asking the
// model a quick side question that must NOT pollute the main conversation.
//
// /btw is intercepted in the REPL loop rather than routed through a slash Action
// closure because it must run an agent stream and read the live main context —
// none of which a pure string→string Action can do, exactly like /compact and
// /goal. It reaches the session's collaborators and mutable state through the
// cli.Host contract and reads follow-up lines through cli.Editor, so it need not
// import the concrete replDeps aggregate that assembles them.
//
// Isolation contract (the whole point of the feature): a side thread runs on a
// COPY of the main conversation as background, and its question/answer are only
// ever appended to that copy — never to host.AgentCtx().Messages. Nothing is
// persisted: no store.Save, no change to the persisted cursor / current leaf /
// header timestamp. Closing the side thread, switching sessions or restarting
// pigo discards everything.
//
// Scope: /btw is intercepted in the REPL loop and runs a side question against a
// copy of the main context (#279); it supports multi-turn follow-ups in the same
// ephemeral thread (#280), bare-/btw reopen of the most recent side thread this
// process (#281), and an optional model/thinking override config (#282, see
// btw_config.go) that affects only the side thread.
package btw

import (
	"context"
	"errors"
	"fmt"
	"io"
	"strings"

	"github.com/smallnest/pigo/internal/agentcore"
	"github.com/smallnest/pigo/internal/agenttool"
	"github.com/smallnest/pigo/internal/cli"
	"github.com/smallnest/pigo/internal/cli/run"
	"github.com/smallnest/pigo/internal/cli/ui"
	"github.com/smallnest/pigo/internal/compaction"
	"github.com/smallnest/pigo/internal/hooks"
	"github.com/smallnest/pigo/internal/provider"
	"github.com/smallnest/pigo/internal/runtime"
	"github.com/smallnest/pigo/internal/trust"
)

// btwHeader is the fixed banner shown when entering a side thread, so the user
// always knows the current input is a throwaway side question, not the main
// conversation (mirrors pi-btw's "btw · side thread" header).
const btwHeader = "btw · side thread"

// BtwHeader exposes the side-thread banner text so callers (and tests) can
// recognize it in output.
const BtwHeader = btwHeader

// btwPrompt is the input prompt shown for follow-up questions inside a side
// thread, distinguishing it from the main "pigo(model)>" prompt.
const btwPrompt = "btw> "

// RunBtw handles a /btw invocation. With an argument it starts a fresh side
// thread, asks that question, then enters a follow-up loop so the user can keep
// asking in the same ephemeral thread. Bare "/btw" reopens the most recent side
// thread from this process — replaying its Q&A history — and drops back into the
// follow-up loop; if none exists yet it guides the user to supply a question
// (US-004, #281). setCancel publishes the active run's cancel func so the REPL's
// SIGINT handler can interrupt the side run, reusing the same plumbing as a
// normal turn.
//
// The main context is never mutated: RunBtw builds a private side AgentContext
// seeded with a copy of the main messages, runs every turn against that copy,
// and returns without touching host.AgentCtx() or persisting anything. The side
// thread is retained in-process (host.LastBtw()) so a later bare /btw can reopen
// it, but it is never written to disk — restarting pigo discards it.
func RunBtw(setCancel func(context.CancelFunc), out io.Writer, host cli.Host, editor cli.Editor, line string) {
	question := strings.TrimSpace(strings.TrimPrefix(line, "/btw"))
	// Resolve the side thread's model/thinking once per invocation from the
	// session defaults overlaid with btw.json (#282). Re-read each call so an
	// edit takes effect next time with no restart.
	settings := ResolveBtwSettings(out, host)
	if question == "" {
		// Bare /btw: reopen the most recent side thread if one exists this process,
		// replaying its history; otherwise guide the user to supply a question.
		if host.LastBtw() == nil {
			fmt.Fprintln(out, "usage: /btw <question> — ask a quick side question without touching the main conversation")
			return
		}
		printBtwHeader(out)
		replaySideHistory(out, host.LastBtw(), host.LastBtwBase())
		if editor != nil {
			btwFollowUpLoop(setCancel, out, host, editor, host.LastBtw(), settings)
		}
		return
	}

	side := NewSideContext(host.AgentCtx())
	// Remember this thread so a later bare /btw can reopen it. LastBtwBase marks
	// where the copied background ends and the side Q&A begins, so a reopen only
	// replays the side turns, not the whole main transcript.
	host.SetLastBtw(side)
	host.SetLastBtwBase(len(side.Messages))
	printBtwHeader(out)
	AskSide(setCancel, out, host, side, settings, question)
	// Follow-up loop: keep answering in the same ephemeral thread until the user
	// exits. A nil editor (direct test callers that only ask one question) skips
	// the loop entirely, so a single /btw asks exactly one question and returns.
	if editor != nil {
		btwFollowUpLoop(setCancel, out, host, editor, side, settings)
	}
}

// replaySideHistory prints the side thread's own Q&A (everything after the
// copied main-conversation background at index base) when a bare /btw reopens a
// prior thread, so the user can browse earlier answers before continuing. Only
// user questions and assistant text are shown; tool activity is omitted to keep
// the recap compact.
func replaySideHistory(out io.Writer, side *agentcore.AgentContext, base int) {
	if base > len(side.Messages) {
		base = len(side.Messages)
	}
	for _, msg := range side.Messages[base:] {
		switch m := msg.(type) {
		case agentcore.UserMessage:
			fmt.Fprintf(out, "%s %s\n", ui.Colorize(ui.Enabled(), ui.Dim, "you:"), agentcore.ContentToText(m.Content))
		case agentcore.AssistantMessage:
			if text := agentcore.ContentToText(m.Content); text != "" {
				rendered := ui.RenderMarkdown(text)
				fmt.Fprint(out, rendered)
				if !strings.HasSuffix(rendered, "\n") {
					fmt.Fprintln(out)
				}
			}
		}
	}
}

// btwFollowUpLoop reads follow-up questions and answers them in the same side
// context, so each answer sees the prior side Q&A (FR-4). It exits on /exit,
// /quit, EOF, or an idle Ctrl+C (errLineInterrupted) — the same exit affordances
// as the main REPL, but confined to the side thread (FR-5). A blank line is
// ignored (stays in the thread). Nothing here touches the main context.
func btwFollowUpLoop(setCancel func(context.CancelFunc), out io.Writer, host cli.Host, editor cli.Editor, side *agentcore.AgentContext, settings BtwRunSettings) {
	for {
		raw, err := editor.ReadLine(btwPrompt)
		if errors.Is(err, cli.ErrLineInterrupted) {
			// Idle Ctrl+C at the side prompt leaves the thread (a Ctrl+C during a
			// run is handled inside askSide via the SIGINT cancel plumbing).
			fmt.Fprintln(out, "left side thread")
			return
		}
		q := strings.TrimSpace(raw)
		if err != nil && q == "" {
			// EOF or read error with no partial line: leave the thread.
			fmt.Fprintln(out, "left side thread")
			return
		}
		if q == "/exit" || q == "/quit" {
			fmt.Fprintln(out, "left side thread")
			return
		}
		if q == "" {
			continue
		}
		AskSide(setCancel, out, host, side, settings, q)
	}
}

// NewSideContext builds the side thread's private AgentContext. Its Messages are
// a fresh slice seeded with a shallow COPY of the main messages (the elements
// are immutable value/interface messages, so a copied slice header is enough to
// guarantee appends to the side thread never reach the main context's Messages).
// The system prompt and tools are shared by value; only Messages diverges.
func NewSideContext(main *agentcore.AgentContext) *agentcore.AgentContext {
	msgs := make(agentcore.MessageList, len(main.Messages))
	copy(msgs, main.Messages)
	return &agentcore.AgentContext{
		SystemPrompt: main.SystemPrompt,
		Messages:     msgs,
		Tools:        main.Tools,
	}
}

// printBtwHeader prints the side-thread banner.
func printBtwHeader(out io.Writer) {
	fmt.Fprintln(out, ui.Colorize(ui.Enabled(), ui.Dim, btwHeader))
}

// AskSide appends the question to the side context and streams one answer,
// mirroring streamRun's rendering but targeting the side context so nothing is
// written back to the main conversation or to disk. It reuses the REPL's SIGINT
// cancel plumbing via setCancel. The model/provider/thinking come from settings
// (session defaults overlaid with btw.json, #282), never from host.Live(), so a
// /btw override cannot leak into the main session.
func AskSide(setCancel func(context.CancelFunc), out io.Writer, host cli.Host, side *agentcore.AgentContext, settings BtwRunSettings, question string) {
	content, err := ui.BuildUserContent(question)
	if err != nil {
		fmt.Fprintf(out, "pigo: %v\n", err)
		return
	}
	side.Messages = append(side.Messages, agentcore.UserMessage{
		RoleField: agentcore.RoleUser,
		Content:   content,
	})

	runCtx, cancel := context.WithCancel(context.Background())
	setCancel(cancel)
	defer func() {
		cancel()
		setCancel(nil)
	}()

	// Show a transient status while the model works (FR-9). It is printed on its
	// own line; the streamed answer follows below it.
	fmt.Fprintln(out, ui.Colorize(ui.Enabled(), ui.Dim, "Answering…"))

	cfg := runtime.RunConfig{
		LoopConfig: runtime.LoopConfig{
			Model:         settings.Model,
			Provider:      settings.ProviderName,
			ThinkingLevel: settings.ThinkingLevel,
			Stream:        provider.StreamFnFromProvider(settings.Provider),
			GetAPIKey:     host.Creds().GetAPIKey,
			ContextWindow: host.Live().ContextWindow,
			Compaction:    compaction.DefaultCompactionSettings,
		},
		Batch: agenttool.BatchConfig{
			ToolExecutorConfig: agenttool.ToolExecutorConfig{
				Registry:       host.Registry(),
				BeforeToolCall: trust.BeforeToolCall(host.Trust(), host.Cwd(), host.Input(), out, host.ConfirmMu()),
			},
		},
		Reminders: host.Reminders(),
	}
	// Wire the per-turn hook seams onto the side run's cfg; nil dispatcher is a
	// no-op (FR-18).
	if d := host.Dispatcher(); d != nil {
		run.InstallSeams(&cfg, d, host.HookDeps())
	}
	stream := runtime.StartRun(runCtx, side, cfg)
	drainSideStream(runCtx, out, host, stream)
}

// chainBtwEvent returns the OnEvent observer for a /btw side run: the plugin
// notifier, with the SessionEnd/PreCompact hook notifier chained after it when
// hooks are configured, mirroring the REPL's OnEvent composition.
func chainBtwEvent(host cli.Host) func(agentcore.AgentEvent) {
	notifier := host.NotifierHandle()
	d := host.Dispatcher()
	if d == nil {
		return notifier
	}
	deps := host.HookDeps()
	hookEvent := hooks.NewHookNotifier(d, deps.SessionID, deps.ProjectDir).Handle
	if notifier == nil {
		return hookEvent
	}
	return func(ev agentcore.AgentEvent) {
		notifier(ev)
		hookEvent(ev)
	}
}

// drainSideStream prints the streamed assistant text and tool activity of a side
// run, mirroring streamRun/drainGoalStream. It blocks until the run ends. Unlike
// the main loop it persists nothing.
func drainSideStream(ctx context.Context, out io.Writer, host cli.Host, stream *runtime.LoopEventStream) {
	var reply strings.Builder
	flushReply := func() {
		if reply.Len() == 0 {
			return
		}
		rendered := ui.RenderMarkdown(reply.String())
		fmt.Fprint(out, rendered)
		if !strings.HasSuffix(rendered, "\n") {
			fmt.Fprintln(out)
		}
		reply.Reset()
	}
	_, err := runtime.DrainStream(ctx, stream, runtime.StreamHandler{
		OnEvent: chainBtwEvent(host),
		OnText: func(delta string) {
			reply.WriteString(delta)
		},
		OnTurnEnd: func(msg agentcore.AssistantMessage, results []agentcore.ToolResultMessage) {
			flushReply()
			for _, c := range msg.ToolCalls() {
				fmt.Fprintf(out, "  %s %s\n", ui.Colorize(ui.Enabled(), ui.Green, "→ tool:"), ui.ToolCallLabel(c))
			}
			for _, tr := range results {
				ui.RenderToolResult(out, tr)
			}
		},
	})
	flushReply()
	if err != nil {
		if ctx.Err() != nil {
			// A Ctrl+C during the run cancels just this answer; the follow-up loop
			// then returns to the btw prompt so the user can ask again or exit with
			// another Ctrl+C (FR-5).
			fmt.Fprintln(out, "^C interrupted — answer cancelled")
		} else {
			fmt.Fprintf(out, "error: %v\n", err)
		}
	}
}
