// This file implements the /btw model/thinking override config (US-005, #282):
// an optional per-command config that lets a side thread use a different model
// and/or reasoning effort than the main session, without touching the main
// session's settings (mirrors pi-btw's pi-btw.json).
//
// The config lives at $PIGO_HOME/btw.json (or ~/.pigo/btw.json). It is read
// fresh on every /btw invocation, so editing it takes effect on the next call
// with no restart. A missing file, an empty object, or an absent field all mean
// "inherit the session default" silently — only a malformed file or an
// unusable model override produces a (non-fatal) warning.
package btw

import (
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"

	"github.com/smallnest/pigo/internal/agentcore"
	"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/provider"
)

// btwConfig is the on-disk shape of ~/.pigo/btw.json. Both fields are optional;
// an absent field (nil / empty) inherits the session default. Pointers/empty
// strings distinguish "not set" from a real value so a partial file still falls
// back per-field.
type btwConfig struct {
	Model         string `json:"model,omitempty"`
	ThinkingLevel string `json:"thinkingLevel,omitempty"`
}

// BtwRunSettings is the resolved model/provider/thinking a side run uses. It is
// computed once per /btw invocation from the session defaults overlaid with
// btw.json, and passed down to AskSide so every turn of that invocation uses
// the same settings.
type BtwRunSettings struct {
	Model         string
	ProviderName  string
	Provider      provider.Provider
	ThinkingLevel agentcore.ThinkingLevel
}

// btwConfigPath returns the path to the /btw override config, or "" when the
// config directory cannot be resolved (then the config is treated as absent).
func btwConfigPath() string {
	dir := run.ConfigDir()
	if dir == "" {
		return ""
	}
	return filepath.Join(dir, "btw.json")
}

// loadBtwConfig reads and parses btw.json. A missing file returns a zero config
// with no error (inherit everything). A malformed file returns an error so the
// caller can warn and fall back. An empty object parses to a zero config.
func loadBtwConfig(path string) (btwConfig, error) {
	if path == "" {
		return btwConfig{}, nil
	}
	data, err := os.ReadFile(path)
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return btwConfig{}, nil
		}
		return btwConfig{}, err
	}
	var cfg btwConfig
	if err := json.Unmarshal(data, &cfg); err != nil {
		return btwConfig{}, fmt.Errorf("parse %s: %w", path, err)
	}
	return cfg, nil
}

// ResolveBtwSettings computes the model/provider/thinking a side run should use.
// It starts from the session defaults (host.Live()) and overlays btw.json:
//
//   - No config / empty object / absent fields → inherit the session values.
//   - thinkingLevel set → validate and override (invalid value warns, falls back).
//   - model set → resolve its provider (reusing resolveProvider like /model);
//     if the model cannot be resolved/authenticated, warn on one line and fall
//     back to the session model+provider.
//
// A malformed config file warns once and inherits everything. Nothing here
// mutates the session live config, so the override is confined to the side
// thread (FR-8).
func ResolveBtwSettings(out io.Writer, host cli.Host) BtwRunSettings {
	live := host.Live()
	s := BtwRunSettings{
		Model:         live.Model,
		ProviderName:  live.ProviderName,
		Provider:      live.Provider,
		ThinkingLevel: live.ThinkingLevel,
	}

	cfg, err := loadBtwConfig(btwConfigPath())
	if err != nil {
		fmt.Fprintf(out, "%s\n", ui.Colorize(ui.Enabled(), ui.Dim, "btw: ignoring invalid btw.json: "+err.Error()))
		return s
	}

	if lvl := strings.TrimSpace(cfg.ThinkingLevel); lvl != "" {
		if v, ok := validThinkingLevel(lvl); ok {
			s.ThinkingLevel = v
		} else {
			fmt.Fprintf(out, "%s\n", ui.Colorize(ui.Enabled(), ui.Dim, fmt.Sprintf("btw: ignoring invalid thinkingLevel %q, using %q", lvl, s.ThinkingLevel)))
		}
	}

	if model := strings.TrimSpace(cfg.Model); model != "" && model != s.Model {
		prov, providerName, perr := provider.ResolveProvider(model, live.BaseURL, live.Protocol, "", os.Getenv)
		if perr != nil {
			fmt.Fprintf(out, "%s\n", ui.Colorize(ui.Enabled(), ui.Dim, fmt.Sprintf("btw: cannot use model %q (%v), falling back to %q", model, perr, s.Model)))
		} else {
			s.Model = model
			s.ProviderName = providerName
			s.Provider = prov
		}
	}

	return s
}

// validThinkingLevel reports whether s is one of the known reasoning-effort
// levels and returns the typed value. It mirrors the enum in agentcore so an
// invalid btw.json value can be rejected without importing the config layer.
func validThinkingLevel(s string) (agentcore.ThinkingLevel, bool) {
	switch agentcore.ThinkingLevel(s) {
	case agentcore.ThinkingOff, agentcore.ThinkingMinimal, agentcore.ThinkingLow,
		agentcore.ThinkingMedium, agentcore.ThinkingHigh, agentcore.ThinkingXHigh, agentcore.ThinkingMax:
		return agentcore.ThinkingLevel(s), true
	default:
		return "", false
	}
}
