// This file wires the line-based REPL (US-003) and session persistence
// (US-024, #43) into the pigo command. When invoked without a prompt on a
// terminal, pigo starts the REPL loop (see repl.go); each run's messages are
// persisted to a local JSONL session so the conversation can be listed, resumed
// and replayed later.
package repl

import (
	"bufio"
	"fmt"
	"os"
	"path/filepath"
	"sync"
	"time"

	"github.com/smallnest/pigo/internal/agentcore"
	"github.com/smallnest/pigo/internal/agenttool"
	"github.com/smallnest/pigo/internal/cli"
	"github.com/smallnest/pigo/internal/cli/headless"
	"github.com/smallnest/pigo/internal/cli/prompts"
	"github.com/smallnest/pigo/internal/cli/run"
	"github.com/smallnest/pigo/internal/dream"
	"github.com/smallnest/pigo/internal/plugin"
	"github.com/smallnest/pigo/internal/provider"
	"github.com/smallnest/pigo/internal/runtime"
	"github.com/smallnest/pigo/internal/session"
	"github.com/smallnest/pigo/internal/trust"
)

// sessionStore returns the session store for the interactive REPL. It is a thin
// alias for headless.SessionStore so the REPL and headless runs share one store
// rooted at ~/.pigo/sessions (or PIGO_HOME).
func sessionStore() (*session.Store, error) {
	return headless.SessionStore()
}

// Options carries the resolved run configuration plus optional
// resume state into Run.
type Options struct {
	Model        string
	ProviderName string
	Provider     provider.Provider
	BaseURL      string
	APIKey       string
	Protocol     string
	// ThinkingLevel is the resolved reasoning-effort level (US-023): it seeds the
	// live run config so every REPL turn requests it, until a control command
	// changes it.
	ThinkingLevel agentcore.ThinkingLevel
	Tools         []agentcore.AgentTool
	SysPrompt     string

	// ResumeID, when non-empty, resumes an existing session: its messages seed
	// the context and replayed transcript. Otherwise a fresh session is created.
	ResumeID string

	// Approve, when true, grants the launch directory session trust before the
	// run so the first-launch trust prompt is skipped and side-effect tools run
	// without per-call confirmation (mirrors pi's --approve/-a).
	Approve bool
	// Skills is the pre-loaded skill set (loaded once by setupAgentEnv, shared
	// with prompt injection). Each is registered as a /skill-name command. Empty
	// under --no-skills, so nothing is registered.
	Skills []*runtime.Skill

	// Plugins holds the loaded plugin manager so the REPL can deliver lifecycle
	// events to subscribed plugins (US-017, #133). It may be nil (no plugins).
	Plugins *plugin.Manager

	// ConfigPrompts holds prompt-template paths from the config.toml `prompts`
	// array (settings tier); each is a file or dir loaded non-recursively.
	ConfigPrompts []string
	// CliPrompts holds --prompt-template paths (CLI tier, repeatable).
	CliPrompts []string
	// NoPromptTemplates disables all prompt-template discovery (global, project,
	// settings, CLI); built-in slash commands are unaffected. Independent of
	// --no-skills.
	NoPromptTemplates bool

	// Dream is the resolved [dream] configuration (US-008). Run uses it to decide
	// whether to launch the startup background consolidation; a zero value
	// (Enabled false) disables the auto-trigger entirely.
	Dream dream.Config
}

// Run starts the line-based REPL over a persisted session. It keeps
// a single growing AgentContext across prompts (so turns share history) and
// saves the session's messages after each run completes (see runREPL/streamRun
// in repl.go).
func Run(opts Options) error {
	creds := provider.NewCredentialStore(nil)
	creds.SetOverride(opts.ProviderName, opts.APIKey)
	reg := run.ToolRegistry(opts.Tools)

	store, err := sessionStore()
	if err != nil {
		return err
	}

	// Resolve the launch directory once (pigo does not cd during a session). It is
	// the trust key, the directory side-effect tools are gated against, and — new
	// for #526 — the value stamped onto a fresh SessionHeader.Cwd so the session is
	// attributed to a project and a later /dream pass can distill it under the
	// right scope (mirrors headless.headlessCwd). An unresolvable cwd yields ""
	// (the session stays unattributed) rather than aborting the session.
	cwd, cwdErr := os.Getwd()

	// Establish the session: resume an existing one or create a fresh header.
	now := time.Now().UTC()
	var (
		agentCtx *agentcore.AgentContext
		header   session.SessionHeader
		history  []agentcore.AgentMessage
		curLeaf  string // active leaf id on resume; "" for a fresh session
	)
	if opts.ResumeID != "" {
		// Interactive resume always appends a fresh user message before running,
		// so a session that ended normally (trailing assistant reply) is resumable
		// here. Load the raw session and rebuild the context directly.
		h, entries, err := store.LoadEntries(opts.ResumeID)
		if err != nil {
			return err
		}
		msgs := make(agentcore.MessageList, len(entries))
		for i, e := range entries {
			msgs[i] = e.Message
		}
		if len(entries) > 0 {
			curLeaf = entries[len(entries)-1].ID
		}
		header = h
		agentCtx = &agentcore.AgentContext{SystemPrompt: h.SystemPrompt, Messages: msgs, Tools: opts.Tools}
		history = msgs
		if agentCtx.SystemPrompt == "" {
			agentCtx.SystemPrompt = opts.SysPrompt
		}
	} else {
		agentCtx = &agentcore.AgentContext{SystemPrompt: opts.SysPrompt, Tools: opts.Tools}
		header = session.SessionHeader{
			ID:           session.NewID(now),
			CreatedAt:    now,
			UpdatedAt:    now,
			Model:        opts.Model,
			Provider:     opts.ProviderName,
			SystemPrompt: opts.SysPrompt,
			Cwd:          cwd,
		}
	}

	// live holds the run configuration that a control command (e.g. /model) may
	// mutate mid-session. streamRun reads it on each prompt so a model switch
	// takes effect on the next turn; header is updated so the switch is persisted
	// with the session.
	live := &cli.LiveConfig{
		Model:         opts.Model,
		ProviderName:  opts.ProviderName,
		Provider:      opts.Provider,
		BaseURL:       opts.BaseURL,
		Protocol:      opts.Protocol,
		ThinkingLevel: opts.ThinkingLevel,
		ContextWindow: cli.DefaultContextWindow,
	}

	// Project trust (US-018, #134): load the persisted trust store for the
	// launch directory. A load failure (e.g. a corrupted trust.json) is
	// non-fatal: trust is disabled (mgr stays nil) and the REPL still runs -
	// the store is surfaced rather than silently overwritten. cwd is captured
	// once above since pigo does not cd during a session; if it cannot be resolved
	// trust is disabled too, since an empty cwd would silently never match.
	mgr, mgrErr := trust.NewManager(trust.DefaultPath())
	if mgrErr != nil {
		fmt.Fprintf(os.Stderr, "pigo: trust store unavailable, trust disabled: %v\n", mgrErr)
		mgr = nil
	}
	if cwdErr != nil && mgr != nil {
		fmt.Fprintf(os.Stderr, "pigo: cannot resolve working directory, trust disabled: %v\n", cwdErr)
		mgr = nil
	}
	// in is the shared input reader for the main loop and the tool-call
	// confirmation prompt (see repl.go). Wrapping os.Stdin once here means both
	// read from the same buffer.
	reader := bufio.NewReaderSize(os.Stdin, replScanBufInit)

	// Wire slash-commands: built-ins (compile-time) plus any user templates under
	// ~/.pigo/commands (mirrors the commands/*.md convention) plus skills under
	// ~/.agents/skills. A load error is non-fatal — the REPL still runs with the
	// built-ins. Instance built-ins that need live state (/model, /help) are
	// registered against `live`.
	slash, err := prompts.BuildSlashRegistry(live, opts.Skills, opts.Plugins, prompts.PromptTemplateSources{
		Settings:       opts.ConfigPrompts,
		CLI:            opts.CliPrompts,
		Disable:        opts.NoPromptTemplates,
		ProjectDir:     filepath.Join(cwd, ".pigo", "prompts"),
		ProjectTrusted: mgr != nil && mgr.IsTrusted(cwd),
	})
	if err != nil {
		fmt.Fprintf(os.Stderr, "pigo: slash-commands: %v\n", err)
	}
	trust.RegisterCommand(slash, mgr, cwd)

	// --approve grants the launch directory session trust up front (mirrors pi's
	// --approve/-a), so the first-launch prompt is skipped and side-effect tools
	// run without per-call confirmation. Otherwise, on the first launch in an
	// undecided directory, ask the user how much to trust it before any tool
	// runs. This happens before replay so the trust question is the first thing
	// the user sees, not their prior history.
	trust.EstablishTrust(os.Stdout, reader, mgr, cwd, opts.Approve)

	// Replay the resumed conversation so the user sees history before re-prompting.
	if len(history) > 0 {
		replayTranscript(os.Stdout, history)
	}

	// Startup background consolidation (US-008, FR-4/FR-17): if dream is enabled
	// and due, spawn `pigo --dream` in a goroutine now so it runs while the user
	// works — it never blocks the first prompt, and prints a one-line notice on
	// completion. The dream state/lock live under dream.ResolveMemoryRoot (the
	// same root the subprocess consolidates), independent of whether the memory
	// tool is wired into this session. Not-due / disabled is a cheap no-op.
	maybeStartBackgroundDream(os.Stdout, dream.ResolveMemoryRoot(), cwd, opts.Dream)

	return runREPL(os.Stdin, os.Stdout, replDeps{
		store:     store,
		header:    header,
		agentCtx:  agentCtx,
		live:      live,
		reg:       reg,
		reminders: run.TodoReminders(opts.Tools),
		slash:     slash,
		creds:     creds,
		trust:     mgr,
		cwd:       cwd,
		in:        reader,
		confirmMu: &sync.Mutex{},
		curLeaf:   curLeaf,
		persisted: len(history),
		memoryRoot: run.MemoryRootFromTools(opts.Tools),
		memstore:   run.MemoryStoreFromTools(opts.Tools),
		snap:       run.SnapshotRecorderFromTools(opts.Tools),
		jobs:       run.BashJobStoreFromTools(opts.Tools),
		notifier:  plugin.NewEventNotifier(opts.Plugins, os.Stderr),
		goal:      agenttool.NewGoalState(),
		telemetry: cli.NewTelemetryHolder(),
	})
}

// formatHelpLine renders one slash-command line for /help as
// "/name <argument-hint> - description (source: <tier>)", omitting the hint
// segment when absent. It is the plain, testable form of the /help line; the
// /help Action applies color on top of the same structure.
func formatHelpLine(c runtime.SlashCommand) string {
	s := "/" + c.Name
	if c.ArgumentHint != "" {
		s += " " + c.ArgumentHint
	}
	if c.Description != "" {
		s += " - " + c.Description
	}
	s += " (source: " + c.Tier.String() + ")"
	return s
}
