package pconfig

import (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"slices"
	"strings"
	"time"

	"github.com/vxcontrol/langchaingo/llms"
	"github.com/vxcontrol/langchaingo/llms/openai"
	"github.com/vxcontrol/langchaingo/llms/reasoning"
	"gopkg.in/yaml.v3"
)

type CallUsage struct {
	Input      int64   `json:"input" yaml:"input"`
	Output     int64   `json:"output" yaml:"output"`
	CacheRead  int64   `json:"cache_read" yaml:"cache_read"`
	CacheWrite int64   `json:"cache_write" yaml:"cache_write"`
	CostInput  float64 `json:"cost_input" yaml:"cost_input"`
	CostOutput float64 `json:"cost_output" yaml:"cost_output"`
}

func NewCallUsage(info map[string]any) CallUsage {
	usage := CallUsage{}
	usage.Fill(info)

	return usage
}

func (c *CallUsage) getInt64(info map[string]any, key string) int64 {
	if info == nil {
		return 0
	}

	if value, ok := info[key]; ok {
		switch v := value.(type) {
		case int:
			return int64(v)
		case int64:
			return v
		case int32:
			return int64(v)
		case float64:
			return int64(v)
		}
	}

	return 0
}

func (c *CallUsage) getFloat64(info map[string]any, key string) float64 {
	if info == nil {
		return 0.0
	}

	if value, ok := info[key]; ok {
		switch v := value.(type) {
		case float64:
			return v
		}
	}

	return 0.0
}

func (c *CallUsage) Fill(info map[string]any) {
	c.Input = c.getInt64(info, "PromptTokens")
	c.Output = c.getInt64(info, "CompletionTokens")
	c.CacheRead = c.getInt64(info, "CacheReadInputTokens")
	c.CacheWrite = c.getInt64(info, "CacheCreationInputTokens")
	c.CostInput = c.getFloat64(info, "UpstreamInferencePromptCost")
	c.CostOutput = c.getFloat64(info, "UpstreamInferenceCompletionsCost")
}

func (c *CallUsage) Merge(other CallUsage) {
	if other.Input > 0 {
		c.Input = other.Input
	}
	if other.Output > 0 {
		c.Output = other.Output
	}
	if other.CacheRead > 0 {
		c.CacheRead = other.CacheRead
	}
	if other.CacheWrite > 0 {
		c.CacheWrite = other.CacheWrite
	}
	if other.CostInput > 0 {
		c.CostInput = other.CostInput
	}
	if other.CostOutput > 0 {
		c.CostOutput = other.CostOutput
	}
}

func (c *CallUsage) UpdateCost(price *PriceInfo) {
	if price == nil {
		return
	}

	// If cost is already calculated by the provider (OpenRouter), don't overwrite it
	if c.CostInput != 0.0 || c.CostOutput != 0.0 {
		return
	}

	// If there are no cache prices, calculate everything at full cost (fallback)
	if price.CacheRead == 0.0 && price.CacheWrite == 0.0 {
		c.CostInput = float64(c.Input) * price.Input / 1e6
		c.CostOutput = float64(c.Output) * price.Output / 1e6
		return
	}

	// Calculation with cache
	uncachedTokens := max(float64(c.Input-c.CacheRead), 0.0)
	cacheReadCost := float64(c.CacheRead) * price.CacheRead / 1e6
	cacheWriteCost := float64(c.CacheWrite) * price.CacheWrite / 1e6

	c.CostInput = uncachedTokens*price.Input/1e6 + cacheReadCost + cacheWriteCost
	c.CostOutput = float64(c.Output) * price.Output / 1e6
}

func (c *CallUsage) IsZero() bool {
	return c.Input == 0 &&
		c.Output == 0 &&
		c.CacheRead == 0 &&
		c.CacheWrite == 0 &&
		c.CostInput == 0.0 &&
		c.CostOutput == 0.0
}

func (c *CallUsage) String() string {
	return fmt.Sprintf("Input: %d, Output: %d, CacheRead: %d, CacheWrite: %d, CostInput: %f, CostOutput: %f",
		c.Input, c.Output, c.CacheRead, c.CacheWrite, c.CostInput, c.CostOutput)
}

type ProviderOptionsType string

const (
	OptionsTypePrimaryAgent ProviderOptionsType = "primary_agent"
	OptionsTypeAssistant    ProviderOptionsType = "assistant"
	OptionsTypeSimple       ProviderOptionsType = "simple"
	OptionsTypeSimpleJSON   ProviderOptionsType = "simple_json"
	OptionsTypeAdviser      ProviderOptionsType = "adviser"
	OptionsTypeGenerator    ProviderOptionsType = "generator"
	OptionsTypeRefiner      ProviderOptionsType = "refiner"
	OptionsTypeSearcher     ProviderOptionsType = "searcher"
	OptionsTypeEnricher     ProviderOptionsType = "enricher"
	OptionsTypeCoder        ProviderOptionsType = "coder"
	OptionsTypeInstaller    ProviderOptionsType = "installer"
	OptionsTypePentester    ProviderOptionsType = "pentester"
	OptionsTypeReflector    ProviderOptionsType = "reflector"
)

var AllAgentTypes = []ProviderOptionsType{
	OptionsTypeSimple,
	OptionsTypeSimpleJSON,
	OptionsTypePrimaryAgent,
	OptionsTypeAssistant,
	OptionsTypeGenerator,
	OptionsTypeRefiner,
	OptionsTypeAdviser,
	OptionsTypeReflector,
	OptionsTypeSearcher,
	OptionsTypeEnricher,
	OptionsTypeCoder,
	OptionsTypeInstaller,
	OptionsTypePentester,
}

func (opt ProviderOptionsType) UsesTools() bool {
	switch opt {
	case OptionsTypeSimpleJSON, OptionsTypeAdviser, OptionsTypeReflector:
		return false
	default:
		return true
	}
}

// A missing ContextWindow or MaxOutputTokens means "the vendor does not publish
// it", never "unlimited": a consumer must keep its prior behaviour for such a model.
type ModelConfig struct {
	Name            string              `json:"name,omitempty" yaml:"name,omitempty"`
	Description     *string             `json:"description,omitempty" yaml:"description,omitempty"`
	ReleaseDate     *time.Time          `json:"release_date,omitempty" yaml:"release_date,omitempty"`
	Thinking        *bool               `json:"thinking,omitempty" yaml:"thinking,omitempty"`
	Reasoning       *ModelReasoningInfo `json:"reasoning,omitempty" yaml:"reasoning,omitempty"`
	Price           *PriceInfo          `json:"price,omitempty" yaml:"price,omitempty"`
	ContextWindow   *int                `json:"context_window,omitempty" yaml:"context_window,omitempty"`
	MaxOutputTokens *int                `json:"max_output_tokens,omitempty" yaml:"max_output_tokens,omitempty"`
}

// ModelReasoningMode declares a model's reasoning capability. It is distinct from
// the per-agent ReasoningConfig.Mode (the chosen mode): it is the source of truth
// that lets a provider force the correct mode for models that require it.
type ModelReasoningMode string

const (
	ModelReasoningNone         ModelReasoningMode = ""              // no extended thinking
	ModelReasoningBudget       ModelReasoningMode = "budget"        // budget thinking only (older Claude)
	ModelReasoningAdaptive     ModelReasoningMode = "adaptive"      // budget or adaptive (Opus 4.6, Sonnet 4.6)
	ModelReasoningAdaptiveOnly ModelReasoningMode = "adaptive-only" // adaptive required; budget returns 400 (Opus 4.7/4.8)
)

// ModelReasoningInfo describes how a model supports extended thinking, so the
// provider and UI can pick a valid mode/effort without hardcoding model names.
type ModelReasoningInfo struct {
	Mode    ModelReasoningMode     `json:"mode,omitempty" yaml:"mode,omitempty"`
	Efforts []llms.ReasoningEffort `json:"efforts,omitempty" yaml:"efforts,omitempty"`
}

type ModelsConfig []ModelConfig

type PriceInfo struct {
	Input      float64 `json:"input,omitempty" yaml:"input,omitempty"`
	Output     float64 `json:"output,omitempty" yaml:"output,omitempty"`
	CacheRead  float64 `json:"cache_read,omitempty" yaml:"cache_read,omitempty"`
	CacheWrite float64 `json:"cache_write,omitempty" yaml:"cache_write,omitempty"`
}

type ReasoningMode string

const (
	ReasoningModeDefault  ReasoningMode = ""
	ReasoningModeAdaptive ReasoningMode = "adaptive"
	ReasoningModeBudget   ReasoningMode = "budget"
	// ReasoningModeOff explicitly disables thinking. It is distinct from Default:
	// Default defers to the model (models like Gemini 2.5 / Claude Fable 5 think
	// by default), while Off forces the provider's disable wire via langchaingo.
	ReasoningModeOff ReasoningMode = "off"
)

type ReasoningConfig struct {
	Mode      ReasoningMode        `json:"mode,omitempty" yaml:"mode,omitempty"`
	Effort    llms.ReasoningEffort `json:"effort,omitempty" yaml:"effort,omitempty"`
	MaxTokens int                  `json:"max_tokens,omitempty" yaml:"max_tokens,omitempty"`
}

func (rc ReasoningConfig) IsZero() bool {
	return rc.Mode == ReasoningModeDefault &&
		rc.Effort == llms.ReasoningNone &&
		rc.MaxTokens == 0
}

func (rc ReasoningConfig) EffectiveMode() ReasoningMode {
	if rc.Mode != ReasoningModeDefault {
		return rc.Mode
	}
	if rc.MaxTokens != 0 && (rc.Effort == llms.ReasoningNone || rc.Effort == llms.ReasoningEffort("none")) {
		return ReasoningModeBudget
	}
	return ReasoningModeDefault
}

var forwardedEfforts = []llms.ReasoningEffort{
	llms.ReasoningMinimal, llms.ReasoningLow, llms.ReasoningMedium, llms.ReasoningHigh, llms.ReasoningXHigh, llms.ReasoningMax,
}

func (rc ReasoningConfig) levelAsked() string {
	switch rc.EffectiveMode() {
	case ReasoningModeOff:
	case ReasoningModeBudget:
		if rc.MaxTokens > 0 {
			return fmt.Sprintf("a reasoning budget of %d tokens", rc.MaxTokens)
		}
	case ReasoningModeAdaptive:
		if rc.Effort != llms.ReasoningNone {
			return fmt.Sprintf("reasoning effort %q", rc.Effort)
		}
	default:
		if slices.Contains(forwardedEfforts, rc.Effort) {
			return fmt.Sprintf("reasoning effort %q", rc.Effort)
		}
	}
	return ""
}

// AgentConfig represents the configuration for a single agent
type AgentConfig struct {
	Model             string          `json:"model,omitempty" yaml:"model,omitempty"`
	MaxTokens         int             `json:"max_tokens,omitempty" yaml:"max_tokens,omitempty"`
	Temperature       float64         `json:"temperature,omitempty" yaml:"temperature,omitempty"`
	TopK              int             `json:"top_k,omitempty" yaml:"top_k,omitempty"`
	TopP              float64         `json:"top_p,omitempty" yaml:"top_p,omitempty"`
	MinP              float64         `json:"min_p,omitempty" yaml:"min_p,omitempty"`
	N                 int             `json:"n,omitempty" yaml:"n,omitempty"`
	MinLength         int             `json:"min_length,omitempty" yaml:"min_length,omitempty"`
	MaxLength         int             `json:"max_length,omitempty" yaml:"max_length,omitempty"`
	RepetitionPenalty float64         `json:"repetition_penalty,omitempty" yaml:"repetition_penalty,omitempty"`
	FrequencyPenalty  float64         `json:"frequency_penalty,omitempty" yaml:"frequency_penalty,omitempty"`
	PresencePenalty   float64         `json:"presence_penalty,omitempty" yaml:"presence_penalty,omitempty"`
	JSON              bool            `json:"json,omitempty" yaml:"json,omitempty"`
	ResponseMIMEType  string          `json:"response_mime_type,omitempty" yaml:"response_mime_type,omitempty"`
	Reasoning         ReasoningConfig `json:"reasoning,omitempty" yaml:"reasoning,omitempty"`
	Price             *PriceInfo      `json:"price,omitempty" yaml:"price,omitempty"`
	ExtraBody         map[string]any  `json:"extra_body,omitempty" yaml:"extra_body,omitempty"`
	raw               map[string]any  `json:"-" yaml:"-"`
}

// ProviderConfig represents the configuration for all agents
type ProviderConfig struct {
	Name           string            `json:"name,omitempty" yaml:"name,omitempty"`
	Simple         *AgentConfig      `json:"simple,omitempty" yaml:"simple,omitempty"`
	SimpleJSON     *AgentConfig      `json:"simple_json,omitempty" yaml:"simple_json,omitempty"`
	PrimaryAgent   *AgentConfig      `json:"primary_agent,omitempty" yaml:"primary_agent,omitempty"`
	Assistant      *AgentConfig      `json:"assistant,omitempty" yaml:"assistant,omitempty"`
	Generator      *AgentConfig      `json:"generator,omitempty" yaml:"generator,omitempty"`
	Refiner        *AgentConfig      `json:"refiner,omitempty" yaml:"refiner,omitempty"`
	Adviser        *AgentConfig      `json:"adviser,omitempty" yaml:"adviser,omitempty"`
	Reflector      *AgentConfig      `json:"reflector,omitempty" yaml:"reflector,omitempty"`
	Searcher       *AgentConfig      `json:"searcher,omitempty" yaml:"searcher,omitempty"`
	Enricher       *AgentConfig      `json:"enricher,omitempty" yaml:"enricher,omitempty"`
	Coder          *AgentConfig      `json:"coder,omitempty" yaml:"coder,omitempty"`
	Installer      *AgentConfig      `json:"installer,omitempty" yaml:"installer,omitempty"`
	Pentester      *AgentConfig      `json:"pentester,omitempty" yaml:"pentester,omitempty"`
	defaultOptions []llms.CallOption `json:"-" yaml:"-"`
	rawConfig      []byte            `json:"-" yaml:"-"`
}

// Validate rejects universally-invalid agent values (negatives where a value is
// physically meaningless, an inverted length window, an out-of-range probability
// or temperature, a reasoning budget over the engine cap). It stays permissive —
// 0 means "unset", and provider-specific tuning ranges are the LLM API's job —
// so it never rejects a valid provider-specific config.
func (pc *ProviderConfig) Validate() error {
	agents := map[string]*AgentConfig{
		"simple": pc.Simple, "simple_json": pc.SimpleJSON, "primary_agent": pc.PrimaryAgent,
		"assistant": pc.Assistant, "generator": pc.Generator, "refiner": pc.Refiner,
		"adviser": pc.Adviser, "reflector": pc.Reflector, "searcher": pc.Searcher,
		"enricher": pc.Enricher, "coder": pc.Coder, "installer": pc.Installer,
		"pentester": pc.Pentester,
	}
	for name, ac := range agents {
		if ac == nil {
			continue
		}
		if err := ac.Validate(); err != nil {
			return fmt.Errorf("%s: %w", name, err)
		}
	}
	return nil
}

func (pc *ProviderConfig) ValidateToolReasoning(rp reasoning.Provider) error {
	if pc == nil || rp != reasoning.ProviderOpenAI {
		return nil
	}
	for _, opt := range AllAgentTypes {
		ac := pc.AgentConfigForType(opt)
		if !opt.UsesTools() || ac == nil {
			continue
		}
		level := ac.Reasoning.levelAsked()
		if level == "" {
			continue
		}
		if model := pc.effectiveModel(opt); reasoning.EffortWithTools(model) != reasoning.EffortToolsFree {
			return fmt.Errorf("%s: model %q refuses %s on requests with function tools; clear it or turn reasoning off",
				opt, model, level)
		}
	}
	return nil
}

func (pc *ProviderConfig) ValidateReasoningOff(rp reasoning.Provider) error {
	if pc == nil || rp == reasoning.ProviderUnknown {
		return nil
	}
	for _, opt := range AllAgentTypes {
		ac := pc.AgentConfigForType(opt)
		if ac == nil || ac.Reasoning.EffectiveMode() != ReasoningModeOff {
			continue
		}
		if model := pc.effectiveModel(opt); model != "" && reasoning.ResolveOff(model, rp) == reasoning.OffUnsupported {
			return fmt.Errorf("%s: model %q cannot turn reasoning off; clear Reasoning Mode Off", opt, model)
		}
	}
	return nil
}

func (pc *ProviderConfig) ValidateThinkingBudget(rp reasoning.Provider, prefix string) error {
	if pc == nil || rp != reasoning.ProviderOpenAI {
		return nil
	}
	for _, opt := range AllAgentTypes {
		ac := pc.AgentConfigForType(opt)
		if ac == nil || ac.Reasoning.EffectiveMode() != ReasoningModeBudget || ac.Reasoning.MaxTokens <= 0 {
			continue
		}
		model := pc.effectiveModel(opt)
		if model == "" {
			continue
		}
		if prefix != "" {
			model = prefix + "/" + model
		}
		if reasoning.TakesNoThinkingDepth(model) {
			return fmt.Errorf("%s: model %q takes no thinking budget; clear Reasoning Max Tokens", opt, model)
		}
	}
	return nil
}

func (ac *AgentConfig) Validate() error {
	if ac.Model != "" && strings.TrimSpace(ac.Model) == "" {
		return fmt.Errorf("model must not consist of whitespace only")
	}
	if ac.Temperature < 0 || ac.Temperature > 2 {
		return fmt.Errorf("temperature %v out of range [0, 2]", ac.Temperature)
	}
	if ac.TopP < 0 || ac.TopP > 1 {
		return fmt.Errorf("top_p %v out of range [0, 1]", ac.TopP)
	}
	if ac.TopK < 0 {
		return fmt.Errorf("top_k %d must be >= 0", ac.TopK)
	}
	if ac.MaxTokens < 0 {
		return fmt.Errorf("max_tokens %d must be >= 0", ac.MaxTokens)
	}
	if ac.MinLength < 0 {
		return fmt.Errorf("min_length %d must be >= 0", ac.MinLength)
	}
	if ac.MaxLength < 0 {
		return fmt.Errorf("max_length %d must be >= 0", ac.MaxLength)
	}
	if ac.MinLength > 0 && ac.MaxLength > 0 && ac.MinLength > ac.MaxLength {
		return fmt.Errorf("min_length %d must not exceed max_length %d", ac.MinLength, ac.MaxLength)
	}
	if ac.RepetitionPenalty < 0 || ac.RepetitionPenalty > 2 {
		return fmt.Errorf("repetition_penalty %v out of range [0, 2]", ac.RepetitionPenalty)
	}
	if ac.FrequencyPenalty < -2 || ac.FrequencyPenalty > 2 {
		return fmt.Errorf("frequency_penalty %v out of range [-2, 2]", ac.FrequencyPenalty)
	}
	if ac.PresencePenalty < -2 || ac.PresencePenalty > 2 {
		return fmt.Errorf("presence_penalty %v out of range [-2, 2]", ac.PresencePenalty)
	}
	if ac.Reasoning.MaxTokens < 0 || ac.Reasoning.MaxTokens > 32768 {
		return fmt.Errorf("reasoning.max_tokens %d out of range [0, 32768]", ac.Reasoning.MaxTokens)
	}
	if ac.Reasoning.Mode == ReasoningModeBudget && ac.Reasoning.MaxTokens <= 0 {
		return fmt.Errorf("reasoning.max_tokens is required when reasoning.mode is %q", ReasoningModeBudget)
	}
	if ac.Price != nil &&
		(ac.Price.Input < 0 || ac.Price.Output < 0 || ac.Price.CacheRead < 0 || ac.Price.CacheWrite < 0) {
		return fmt.Errorf("price values must be >= 0")
	}
	return nil
}

// ValidateThinkingAgreement rejects a config where an extra_body thinking toggle
// contradicts the reasoning block. Only the openai door merges extra_body over its
// own fields, where a toggle wins over the reasoning block on the wire: Reasoning
// Mode Off would still send thinking, or a reasoning effort would still send a
// disable. The other doors drop extra_body unread.
func (pc *ProviderConfig) ValidateThinkingAgreement(rp reasoning.Provider) error {
	if pc == nil || rp != reasoning.ProviderOpenAI {
		return nil
	}
	for _, opt := range AllAgentTypes {
		ac := pc.AgentConfigForType(opt)
		if ac == nil {
			continue
		}
		if err := ac.validateThinkingAgreement(); err != nil {
			return fmt.Errorf("%s: %w", opt, err)
		}
	}
	return nil
}

func (ac *AgentConfig) validateThinkingAgreement() error {
	mode := ac.Reasoning.EffectiveMode()
	reasoningOff := mode == ReasoningModeOff
	reasoningOn := !reasoningOff && (mode == ReasoningModeAdaptive || mode == ReasoningModeBudget ||
		slices.Contains(forwardedEfforts, ac.Reasoning.Effort))

	extraOn, extraOff := extraBodyThinkingIntent(ac.ExtraBody)

	switch {
	case reasoningOff && extraOn:
		return fmt.Errorf("extra_body enables thinking while reasoning.mode is %q; "+
			"drop the extra_body thinking toggle or clear reasoning.mode", ReasoningModeOff)
	case reasoningOn && extraOff:
		return fmt.Errorf("extra_body disables thinking while the reasoning block requests it; " +
			"drop the extra_body thinking toggle or turn reasoning off")
	}
	return nil
}

// extraBodyThinkingIntent reads a thinking decision out of every extra_body key
// the openai door writes one under: enable_thinking, thinking.type,
// reasoning_effort and reasoning.effort, where "none" is the disable level.
func extraBodyThinkingIntent(extra map[string]any) (on, off bool) {
	if extra == nil {
		return
	}
	if v, ok := extra["enable_thinking"].(bool); ok {
		on, off = v, !v
	}
	if th, ok := extra["thinking"].(map[string]any); ok {
		if kind, typed := th["type"]; typed {
			if kind == "disabled" {
				off = true
			} else {
				on = true
			}
		}
	}
	levels := []any{extra["reasoning_effort"]}
	if r, ok := extra["reasoning"].(map[string]any); ok {
		levels = append(levels, r["effort"])
	}
	for _, level := range levels {
		if level, ok := level.(string); ok && level != "" {
			if level == reasoning.OpenAIDisableEffort {
				off = true
			} else {
				on = true
			}
		}
	}
	return on, off
}

const EmptyProviderConfigRaw = `{
  "simple": {},
  "simple_json": {},
  "primary_agent": {},
  "assistant": {},
  "generator": {},
  "refiner": {},
  "adviser": {},
  "reflector": {},
  "searcher": {},
  "enricher": {},
  "coder": {},
  "installer": {},
  "pentester": {}
}`

func LoadConfig(configPath string, defaultOptions []llms.CallOption) (*ProviderConfig, error) {
	if configPath == "" {
		return nil, nil
	}

	data, err := os.ReadFile(configPath)
	if err != nil {
		return nil, fmt.Errorf("failed to read config file: %w", err)
	}

	var config ProviderConfig
	ext := filepath.Ext(configPath)
	switch ext {
	case ".json":
		if err := json.Unmarshal(data, &config); err != nil {
			return nil, fmt.Errorf("failed to parse JSON config: %w", err)
		}
	case ".yaml", ".yml":
		if err := yaml.Unmarshal(data, &config); err != nil {
			return nil, fmt.Errorf("failed to parse YAML config: %w", err)
		}
	default:
		return nil, fmt.Errorf("unsupported config file extension: %s", ext)
	}

	// handle backward compatibility with legacy config format
	handleLegacyConfig(&config, data)

	config.SetDefaultOptions(defaultOptions)
	config.rawConfig = data

	return &config, nil
}

func LoadConfigData(configData []byte, defaultOptions []llms.CallOption) (*ProviderConfig, error) {
	var config ProviderConfig

	if err := yaml.Unmarshal(configData, &config); err != nil {
		if err := json.Unmarshal(configData, &config); err != nil {
			return nil, fmt.Errorf("failed to parse config: %w", err)
		}
	}

	// handle backward compatibility with legacy config format
	handleLegacyConfig(&config, configData)

	config.SetDefaultOptions(defaultOptions)
	config.rawConfig = configData

	return &config, nil
}

func LoadModelsConfigData(configData []byte) (ModelsConfig, error) {
	var modelsConfig ModelsConfig

	if err := yaml.Unmarshal(configData, &modelsConfig); err != nil {
		return nil, fmt.Errorf("failed to parse models config: %w", err)
	}

	return modelsConfig, nil
}

// UnmarshalJSON implements custom JSON unmarshaling for ModelConfig
func modelLimit(raw map[string]any, key string) *int {
	switch v := raw[key].(type) {
	case float64:
		n := int(v)
		return &n
	case int:
		return &v
	}

	return nil
}

func (mc *ModelConfig) UnmarshalJSON(data []byte) error {
	var raw map[string]any
	if err := json.Unmarshal(data, &raw); err != nil {
		return err
	}

	if name, ok := raw["name"].(string); ok {
		mc.Name = name
	}

	if desc, ok := raw["description"].(string); ok {
		mc.Description = &desc
	}

	if thinking, ok := raw["thinking"].(bool); ok {
		mc.Thinking = &thinking
	}

	if dateStr, ok := raw["release_date"].(string); ok && dateStr != "" {
		parsedDate, err := time.Parse("2006-01-02", dateStr)
		if err != nil {
			return fmt.Errorf("invalid release_date format, expected YYYY-MM-DD: %w", err)
		}
		mc.ReleaseDate = &parsedDate
	}

	if priceData, ok := raw["price"]; ok && priceData != nil {
		priceBytes, err := json.Marshal(priceData)
		if err != nil {
			return err
		}
		var price PriceInfo
		if err := json.Unmarshal(priceBytes, &price); err != nil {
			return err
		}
		mc.Price = &price
	}

	if reasoningData, ok := raw["reasoning"]; ok && reasoningData != nil {
		reasoningBytes, err := json.Marshal(reasoningData)
		if err != nil {
			return err
		}
		var reasoning ModelReasoningInfo
		if err := json.Unmarshal(reasoningBytes, &reasoning); err != nil {
			return err
		}
		mc.Reasoning = &reasoning
	}

	mc.ContextWindow = modelLimit(raw, "context_window")
	mc.MaxOutputTokens = modelLimit(raw, "max_output_tokens")

	return nil
}

// UnmarshalYAML implements custom YAML unmarshaling for ModelConfig
func (mc *ModelConfig) UnmarshalYAML(value *yaml.Node) error {
	var raw map[string]any
	if err := value.Decode(&raw); err != nil {
		return err
	}

	if name, ok := raw["name"].(string); ok {
		mc.Name = name
	}

	if desc, ok := raw["description"].(string); ok {
		mc.Description = &desc
	}

	if thinking, ok := raw["thinking"].(bool); ok {
		mc.Thinking = &thinking
	}

	// Handle release_date - YAML can parse it as string or time.Time
	if dateValue, ok := raw["release_date"]; ok && dateValue != nil {
		switch v := dateValue.(type) {
		case string:
			if v != "" {
				parsedDate, err := time.Parse("2006-01-02", v)
				if err != nil {
					return fmt.Errorf("invalid release_date format, expected YYYY-MM-DD: %w", err)
				}
				mc.ReleaseDate = &parsedDate
			}
		case time.Time:
			// YAML automatically parsed it as time.Time
			mc.ReleaseDate = &v
		}
	}

	if priceData, ok := raw["price"]; ok && priceData != nil {
		priceBytes, err := yaml.Marshal(priceData)
		if err != nil {
			return err
		}
		var price PriceInfo
		if err := yaml.Unmarshal(priceBytes, &price); err != nil {
			return err
		}
		mc.Price = &price
	}

	if reasoningData, ok := raw["reasoning"]; ok && reasoningData != nil {
		reasoningBytes, err := yaml.Marshal(reasoningData)
		if err != nil {
			return err
		}
		var reasoning ModelReasoningInfo
		if err := yaml.Unmarshal(reasoningBytes, &reasoning); err != nil {
			return err
		}
		mc.Reasoning = &reasoning
	}

	mc.ContextWindow = modelLimit(raw, "context_window")
	mc.MaxOutputTokens = modelLimit(raw, "max_output_tokens")

	return nil
}

// MarshalJSON implements custom JSON marshaling for ModelConfig
func (mc ModelConfig) MarshalJSON() ([]byte, error) {
	aux := map[string]any{}

	if mc.Name != "" {
		aux["name"] = mc.Name
	}
	if mc.Description != nil {
		aux["description"] = *mc.Description
	}
	if mc.Thinking != nil {
		aux["thinking"] = *mc.Thinking
	}
	if mc.ReleaseDate != nil {
		aux["release_date"] = mc.ReleaseDate.Format("2006-01-02")
	}
	if mc.Price != nil {
		aux["price"] = mc.Price
	}
	if mc.Reasoning != nil {
		aux["reasoning"] = mc.Reasoning
	}
	if mc.ContextWindow != nil {
		aux["context_window"] = *mc.ContextWindow
	}
	if mc.MaxOutputTokens != nil {
		aux["max_output_tokens"] = *mc.MaxOutputTokens
	}

	return json.Marshal(aux)
}

// MarshalYAML implements custom YAML marshaling for ModelConfig
func (mc ModelConfig) MarshalYAML() (any, error) {
	aux := map[string]any{}

	if mc.Name != "" {
		aux["name"] = mc.Name
	}
	if mc.Description != nil {
		aux["description"] = *mc.Description
	}
	if mc.Thinking != nil {
		aux["thinking"] = *mc.Thinking
	}
	if mc.ReleaseDate != nil {
		aux["release_date"] = mc.ReleaseDate.Format("2006-01-02")
	}
	if mc.Price != nil {
		aux["price"] = mc.Price
	}
	if mc.Reasoning != nil {
		aux["reasoning"] = mc.Reasoning
	}
	if mc.ContextWindow != nil {
		aux["context_window"] = *mc.ContextWindow
	}
	if mc.MaxOutputTokens != nil {
		aux["max_output_tokens"] = *mc.MaxOutputTokens
	}

	return aux, nil
}

// handleLegacyConfig provides backward compatibility for old config format
// where "agent" was used instead of "primary_agent"
func handleLegacyConfig(config *ProviderConfig, data []byte) {
	// only process if PrimaryAgent is not set
	if config.PrimaryAgent != nil {
		// still handle assistant backward compatibility
		if config.Assistant == nil {
			config.Assistant = config.PrimaryAgent
		}
		return
	}

	// define legacy config structure with old "agent" field
	type LegacyProviderConfig struct {
		Agent *AgentConfig `json:"agent,omitempty" yaml:"agent,omitempty"`
	}

	var legacyConfig LegacyProviderConfig

	if err := yaml.Unmarshal(data, &legacyConfig); err != nil {
		if err := json.Unmarshal(data, &legacyConfig); err != nil {
			return
		}
	}

	if legacyConfig.Agent != nil {
		config.PrimaryAgent = legacyConfig.Agent
	}

	if config.Assistant == nil {
		config.Assistant = config.PrimaryAgent
	}
}

func (ac *AgentConfig) UnmarshalJSON(data []byte) error {
	type embed AgentConfig
	var unmarshaler embed
	if err := json.Unmarshal(data, &unmarshaler); err != nil {
		return err
	}
	*ac = AgentConfig(unmarshaler)

	var raw map[string]any
	if err := json.Unmarshal(data, &raw); err != nil {
		return err
	}
	ac.raw = raw
	ac.normaliseModel()

	return nil
}

// IsSet reports whether the decoded config carried this key, whatever its value.
func (ac *AgentConfig) IsSet(key string) bool {
	if ac == nil || ac.raw == nil {
		return false
	}

	_, ok := ac.raw[key]

	return ok
}

// ClearRaw clears the raw map, forcing marshal to use struct field values
func (ac *AgentConfig) ClearRaw() {
	if ac != nil {
		ac.raw = nil
	}
}

func (ac *AgentConfig) UnmarshalYAML(value *yaml.Node) error {
	type embed AgentConfig
	var unmarshaler embed
	if err := value.Decode(&unmarshaler); err != nil {
		return err
	}
	*ac = AgentConfig(unmarshaler)

	var raw map[string]any
	if err := value.Decode(&raw); err != nil {
		return err
	}
	ac.raw = raw
	ac.normaliseModel()

	return nil
}

func (ac *AgentConfig) normaliseModel() {
	trimmed := strings.TrimSpace(ac.Model)
	if trimmed == "" {
		return
	}

	ac.Model = trimmed
	if _, ok := ac.raw["model"].(string); ok {
		ac.raw["model"] = trimmed
	}
}

func (ac *AgentConfig) BuildOptions() []llms.CallOption {
	if ac == nil || ac.raw == nil {
		return nil
	}

	var options []llms.CallOption

	if _, ok := ac.raw["model"]; ok && ac.Model != "" {
		options = append(options, llms.WithModel(ac.Model))
	}
	if _, ok := ac.raw["max_tokens"]; ok {
		options = append(options, llms.WithMaxTokens(ac.MaxTokens))
	}
	if _, ok := ac.raw["temperature"]; ok {
		options = append(options, llms.WithTemperature(ac.Temperature))
	}
	if _, ok := ac.raw["top_k"]; ok {
		options = append(options, llms.WithTopK(ac.TopK))
	}
	if _, ok := ac.raw["top_p"]; ok {
		options = append(options, llms.WithTopP(ac.TopP))
	}
	if _, ok := ac.raw["min_p"]; ok {
		options = append(options, llms.WithMinP(ac.MinP))
	}
	if _, ok := ac.raw["n"]; ok {
		options = append(options, llms.WithN(ac.N))
	}
	if _, ok := ac.raw["min_length"]; ok {
		options = append(options, llms.WithMinLength(ac.MinLength))
	}
	if _, ok := ac.raw["max_length"]; ok {
		options = append(options, llms.WithMaxLength(ac.MaxLength))
	}
	if _, ok := ac.raw["repetition_penalty"]; ok {
		options = append(options, llms.WithRepetitionPenalty(ac.RepetitionPenalty))
	}
	if _, ok := ac.raw["frequency_penalty"]; ok {
		options = append(options, llms.WithFrequencyPenalty(ac.FrequencyPenalty))
	}
	if _, ok := ac.raw["presence_penalty"]; ok {
		options = append(options, llms.WithPresencePenalty(ac.PresencePenalty))
	}
	if _, ok := ac.raw["json"]; ok {
		options = append(options, llms.WithJSONMode())
	}
	if _, ok := ac.raw["response_mime_type"]; ok && ac.ResponseMIMEType != "" {
		options = append(options, llms.WithResponseMIMEType(ac.ResponseMIMEType))
	}
	// The reasoning block is the provider-agnostic thinking control. langchaingo's
	// shared openai adapter turns it into each vendor's own wire: reasoning.mode
	// "off" becomes thinking.type=disabled (GLM/Kimi/DeepSeek) or enable_thinking
	// false (DashScope), and reasoning.effort becomes the reasoning_effort string
	// where the vendor documents it. extra_body (emitted below) merges into that
	// wire key by key and wins on every key, so a thinking decision there that
	// contradicts this block is refused before persist by
	// ProviderConfig.ValidateThinkingAgreement.
	if _, ok := ac.raw["reasoning"]; ok && !ac.Reasoning.IsZero() {
		switch ac.Reasoning.EffectiveMode() {
		case ReasoningModeOff:
			options = append(options, llms.WithReasoningDisabled())
		case ReasoningModeAdaptive:
			// Adaptive thinking is applied per-call by PrepareAdaptiveCallOptions
			// (it appends llms.WithAdaptiveReasoning); no CallOption is emitted here.
		case ReasoningModeBudget:
			if ac.Reasoning.MaxTokens > 0 && ac.Reasoning.MaxTokens <= 32768 {
				options = append(options, llms.WithReasoning(llms.ReasoningNone, ac.Reasoning.MaxTokens))
			}
		default:
			if slices.Contains(forwardedEfforts, ac.Reasoning.Effort) {
				options = append(options, llms.WithReasoning(ac.Reasoning.Effort, 0))
			}
		}
	}
	if _, ok := ac.raw["extra_body"]; ok && ac.ExtraBody != nil {
		options = append(options, openai.WithExtraBody(ac.ExtraBody))
	}

	return options
}

func (ac *AgentConfig) marshalMap() map[string]any {
	if ac == nil {
		return nil
	}

	if ac.raw != nil {
		return ac.raw
	}

	// add non-zero values
	output := make(map[string]any)
	if ac.Model != "" {
		output["model"] = ac.Model
	}
	if ac.MaxTokens != 0 {
		output["max_tokens"] = ac.MaxTokens
	}
	if ac.Temperature != 0 {
		output["temperature"] = ac.Temperature
	}
	if ac.TopK != 0 {
		output["top_k"] = ac.TopK
	}
	if ac.TopP != 0 {
		output["top_p"] = ac.TopP
	}
	if ac.MinP != 0 {
		output["min_p"] = ac.MinP
	}
	if ac.N != 0 {
		output["n"] = ac.N
	}
	if ac.MinLength != 0 {
		output["min_length"] = ac.MinLength
	}
	if ac.MaxLength != 0 {
		output["max_length"] = ac.MaxLength
	}
	if ac.RepetitionPenalty != 0 {
		output["repetition_penalty"] = ac.RepetitionPenalty
	}
	if ac.FrequencyPenalty != 0 {
		output["frequency_penalty"] = ac.FrequencyPenalty
	}
	if ac.PresencePenalty != 0 {
		output["presence_penalty"] = ac.PresencePenalty
	}
	if ac.JSON {
		output["json"] = ac.JSON
	}
	if ac.ResponseMIMEType != "" {
		output["response_mime_type"] = ac.ResponseMIMEType
	}
	if !ac.Reasoning.IsZero() {
		output["reasoning"] = ac.Reasoning
	}
	if ac.Price != nil {
		output["price"] = ac.Price
	}
	if ac.ExtraBody != nil {
		output["extra_body"] = ac.ExtraBody
	}

	return output
}

func (ac *AgentConfig) MarshalJSON() ([]byte, error) {
	if ac == nil {
		return []byte("null"), nil
	}
	return json.Marshal(ac.marshalMap())
}

func (ac *AgentConfig) MarshalYAML() (any, error) {
	if ac == nil {
		return nil, nil
	}
	return ac.marshalMap(), nil
}

func (pc *ProviderConfig) SetDefaultOptions(defaultOptions []llms.CallOption) {
	if pc == nil {
		return
	}
	pc.defaultOptions = slices.Clip(defaultOptions)
}

func (pc *ProviderConfig) GetDefaultOptions() []llms.CallOption {
	if pc == nil {
		return nil
	}
	return pc.defaultOptions
}

func (pc *ProviderConfig) SetRawConfig(rawConfig []byte) {
	if pc == nil {
		return
	}
	pc.rawConfig = rawConfig
}

func (pc *ProviderConfig) GetRawConfig() []byte {
	if pc == nil {
		return nil
	}
	return pc.rawConfig
}

func (pc *ProviderConfig) GetModelsMap() map[ProviderOptionsType]string {
	if pc == nil {
		return nil
	}

	models := make(map[ProviderOptionsType]string)
	options := pc.BuildOptionsMap()
	for optType, options := range options {
		if len(options) == 0 {
			continue
		}

		var callOptions llms.CallOptions
		for _, option := range options {
			option(&callOptions)
		}
		if callOptions.Model != nil {
			models[optType] = callOptions.GetModel()
		}
	}

	return models
}

func (pc *ProviderConfig) GetOptionsForType(optType ProviderOptionsType) []llms.CallOption {
	if pc == nil {
		return nil
	}

	var agentConfig *AgentConfig
	switch optType {
	case OptionsTypeSimple:
		agentConfig = pc.Simple
	case OptionsTypeSimpleJSON:
		return pc.buildSimpleJSONOptions()
	case OptionsTypePrimaryAgent:
		agentConfig = pc.PrimaryAgent
	case OptionsTypeAssistant:
		return pc.buildAssistantOptions()
	case OptionsTypeGenerator:
		agentConfig = pc.Generator
	case OptionsTypeRefiner:
		agentConfig = pc.Refiner
	case OptionsTypeAdviser:
		agentConfig = pc.Adviser
	case OptionsTypeReflector:
		agentConfig = pc.Reflector
	case OptionsTypeSearcher:
		agentConfig = pc.Searcher
	case OptionsTypeEnricher:
		agentConfig = pc.Enricher
	case OptionsTypeCoder:
		agentConfig = pc.Coder
	case OptionsTypeInstaller:
		agentConfig = pc.Installer
	case OptionsTypePentester:
		agentConfig = pc.Pentester
	default:
		return nil
	}

	return pc.layerOverDefaults(agentConfig.BuildOptions())
}

func (pc *ProviderConfig) layerOverDefaults(options []llms.CallOption) []llms.CallOption {
	switch {
	case len(options) == 0:
		return pc.defaultOptions
	case len(pc.defaultOptions) == 0:
		return options
	}

	layered := make([]llms.CallOption, 0, len(pc.defaultOptions)+len(options))
	layered = append(layered, pc.defaultOptions...)

	return append(layered, options...)
}

// AgentConfigForType returns the agent config for opt, applying the
// SimpleJSON→Simple and Assistant→PrimaryAgent fallbacks. Returns nil for an
// unset slot or unknown type.
func (pc *ProviderConfig) AgentConfigForType(optType ProviderOptionsType) *AgentConfig {
	if pc == nil {
		return nil
	}

	switch optType {
	case OptionsTypeSimple:
		return pc.Simple
	case OptionsTypeSimpleJSON:
		if pc.SimpleJSON != nil {
			return pc.SimpleJSON
		}
		return pc.Simple
	case OptionsTypePrimaryAgent:
		return pc.PrimaryAgent
	case OptionsTypeAssistant:
		if pc.Assistant != nil {
			return pc.Assistant
		}
		return pc.PrimaryAgent
	case OptionsTypeGenerator:
		return pc.Generator
	case OptionsTypeRefiner:
		return pc.Refiner
	case OptionsTypeAdviser:
		return pc.Adviser
	case OptionsTypeReflector:
		return pc.Reflector
	case OptionsTypeSearcher:
		return pc.Searcher
	case OptionsTypeEnricher:
		return pc.Enricher
	case OptionsTypeCoder:
		return pc.Coder
	case OptionsTypeInstaller:
		return pc.Installer
	case OptionsTypePentester:
		return pc.Pentester
	default:
		return nil
	}
}

func (pc *ProviderConfig) GetPriceInfoForType(optType ProviderOptionsType) *PriceInfo {
	if agentConfig := pc.AgentConfigForType(optType); agentConfig != nil {
		return agentConfig.Price
	}

	return nil
}

func (pc *ProviderConfig) effectiveModel(opt ProviderOptionsType) string {
	if agentConfig := pc.AgentConfigForType(opt); agentConfig != nil && agentConfig.Model != "" {
		return agentConfig.Model
	}

	var options llms.CallOptions
	for _, option := range pc.GetOptionsForType(opt) {
		option(&options)
	}

	return options.GetModel()
}

func (pc *ProviderConfig) modelReasoningMode(models ModelsConfig, opt ProviderOptionsType) ModelReasoningMode {
	model := pc.effectiveModel(opt)
	if model == "" {
		return ModelReasoningNone
	}

	for _, m := range models {
		if m.Name == model && m.Reasoning != nil {
			return m.Reasoning.Mode
		}
	}

	return ModelReasoningNone
}

func (pc *ProviderConfig) reasoningConfigForType(opt ProviderOptionsType) (ReasoningConfig, bool) {
	agentConfig := pc.AgentConfigForType(opt)
	if agentConfig == nil || agentConfig.Reasoning.IsZero() {
		return ReasoningConfig{}, false
	}

	return agentConfig.Reasoning, true
}

// UsesAdaptiveThinking reports whether this agent's call must use adaptive
// thinking: the agent selected adaptive mode, or the model only supports adaptive
// (e.g. Opus 4.7/4.8, where budget thinking returns a 400).
func (pc *ProviderConfig) UsesAdaptiveThinking(models ModelsConfig, opt ProviderOptionsType) bool {
	// An explicit off wins over the adaptive-only auto-adaptive below: the caller
	// disabled thinking, so no adaptive CallOption may be appended (the disable is
	// emitted in BuildOptions instead).
	if reasoning, ok := pc.reasoningConfigForType(opt); ok && reasoning.EffectiveMode() == ReasoningModeOff {
		return false
	}
	if pc.modelReasoningMode(models, opt) == ModelReasoningAdaptiveOnly {
		return true
	}

	reasoning, ok := pc.reasoningConfigForType(opt)
	return ok && reasoning.EffectiveMode() == ReasoningModeAdaptive
}

func (pc *ProviderConfig) PrepareAdaptiveCallOptions(
	ctx context.Context,
	models ModelsConfig,
	opt ProviderOptionsType,
	options []llms.CallOption,
) (context.Context, []llms.CallOption) {
	if !pc.UsesAdaptiveThinking(models, opt) {
		return ctx, options
	}

	reasoning, _ := pc.reasoningConfigForType(opt)
	options = append(options, llms.WithAdaptiveReasoning(reasoning.Effort))

	return ctx, options
}

func (pc *ProviderConfig) BuildOptionsMap() map[ProviderOptionsType][]llms.CallOption {
	if pc == nil {
		return nil
	}

	options := map[ProviderOptionsType][]llms.CallOption{
		OptionsTypeSimple:       pc.GetOptionsForType(OptionsTypeSimple),
		OptionsTypeSimpleJSON:   pc.GetOptionsForType(OptionsTypeSimpleJSON),
		OptionsTypePrimaryAgent: pc.GetOptionsForType(OptionsTypePrimaryAgent),
		OptionsTypeAssistant:    pc.GetOptionsForType(OptionsTypeAssistant),
		OptionsTypeGenerator:    pc.GetOptionsForType(OptionsTypeGenerator),
		OptionsTypeRefiner:      pc.GetOptionsForType(OptionsTypeRefiner),
		OptionsTypeAdviser:      pc.GetOptionsForType(OptionsTypeAdviser),
		OptionsTypeReflector:    pc.GetOptionsForType(OptionsTypeReflector),
		OptionsTypeSearcher:     pc.GetOptionsForType(OptionsTypeSearcher),
		OptionsTypeEnricher:     pc.GetOptionsForType(OptionsTypeEnricher),
		OptionsTypeCoder:        pc.GetOptionsForType(OptionsTypeCoder),
		OptionsTypeInstaller:    pc.GetOptionsForType(OptionsTypeInstaller),
		OptionsTypePentester:    pc.GetOptionsForType(OptionsTypePentester),
	}

	return options
}

func (pc *ProviderConfig) buildSimpleJSONOptions() []llms.CallOption {
	if pc == nil {
		return nil
	}

	if options := pc.SimpleJSON.BuildOptions(); options != nil {
		return pc.layerOverDefaults(options)
	}

	if options := pc.Simple.BuildOptions(); options != nil {
		return append(pc.layerOverDefaults(options), llms.WithJSONMode())
	}

	if pc.defaultOptions != nil {
		return append(pc.defaultOptions, llms.WithJSONMode())
	}

	return nil
}

func (pc *ProviderConfig) buildAssistantOptions() []llms.CallOption {
	if pc == nil {
		return nil
	}

	if options := pc.Assistant.BuildOptions(); options != nil {
		return pc.layerOverDefaults(options)
	}

	return pc.layerOverDefaults(pc.PrimaryAgent.BuildOptions())
}
