package models

import (
	"log"
	"os"
	"strings"

	"github.com/spf13/viper"
)

// loadCustomModelsFromConfig loads custom model definitions from the config file
// and returns them as a map of model ID -> ModelInfo. Returns nil if no custom
// models are configured. Reads from the process-global viper store (the model
// registry is a process-global singleton).
func loadCustomModelsFromConfig() map[string]ModelInfo {
	return loadCustomModelsFrom(viper.GetViper())
}

// loadCustomModelsFrom loads custom model definitions from the supplied store.
// When v is nil the process-global store is used.
func loadCustomModelsFrom(v *viper.Viper) map[string]ModelInfo {
	if v == nil {
		v = viper.GetViper()
	}
	if !v.IsSet("customModels") {
		return nil
	}

	var customModels map[string]CustomModelConfig
	if err := v.UnmarshalKey("customModels", &customModels); err != nil {
		log.Printf("Warning: Failed to parse customModels: %v", err)
		return nil
	}

	result := make(map[string]ModelInfo, len(customModels))
	for modelID, cfg := range customModels {
		info := modelConfigToModelInfo(modelID, cfg)
		result[modelID] = info
	}

	return result
}

// modelConfigToModelInfo converts a CustomModelConfig to a ModelInfo.
func modelConfigToModelInfo(modelID string, cfg CustomModelConfig) ModelInfo {
	info := ModelInfo{
		ID:          modelID,
		Name:        cfg.Name,
		Attachment:  cfg.Attachment,
		Reasoning:   cfg.Reasoning,
		Temperature: cfg.Temperature,
		BaseURL:     cfg.BaseURL,
		APIKey:      cfg.APIKey,
		Cost: Cost{
			Input:  cfg.Cost.Input,
			Output: cfg.Cost.Output,
		},
		Limit: Limit{
			Context: cfg.Limit.Context,
			Output:  cfg.Limit.Output,
		},
	}

	// Convert custom model generation params if any are set.
	if p := convertGenerationParams(cfg.Params); p != nil {
		info.Params = p
	}

	return info
}

// LoadModelSettingsFromConfig loads per-model generation parameter overrides
// from the process-global viper store. Keys are "provider/model" strings.
// Returns nil if no model settings are configured.
func LoadModelSettingsFromConfig() map[string]*GenerationParams {
	return LoadModelSettingsFrom(viper.GetViper())
}

// LoadModelSettingsFrom loads per-model generation parameter overrides from the
// supplied per-instance store. When v is nil the process-global store is used.
// Keys are "provider/model" strings. Returns nil if no model settings are
// configured.
func LoadModelSettingsFrom(v *viper.Viper) map[string]*GenerationParams {
	if v == nil {
		v = viper.GetViper()
	}
	if !v.IsSet("modelSettings") {
		return nil
	}

	var settings map[string]GenerationParamsConfig
	if err := v.UnmarshalKey("modelSettings", &settings); err != nil {
		log.Printf("Warning: Failed to parse modelSettings: %v", err)
		return nil
	}

	result := make(map[string]*GenerationParams, len(settings))
	for modelKey, cfg := range settings {
		if p := convertGenerationParams(cfg); p != nil {
			result[modelKey] = p
		}
	}

	return result
}

// convertGenerationParams converts a GenerationParamsConfig to a GenerationParams.
// Returns nil if no parameters are set.
func convertGenerationParams(cfg GenerationParamsConfig) *GenerationParams {
	p := &GenerationParams{}
	any := false

	if cfg.MaxTokens != nil {
		p.MaxTokens = cfg.MaxTokens
		any = true
	}
	if cfg.Temperature != nil {
		p.Temperature = cfg.Temperature
		any = true
	}
	if cfg.TopP != nil {
		p.TopP = cfg.TopP
		any = true
	}
	if cfg.TopK != nil {
		p.TopK = cfg.TopK
		any = true
	}
	if cfg.FrequencyPenalty != nil {
		p.FrequencyPenalty = cfg.FrequencyPenalty
		any = true
	}
	if cfg.PresencePenalty != nil {
		p.PresencePenalty = cfg.PresencePenalty
		any = true
	}
	if len(cfg.StopSequences) > 0 {
		p.StopSequences = cfg.StopSequences
		any = true
	}
	if cfg.ThinkingLevel != "" {
		p.ThinkingLevel = ParseThinkingLevel(cfg.ThinkingLevel)
		any = true
	}
	if cfg.SystemPrompt != "" {
		p.SystemPrompt = cfg.SystemPrompt
		any = true
	}

	if !any {
		return nil
	}
	return p
}

// ApplyModelSettings merges per-model generation parameter defaults from the
// registry into a ProviderConfig. Model-level params are only applied for
// fields where the user has not explicitly set a value (i.e., the
// corresponding viper key is not set via CLI flag or global config).
//
// The lookup order is:
//  1. modelSettings["provider/model"] from config (highest model-level priority)
//  2. ModelInfo.Params from custom model definitions
//
// Both are overridden by explicit CLI flags / global config values.
func ApplyModelSettings(config *ProviderConfig, modelInfo *ModelInfo) {
	provider, modelName, err := ParseModelString(config.ModelString)
	if err != nil {
		return
	}

	// Resolve the config store: prefer the per-instance store carried on the
	// ProviderConfig (set by BuildProviderConfig / Kit.New), falling back to
	// the process-global store for callers that don't thread one through.
	store := config.ConfigStore

	// Collect model-level params: modelSettings override > custom model params.
	// modelSettings takes priority because it's the more specific/intentional config.
	var params *GenerationParams

	// First check modelSettings from config.
	if settings := LoadModelSettingsFrom(store); settings != nil {
		modelKey := provider + "/" + modelName
		if p, ok := settings[modelKey]; ok {
			params = p
		}
	}

	// Fall back to ModelInfo.Params (from custom model definitions).
	if params == nil && modelInfo != nil && modelInfo.Params != nil {
		params = modelInfo.Params
	}

	if params == nil {
		return
	}

	// Apply each parameter only when the user hasn't explicitly set it.
	// We check viper.IsSet() which returns true only when the key was
	// set via CLI flag, environment variable, or config file global section.

	if params.MaxTokens != nil && !isExplicitlySet(store, "max-tokens") {
		config.MaxTokens = *params.MaxTokens
	}
	if params.Temperature != nil && !isExplicitlySet(store, "temperature") {
		config.Temperature = params.Temperature
	}
	if params.TopP != nil && !isExplicitlySet(store, "top-p") {
		config.TopP = params.TopP
	}
	if params.TopK != nil && !isExplicitlySet(store, "top-k") {
		config.TopK = params.TopK
	}
	if params.FrequencyPenalty != nil && !isExplicitlySet(store, "frequency-penalty") {
		config.FrequencyPenalty = params.FrequencyPenalty
	}
	if params.PresencePenalty != nil && !isExplicitlySet(store, "presence-penalty") {
		config.PresencePenalty = params.PresencePenalty
	}
	if len(params.StopSequences) > 0 && !isExplicitlySet(store, "stop-sequences") {
		config.StopSequences = params.StopSequences
	}
	if params.ThinkingLevel != "" && !isExplicitlySet(store, "thinking-level") {
		config.ThinkingLevel = params.ThinkingLevel
	}
	if params.SystemPrompt != "" && config.SystemPrompt == "" {
		// Resolve file paths: if the value points to an existing file, read it.
		// We check config.SystemPrompt == "" rather than isExplicitlySet because
		// viper.BindPFlag causes IsSet to return true even for unset flags.
		config.SystemPrompt = LoadSystemPromptValue(params.SystemPrompt)
	}
}

// LoadSystemPromptValue resolves a system prompt value that may be either
// inline text or a file path. If the value is a path to an existing file,
// its contents are read and returned. Otherwise the string is returned as-is.
// This mirrors config.LoadSystemPrompt but lives in the models package to
// avoid circular dependencies.
func LoadSystemPromptValue(input string) string {
	if input == "" {
		return ""
	}
	if info, err := os.Stat(input); err == nil && !info.IsDir() {
		content, err := os.ReadFile(input)
		if err != nil {
			log.Printf("Warning: failed to read system prompt file %q: %v", input, err)
			return input
		}
		return strings.TrimSpace(string(content))
	}
	return input
}

// isExplicitlySet returns true when the user has explicitly set a config key
// via CLI flag, environment variable, or the global section of the config file.
// Model-level defaults should not override explicitly set values.
//
// The check runs against the supplied per-instance store when non-nil,
// otherwise the process-global store. This keeps the "explicit vs unset"
// precedence contract per-Kit-instance once a store is threaded through.
func isExplicitlySet(v *viper.Viper, key string) bool {
	if v == nil {
		v = viper.GetViper()
	}
	// viper.IsSet returns true if the key has been set in any of the
	// data stores (flag, env, config file, default). We need to check
	// whether the value was set at the global config level (not just
	// as a default). For generation params, the global config keys use
	// hyphenated names (e.g. "max-tokens", "top-p").
	//
	// Since viper merges all sources, IsSet returns true even for config
	// file values. This means global config file values (e.g.
	// temperature: 0.7 at the top level) will correctly take precedence
	// over model-level defaults, which is the desired behavior.
	return v.IsSet(key)
}

// GenerationParams holds per-model generation parameter defaults.
// These are stored on ModelInfo and applied during provider creation.
// Nil pointer fields mean "no model-level default" — the global config
// or CLI flag value (if any) will be used instead.
type GenerationParams struct {
	MaxTokens        *int
	Temperature      *float32
	TopP             *float32
	TopK             *int32
	FrequencyPenalty *float32
	PresencePenalty  *float32
	StopSequences    []string
	ThinkingLevel    ThinkingLevel
	SystemPrompt     string // Per-model system prompt (inline text or file path)
}

// CustomModelConfig defines a custom model configuration loaded from the config file.
// This is a duplicate here to avoid circular dependencies with internal/config.
type CustomModelConfig struct {
	Name        string                 `json:"name" yaml:"name"`
	BaseURL     string                 `json:"baseUrl,omitempty" yaml:"baseUrl,omitempty"`
	APIKey      string                 `json:"apiKey,omitempty" yaml:"apiKey,omitempty"`
	Family      string                 `json:"family,omitempty" yaml:"family,omitempty"`
	Attachment  bool                   `json:"attachment,omitempty" yaml:"attachment,omitempty"`
	Reasoning   bool                   `json:"reasoning,omitempty" yaml:"reasoning,omitempty"`
	Temperature bool                   `json:"temperature,omitempty" yaml:"temperature,omitempty"`
	Knowledge   string                 `json:"knowledge,omitempty" yaml:"knowledge,omitempty"`
	Cost        CostConfig             `json:"cost" yaml:"cost"`
	Limit       LimitConfig            `json:"limit" yaml:"limit"`
	Params      GenerationParamsConfig `json:"params,omitzero" yaml:"params,omitempty"`
}

// GenerationParamsConfig is the JSON/YAML-serializable form of generation
// parameter defaults. Used in both customModels[].params and modelSettings[].
type GenerationParamsConfig struct {
	MaxTokens        *int     `json:"maxTokens,omitempty" yaml:"maxTokens,omitempty"`
	Temperature      *float32 `json:"temperature,omitempty" yaml:"temperature,omitempty"`
	TopP             *float32 `json:"topP,omitempty" yaml:"topP,omitempty"`
	TopK             *int32   `json:"topK,omitempty" yaml:"topK,omitempty"`
	FrequencyPenalty *float32 `json:"frequencyPenalty,omitempty" yaml:"frequencyPenalty,omitempty"`
	PresencePenalty  *float32 `json:"presencePenalty,omitempty" yaml:"presencePenalty,omitempty"`
	StopSequences    []string `json:"stopSequences,omitempty" yaml:"stopSequences,omitempty"`
	ThinkingLevel    string   `json:"thinkingLevel,omitempty" yaml:"thinkingLevel,omitempty"`
	SystemPrompt     string   `json:"systemPrompt,omitempty" yaml:"systemPrompt,omitempty"`
}

// CostConfig defines the pricing for a custom model.
type CostConfig struct {
	Input  float64 `json:"input" yaml:"input"`
	Output float64 `json:"output" yaml:"output"`
}

// LimitConfig defines context and output limits for a custom model.
type LimitConfig struct {
	Context int `json:"context" yaml:"context"`
	Output  int `json:"output" yaml:"output"`
}
