package cli

import (
	"context"
	"flag"
	"fmt"
	"os"
	"os/signal"
	"path/filepath"
	"runtime"
	"strings"
	"time"

	"reasonix/internal/ablation"
	"reasonix/internal/acp"
	"reasonix/internal/boot"
	"reasonix/internal/config"
	"reasonix/internal/control"
	"reasonix/internal/extension/providerext"
	"reasonix/internal/i18n"
	"reasonix/internal/netclient"
	"reasonix/internal/provider"
	"reasonix/internal/sandbox"
	"reasonix/internal/tool"
	"reasonix/internal/tool/builtin"
)

// acpCommand runs Reasonix as an Agent Client Protocol agent: a stdio JSON-RPC
// server that editors and other host clients drive (initialize, session/new,
// session/prompt, session/cancel). It keeps v2 wire-compatible with the many
// tools that integrated with v1 over ACP.
//
// stdin/stdout are the JSON-RPC channel — nothing else may write to stdout, so
// all diagnostics go to stderr. Each session is assembled by acpFactory, rooted
// at the cwd the client opens.
func acpCommand(args []string, version string) int {
	args, deprecatedMode, err := consumeDeprecatedModeFlags(args, "profile")
	if err != nil {
		fmt.Fprintln(os.Stderr, i18n.M.ErrorPrefix, err)
		return 2
	}
	fs := flag.NewFlagSet("acp", flag.ContinueOnError)
	model := fs.String("model", "", "provider name (default: config default_model)")
	plannerFlag := fs.String("planner", "auto", "planner policy: auto | off")
	networkFlag := fs.String("sandbox-network", "auto", "sandbox network policy: auto | on | off")
	bashFlag := fs.String("sandbox-bash", "auto", "bash sandbox policy: auto | enforce")
	workspaceOnly := fs.Bool("workspace-only", false, "ignore configured extra write roots and confine writes to the session cwd")
	if code, ok := parseCommandFlags(fs, args); !ok {
		return code
	}
	plannerMode := strings.ToLower(strings.TrimSpace(*plannerFlag))
	if plannerMode != "auto" && plannerMode != "off" {
		fmt.Fprintln(os.Stderr, i18n.M.ErrorPrefix, "planner must be auto or off")
		return 2
	}
	networkMode := strings.ToLower(strings.TrimSpace(*networkFlag))
	var networkOverride *bool
	switch networkMode {
	case "auto":
	case "on":
		on := true
		networkOverride = &on
	case "off":
		off := false
		networkOverride = &off
	default:
		fmt.Fprintln(os.Stderr, i18n.M.ErrorPrefix, "sandbox-network must be auto, on, or off")
		return 2
	}
	bashMode := strings.ToLower(strings.TrimSpace(*bashFlag))
	if bashMode != "auto" && bashMode != "enforce" {
		fmt.Fprintln(os.Stderr, i18n.M.ErrorPrefix, "sandbox-bash must be auto or enforce")
		return 2
	}
	if err := acceptDeprecatedModeFlag(deprecatedMode); err != nil {
		fmt.Fprintln(os.Stderr, i18n.M.ErrorPrefix, err)
		return 2
	}

	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
	defer stop()

	factory := &acpFactory{
		model: *model, plannerOff: plannerMode == "off",
		networkOverride: networkOverride, workspaceOnly: *workspaceOnly,
		bashOverride: bashMode, requireSandbox: bashMode == "enforce",
	}
	info := acp.AgentInfo{Name: "reasonix", Version: version}
	if err := acp.Serve(ctx, os.Stdin, os.Stdout, factory, info); err != nil {
		fmt.Fprintln(os.Stderr, i18n.M.ErrorPrefix, err)
		return 1
	}
	return 0
}

// acpFactory builds one control.Controller per ACP session by reusing boot.Build
// with the session cwd as WorkspaceRoot. That keeps ACP aligned with chat,
// desktop, and serve assembly while still adding the host-supplied MCP servers
// for this session only.
type acpFactory struct {
	model            string
	plannerOff       bool
	networkOverride  *bool
	bashOverride     string
	workspaceOnly    bool
	requireSandbox   bool
	sandboxAvailable func() bool
}

func (f *acpFactory) SessionDir() string {
	return config.SessionDir()
}

// ablationSet maps the ACP --planner=off hard override onto the shared
// subsystem switch boot consults.
func (f *acpFactory) ablationSet() ablation.Set {
	if f.plannerOff {
		return ablation.New(ablation.Planner)
	}
	return ablation.Set{}
}

// NewSession assembles the per-session controller. Resources (MCP subprocesses)
// are released via the controller's Cleanup, run on ctrl.Close().
func (f *acpFactory) NewSession(ctx context.Context, p acp.SessionParams) (*control.Controller, error) {
	opts, err := f.sessionBootOptions(p)
	if err != nil {
		return nil, err
	}
	return boot.Build(ctx, opts)
}

// RebuildSession rebuilds the session controller with the same boot.Options.
func (f *acpFactory) RebuildSession(ctx context.Context, p acp.SessionParams, old *control.Controller) (*control.Controller, error) {
	opts, err := f.sessionBootOptions(p)
	if err != nil {
		return nil, err
	}
	res, err := boot.Rebuild(ctx, old, opts)
	if err != nil {
		return nil, err
	}
	// The stage-3a runtime set is always empty, so nothing leaks by returning
	// only the controller (see boot.Build's compatibility wrapper).
	return res.Controller, nil
}

// sessionBootOptions builds the boot.Options every ACP session controller —
// initial build or boot.Rebuild replacement — is assembled from.
func (f *acpFactory) sessionBootOptions(p acp.SessionParams) (boot.Options, error) {
	root := strings.TrimSpace(p.Cwd)
	if root == "" {
		if wd, err := os.Getwd(); err == nil {
			root = wd
		}
	}
	if root != "" && !filepath.IsAbs(root) {
		return boot.Options{}, fmt.Errorf("session cwd must be an absolute path: %s", root)
	}
	bashOverride := ""
	if f.bashOverride == "enforce" {
		bashOverride = "enforce"
	}
	return boot.Options{
		Model:                    firstNonEmpty(p.Model, f.model),
		RequireKey:               true,
		Sink:                     p.Sink,
		StatsSource:              "cli",
		EffortOverride:           p.EffortOverride,
		Stderr:                   os.Stderr,
		WorkspaceRoot:            root,
		ExtraPlugins:             p.MCPServers,
		CleanupPendingReconciler: acp.ReconcileCleanupPending,
		OnSessionRecovered:       p.OnSessionRecovered,
		OnSessionTransition:      p.OnSessionTransition,
		FileOverlay:              p.FileOverlay,
		TerminalRunner:           p.Terminal,
		Ablation:                 f.ablationSet(),
		SandboxNetworkOverride:   f.networkOverride,
		SandboxBashOverride:      bashOverride,
		WorkspaceOnly:            f.workspaceOnly,
	}, nil
}

func (f *acpFactory) SessionRuntimeState(_ context.Context, p acp.SessionRuntimeStateParams) (acp.SessionRuntimeState, error) {
	cfg, err := config.LoadForRoot(p.Cwd)
	if err != nil {
		return acp.SessionRuntimeState{}, err
	}
	plannerMode := effectiveACPPlannerMode(cfg, f.plannerOff, p.Model)
	writeRoots := cfg.WriteRootsForRoot(p.Cwd)
	if f.workspaceOnly {
		writeRoots = []string{p.Cwd}
	}
	networkEnabled := cfg.Sandbox.Network
	if f.networkOverride != nil {
		networkEnabled = *f.networkOverride
	}
	effectiveBash := cfg.BashMode()
	if f.bashOverride == "enforce" {
		effectiveBash = "enforce"
	}
	sandboxAvailable := true
	if effectiveBash == "enforce" {
		sandboxAvailable = f.isSandboxAvailable()
	} else {
		// Without an OS sandbox the shell is intentionally unconfined, including
		// network access; report the actual posture rather than the inert config bit.
		networkEnabled = true
	}
	if f.requireSandbox && !sandboxAvailable {
		return acp.SessionRuntimeState{}, fmt.Errorf("effective bash sandbox unavailable: %s", sandbox.UnavailableMessage())
	}
	return acp.SessionRuntimeState{
		PlannerMode: plannerMode,
		Sandbox: acp.SessionSandboxState{
			Mode:           effectiveBash,
			Engine:         acpSandboxEngine(effectiveBash),
			Available:      sandboxAvailable,
			WorkspaceRoot:  p.Cwd,
			WriteRoots:     writeRoots,
			NetworkEnabled: networkEnabled,
		},
	}, nil
}

func (f *acpFactory) isSandboxAvailable() bool {
	if f.sandboxAvailable != nil {
		return f.sandboxAvailable()
	}
	return sandbox.Available()
}

func effectiveACPPlannerMode(cfg *config.Config, disabled bool, model string) string {
	if cfg == nil || disabled {
		return "off"
	}
	plannerRef := strings.TrimSpace(cfg.Agent.PlannerModel)
	if plannerRef == "" {
		return "off"
	}
	planner, plannerOK := cfg.ResolveModel(plannerRef)
	executor, executorOK := cfg.ResolveModel(strings.TrimSpace(model))
	if !plannerOK || !executorOK || planner.Model == executor.Model {
		return "off"
	}
	return "on"
}

func acpSandboxEngine(mode string) string {
	if mode != "enforce" {
		return "none"
	}
	switch runtime.GOOS {
	case "darwin":
		return "seatbelt"
	case "linux":
		return "bubblewrap"
	default:
		return "none"
	}
}

func (f *acpFactory) SessionConfigState(_ context.Context, p acp.SessionConfigStateParams) (acp.SessionConfigState, error) {
	root := strings.TrimSpace(p.Cwd)
	if root == "" {
		if wd, err := os.Getwd(); err == nil {
			root = wd
		}
	}
	if root != "" && !filepath.IsAbs(root) {
		return acp.SessionConfigState{}, fmt.Errorf("session cwd must be an absolute path: %s", root)
	}
	_, _ = config.MigrateLegacyIfNeededForRoot(root)
	_, _ = config.MigrateMCPToUserConfigOnUpgrade([]string{root})
	cfg, err := config.LoadForRoot(root)
	if err != nil {
		return acp.SessionConfigState{}, err
	}

	// Session or factory model overrides are explicit; empty falls back (#6996).
	explicit := firstNonEmpty(p.Model, f.model)
	ref, _, err := resolveModelForCLI(explicit, cfg)
	if err != nil {
		return acp.SessionConfigState{}, err
	}
	if strings.TrimSpace(ref) == "" {
		return acp.SessionConfigState{}, fmt.Errorf("no default_model configured")
	}
	// Plugin-namespaced refs belong to extension sidecars: they never resolve
	// through the config catalog, so their configured/current handling keys off
	// the ref itself and boot's merged resolver is the gate.
	pluginRef := providerext.PluginRefOwner(ref) != ""
	entry, ok := cfg.ResolveModel(ref)
	if !ok && !pluginRef {
		return acp.SessionConfigState{}, fmt.Errorf("unknown model %q", ref)
	}
	if ok && !entry.Configured() {
		return acp.SessionConfigState{}, fmt.Errorf("model %q is not configured", ref)
	}
	currentModel := ref
	entryDescription := ""
	if ok {
		currentModel = entry.Name + "/" + entry.Model
		entryDescription = entry.Name
	}
	modelOptions, modelInfos := acpModelOptions(cfg)
	if !hasModelOption(modelOptions, currentModel) {
		modelOptions = append(modelOptions, acp.SessionConfigSelectOption{
			Value:       currentModel,
			Name:        currentModel,
			Description: entryDescription,
		})
		modelInfos = append(modelInfos, acp.ModelInfo{
			ModelID:     currentModel,
			Name:        currentModel,
			Description: entryDescription,
		})
	}

	effortEntry := config.ProviderEntry{}
	if ok {
		effortEntry = *entry
	}
	effortOverride := cloneStringPtr(p.EffortOverride)
	hadEffortOverride := effortOverride != nil
	if effortOverride != nil {
		if strings.TrimSpace(*effortOverride) == "" {
			effortEntry.Effort = ""
		} else {
			normalized, err := config.NormalizeEffort(&effortEntry, *effortOverride)
			if err != nil {
				effortEntry.Effort = ""
				cleared := ""
				effortOverride = &cleared
			} else {
				effortEntry.Effort = normalized
				effortOverride = &normalized
			}
		}
	}

	options := []acp.SessionConfigOption{{
		ID:           "model",
		Name:         "Model",
		Category:     "model",
		Type:         "select",
		CurrentValue: currentModel,
		Options:      modelOptions,
	}}
	if cap := config.EffortCapabilityForEntry(&effortEntry); cap.Supported {
		currentEffort := config.EffortDisplay(&effortEntry)
		if !containsString(cap.Levels, currentEffort) {
			currentEffort = "auto"
			auto := ""
			effortOverride = &auto
		}
		options = append(options, acp.SessionConfigOption{
			ID:           "effort",
			Name:         "Effort",
			Category:     "thought_level",
			Type:         "select",
			CurrentValue: currentEffort,
			Options:      acpEffortOptions(cap.Levels),
		})
	} else if hadEffortOverride {
		cleared := ""
		effortOverride = &cleared
	}
	// RuntimeProfile stays pinned for old status readers; mode options are unpublished.
	return acp.SessionConfigState{
		Model:          currentModel,
		EffortOverride: effortOverride,
		RuntimeProfile: "balanced",
		Models: &acp.SessionModelState{
			AvailableModels: modelInfos,
			CurrentModelID:  currentModel,
		},
		ConfigOptions: options,
	}, nil
}

func acpBuiltinTools(cfg *config.Config, cwd string, writeRoots []string) []tool.Tool {
	bashSpec := sandbox.Spec{Mode: cfg.BashMode(), WriteRoots: writeRoots, Network: cfg.Sandbox.Network}
	ws := builtin.Workspace{
		Dir:          cwd,
		WriteRoots:   writeRoots,
		Bash:         bashSpec,
		BashTimeout:  time.Duration(cfg.BashTimeoutSeconds()) * time.Second,
		Search:       builtin.ResolveSearch(cfg.Tools.Search.Engine, cfg.Tools.Search.RgPath, nil),
		ProxySpec:    cfg.NetworkProxySpec(),
		SessionGuard: builtin.NewSessionDataGuard(config.MemoryUserDir(), cfg.AllowWriteRoots()),
	}
	return ws.Tools(cfg.Tools.Enabled...)
}

func acpModelOptions(cfg *config.Config) ([]acp.SessionConfigSelectOption, []acp.ModelInfo) {
	if cfg == nil {
		return nil, nil
	}
	var options []acp.SessionConfigSelectOption
	var models []acp.ModelInfo
	for i := range cfg.Providers {
		p := &cfg.Providers[i]
		if !p.Configured() {
			continue
		}
		for _, model := range p.ChatModelList() {
			ref := p.Name + "/" + model
			options = append(options, acp.SessionConfigSelectOption{
				Value:       ref,
				Name:        ref,
				Description: p.Name,
			})
			models = append(models, acp.ModelInfo{
				ModelID:     ref,
				Name:        ref,
				Description: p.Name,
			})
		}
	}
	return options, models
}

func hasModelOption(options []acp.SessionConfigSelectOption, ref string) bool {
	for _, opt := range options {
		if opt.Value == ref {
			return true
		}
	}
	return false
}

func acpEffortOptions(levels []string) []acp.SessionConfigSelectOption {
	out := make([]acp.SessionConfigSelectOption, 0, len(levels))
	for _, level := range levels {
		out = append(out, acp.SessionConfigSelectOption{Value: level, Name: effortOptionName(level)})
	}
	return out
}

func effortOptionName(level string) string {
	if level == "" {
		return ""
	}
	if level == "xhigh" {
		return "XHigh"
	}
	return strings.ToUpper(level[:1]) + level[1:]
}

func firstNonEmpty(values ...string) string {
	for _, value := range values {
		if strings.TrimSpace(value) != "" {
			return strings.TrimSpace(value)
		}
	}
	return ""
}

func cloneStringPtr(p *string) *string {
	if p == nil {
		return nil
	}
	cp := *p
	return &cp
}

func acpTaskProfileDefaults(cfg *config.Config) (string, string) {
	if cfg == nil {
		return "", ""
	}
	model := strings.TrimSpace(cfg.Agent.SubagentModels["task"])
	if model == "" {
		model = strings.TrimSpace(cfg.Agent.SubagentModel)
	}
	effort := strings.TrimSpace(cfg.Agent.SubagentEfforts["task"])
	if effort == "" {
		effort = strings.TrimSpace(cfg.Agent.SubagentEffort)
	}
	return model, effort
}

func newACPSubagentProviderResolver(cfg *config.Config, parent *config.ProviderEntry, proxySpec netclient.ProxySpec) func(string, string) (provider.Provider, *provider.Pricing, int, error) {
	return func(modelRef, effort string) (provider.Provider, *provider.Pricing, int, error) {
		modelRef = strings.TrimSpace(modelRef)
		effort = strings.TrimSpace(effort)

		var entry *config.ProviderEntry
		if modelRef != "" {
			var ok bool
			entry, ok = cfg.ResolveModel(modelRef)
			if !ok {
				return nil, nil, 0, fmt.Errorf("subagent_model %q is not a configured provider", modelRef)
			}
		} else {
			cp := *parent
			entry = &cp
		}

		if effort != "" {
			normalized, err := config.NormalizeEffort(entry, effort)
			if err != nil {
				return nil, nil, 0, err
			}
			entry.Effort = normalized
			if entry.Kind == "anthropic" && strings.TrimSpace(entry.Effort) != "" && strings.TrimSpace(entry.Thinking) == "" {
				entry.Thinking = "adaptive"
			}
		}

		prov, err := boot.NewProviderWithProxy(entry, proxySpec)
		if err != nil {
			return nil, nil, 0, err
		}
		return prov, entry.Price, entry.ContextWindow, nil
	}
}
