package config

import (
	"fmt"
	"sort"
)

// CurrentConfigVersion is the schema version this binary expects. Files at a
// lower version are migrated automatically when loaded.
const CurrentConfigVersion = 2

// ConfigMigration upgrades a raw YAML map by one version.
type ConfigMigration struct {
	From, To int
	Describe string
	Apply    func(map[string]any) error
}

// migrations is the ordered ladder of upgrades. Add new entries at the end.
var migrations = []ConfigMigration{
	{From: 1, To: 2, Describe: "named providers + role chains", Apply: migrateV1toV2},
}

// Migrate brings raw up to CurrentConfigVersion in place. Returns the list of
// migrations that were applied (may be empty if already current).
func Migrate(raw map[string]any) ([]ConfigMigration, error) {
	if raw == nil {
		return nil, fmt.Errorf("migrate: raw config is nil")
	}

	version := readVersion(raw)
	if version > CurrentConfigVersion {
		return nil, fmt.Errorf("migrate: config version %d is newer than supported version %d", version, CurrentConfigVersion)
	}

	applied := make([]ConfigMigration, 0)
	for {
		if version >= CurrentConfigVersion {
			break
		}
		step, ok := findMigration(version)
		if !ok {
			return nil, fmt.Errorf("migrate: no migration registered from version %d", version)
		}
		if err := step.Apply(raw); err != nil {
			return nil, fmt.Errorf("migrate v%d->v%d: %w", step.From, step.To, err)
		}
		raw["version"] = step.To
		version = step.To
		applied = append(applied, step)
	}
	return applied, nil
}

func findMigration(from int) (ConfigMigration, bool) {
	for _, m := range migrations {
		if m.From == from {
			return m, true
		}
	}
	return ConfigMigration{}, false
}

// readVersion returns the version from raw. Files without a version field are
// treated as version 1 (the original schema).
func readVersion(raw map[string]any) int {
	v, ok := raw["version"]
	if !ok {
		return 1
	}
	switch n := v.(type) {
	case int:
		return n
	case int64:
		return int(n)
	case float64:
		return int(n)
	}
	return 1
}

// migrateV1toV2 lifts the single-provider config into the named-providers +
// role-chains layout. The pre-v2 config implicitly used one provider for both
// embeddings and metadata; we materialise that as a provider named "default".
func migrateV1toV2(raw map[string]any) error {
	aiRaw := mapValue(raw, "ai")
	if aiRaw == nil {
		aiRaw = map[string]any{}
	}

	providerType := stringValue(aiRaw, "provider")
	if providerType == "" {
		providerType = "litellm"
	}

	providers, embeddingModel, metadataModel, fallbackModels := buildV1Provider(aiRaw, providerType)

	embeddingsOld := mapValue(aiRaw, "embeddings")
	dimensions := intValue(embeddingsOld, "dimensions")
	if dimensions <= 0 {
		dimensions = 1536
	}
	if embeddingModel == "" {
		embeddingModel = stringValue(embeddingsOld, "model")
	}

	metadataOld := mapValue(aiRaw, "metadata")
	if metadataModel == "" {
		metadataModel = stringValue(metadataOld, "model")
	}
	temperature := floatValue(metadataOld, "temperature")
	logConversations := boolValue(metadataOld, "log_conversations")
	timeoutStr := stringValue(metadataOld, "timeout")

	if list := stringListValue(metadataOld, "fallback_models"); len(list) > 0 {
		fallbackModels = append(fallbackModels, list...)
	}
	if v := stringValue(metadataOld, "fallback_model"); v != "" {
		fallbackModels = append(fallbackModels, v)
	}

	embeddings := map[string]any{
		"dimensions": dimensions,
		"primary":    map[string]any{"provider": "default", "model": embeddingModel},
	}

	metadata := map[string]any{
		"temperature":       temperature,
		"log_conversations": logConversations,
		"primary":           map[string]any{"provider": "default", "model": metadataModel},
	}
	if timeoutStr != "" {
		metadata["timeout"] = timeoutStr
	}
	if fallbacks := chainTargets("default", fallbackModels); len(fallbacks) > 0 {
		metadata["fallbacks"] = fallbacks
	}

	raw["ai"] = map[string]any{
		"providers":  providers,
		"embeddings": embeddings,
		"metadata":   metadata,
	}
	return nil
}

func buildV1Provider(aiRaw map[string]any, providerType string) (map[string]any, string, string, []string) {
	providers := map[string]any{}
	defaultEntry := map[string]any{"type": providerType}
	embedModel := ""
	metaModel := ""
	var fallbacks []string

	switch providerType {
	case "litellm":
		block := mapValue(aiRaw, "litellm")
		copyKeys(defaultEntry, block, "base_url", "api_key")
		copyHeaders(defaultEntry, block, "request_headers")
		embedModel = stringValue(block, "embedding_model")
		metaModel = stringValue(block, "metadata_model")
		if list := stringListValue(block, "fallback_metadata_models"); len(list) > 0 {
			fallbacks = append(fallbacks, list...)
		}
		if v := stringValue(block, "fallback_metadata_model"); v != "" {
			fallbacks = append(fallbacks, v)
		}
	case "ollama":
		block := mapValue(aiRaw, "ollama")
		copyKeys(defaultEntry, block, "base_url", "api_key")
		copyHeaders(defaultEntry, block, "request_headers")
	case "openrouter":
		block := mapValue(aiRaw, "openrouter")
		copyKeys(defaultEntry, block, "base_url", "api_key", "app_name", "site_url")
		copyHeaders(defaultEntry, block, "extra_headers")
		// rename: extra_headers → request_headers
		if hdr, ok := defaultEntry["extra_headers"]; ok {
			defaultEntry["request_headers"] = hdr
			delete(defaultEntry, "extra_headers")
		}
	}

	providers["default"] = defaultEntry
	return providers, embedModel, metaModel, fallbacks
}

func chainTargets(provider string, models []string) []any {
	out := make([]any, 0, len(models))
	seen := map[string]struct{}{}
	for _, m := range models {
		if m == "" {
			continue
		}
		key := provider + "|" + m
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		out = append(out, map[string]any{"provider": provider, "model": m})
	}
	return out
}

func mapValue(raw map[string]any, key string) map[string]any {
	if raw == nil {
		return nil
	}
	v, ok := raw[key]
	if !ok {
		return nil
	}
	switch m := v.(type) {
	case map[string]any:
		return m
	case map[any]any:
		return convertAnyMap(m)
	}
	return nil
}

func convertAnyMap(in map[any]any) map[string]any {
	out := make(map[string]any, len(in))
	keys := make([]string, 0, len(in))
	for k, v := range in {
		ks, ok := k.(string)
		if !ok {
			continue
		}
		keys = append(keys, ks)
		out[ks] = v
	}
	sort.Strings(keys)
	return out
}

func stringValue(raw map[string]any, key string) string {
	if raw == nil {
		return ""
	}
	v, ok := raw[key]
	if !ok {
		return ""
	}
	if s, ok := v.(string); ok {
		return s
	}
	return ""
}

func intValue(raw map[string]any, key string) int {
	if raw == nil {
		return 0
	}
	switch n := raw[key].(type) {
	case int:
		return n
	case int64:
		return int(n)
	case float64:
		return int(n)
	}
	return 0
}

func floatValue(raw map[string]any, key string) float64 {
	if raw == nil {
		return 0
	}
	switch n := raw[key].(type) {
	case float64:
		return n
	case int:
		return float64(n)
	case int64:
		return float64(n)
	}
	return 0
}

func boolValue(raw map[string]any, key string) bool {
	if raw == nil {
		return false
	}
	if b, ok := raw[key].(bool); ok {
		return b
	}
	return false
}

func stringListValue(raw map[string]any, key string) []string {
	if raw == nil {
		return nil
	}
	v, ok := raw[key]
	if !ok {
		return nil
	}
	list, ok := v.([]any)
	if !ok {
		return nil
	}
	out := make([]string, 0, len(list))
	for _, item := range list {
		if s, ok := item.(string); ok && s != "" {
			out = append(out, s)
		}
	}
	return out
}

func copyKeys(dst, src map[string]any, keys ...string) {
	if src == nil {
		return
	}
	for _, k := range keys {
		if v, ok := src[k]; ok {
			dst[k] = v
		}
	}
}

func copyHeaders(dst, src map[string]any, key string) {
	if src == nil {
		return
	}
	v, ok := src[key]
	if !ok {
		return
	}
	switch headers := v.(type) {
	case map[string]any:
		if len(headers) == 0 {
			return
		}
		dst[key] = headers
	case map[any]any:
		if len(headers) == 0 {
			return
		}
		dst[key] = convertAnyMap(headers)
	}
}
