// PicoClaw - Ultra-lightweight personal AI agent
// License: MIT
//
// Copyright (c) 2026 PicoClaw contributors

package config

import (
	"encoding/json"
	"fmt"
	"os"
	"strings"

	"gopkg.in/yaml.v3"

	"github.com/sipeed/picoclaw/pkg/logger"
)

// buildModelWithProtocol constructs a model string with protocol prefix.
// If the model already contains a "/" (indicating it has a protocol prefix), it is returned as-is.
// Otherwise, the protocol prefix is added.
func buildModelWithProtocol(protocol, model string) string {
	if strings.Contains(model, "/") {
		// Model already has a protocol prefix, return as-is
		return model
	}
	return protocol + "/" + model
}

type legacyDiagnosticConfig struct {
	Version     int                    `json:"version"`
	Isolation   IsolationConfig        `json:"isolation,omitempty"`
	Agents      legacyDiagnosticAgents `json:"agents,omitempty"`
	Session     SessionConfig          `json:"session,omitempty"`
	Channels    map[string]any         `json:"channels,omitempty"`
	ChannelList ChannelsConfig         `json:"channel_list,omitempty"`
	ModelList   []map[string]any       `json:"model_list,omitempty"`
	Gateway     GatewayConfig          `json:"gateway,omitempty"`
	Hooks       HooksConfig            `json:"hooks,omitempty"`
	Tools       ToolsConfig            `json:"tools,omitempty"`
	Heartbeat   HeartbeatConfig        `json:"heartbeat,omitempty"`
	Devices     DevicesConfig          `json:"devices,omitempty"`
	Voice       VoiceConfig            `json:"voice,omitempty"`
	Bindings    json.RawMessage        `json:"bindings,omitempty"`
	Providers   json.RawMessage        `json:"providers,omitempty"`
}

type legacyDiagnosticAgents struct {
	Defaults legacyDiagnosticAgentDefaults `json:"defaults,omitempty"`
	List     []AgentConfig                 `json:"list,omitempty"`
	Dispatch *DispatchConfig               `json:"dispatch,omitempty"`
}

type legacyDiagnosticAgentDefaults struct {
	AgentDefaults
	LegacyModel string `json:"model,omitempty"`
}

func validateLegacyConfigDiagnostics(data []byte) error {
	var cfg legacyDiagnosticConfig
	return decodeJSONWithDiagnostics(data, &cfg, "config.json")
}

func migrateLegacyAgentDefaultsModel(m map[string]any) {
	agents, ok := m["agents"].(map[string]any)
	if !ok {
		return
	}
	defaults, ok := agents["defaults"].(map[string]any)
	if !ok {
		return
	}
	model, hasModel := defaults["model"]
	if !hasModel {
		return
	}
	if _, hasModelName := defaults["model_name"]; !hasModelName {
		defaults["model_name"] = model
	}
	delete(defaults, "model")
}

// loadConfigV1 loads a version 1 config (current schema)
func loadConfig(data []byte) (*Config, error) {
	cfg := DefaultConfig()
	evolutionModeExplicit := configObjectHasField(data, "evolution", "mode")
	evolutionExplicitWithoutMode := configObjectHasTopLevelField(data, "evolution") && !evolutionModeExplicit

	// Pre-scan the JSON to check how many model_list entries the user provided.
	// Go's JSON decoder reuses existing slice backing-array elements rather than
	// zero-initializing them, so fields absent from the user's JSON (e.g. api_base)
	// would silently inherit values from the DefaultConfig template at the same
	// index position. We only reset cfg.ModelList when the user actually provides
	// entries; when count is 0 we keep DefaultConfig's built-in list as fallback.
	var tmp Config
	if err := decodeJSONWithDiagnostics(data, &tmp, "config.json"); err != nil {
		return nil, err
	}
	if len(tmp.ModelList) > 0 {
		cfg.ModelList = nil
	}

	if err := decodeJSONWithDiagnostics(data, cfg, "config.json"); err != nil {
		return nil, err
	}
	if evolutionExplicitWithoutMode {
		cfg.Evolution.Mode = ""
	}
	return cfg, nil
}

func configObjectHasTopLevelField(data []byte, field string) bool {
	var raw map[string]json.RawMessage
	if err := json.Unmarshal(data, &raw); err != nil {
		return false
	}
	_, ok := raw[field]
	return ok
}

func configObjectHasField(data []byte, objectField, nestedField string) bool {
	var raw map[string]json.RawMessage
	if err := json.Unmarshal(data, &raw); err != nil {
		return false
	}
	objectData, ok := raw[objectField]
	if !ok {
		return false
	}
	var object map[string]json.RawMessage
	if err := json.Unmarshal(objectData, &object); err != nil {
		return false
	}
	_, ok = object[nestedField]
	return ok
}

func mergeAPIKeys(apiKey string, apiKeys []string) []string {
	seen := make(map[string]struct{})
	var all []string

	if k := strings.TrimSpace(apiKey); k != "" {
		if _, exists := seen[k]; !exists {
			seen[k] = struct{}{}
			all = append(all, k)
		}
	}

	for _, k := range apiKeys {
		if trimmed := strings.TrimSpace(k); trimmed != "" {
			if _, exists := seen[trimmed]; !exists {
				seen[trimmed] = struct{}{}
				all = append(all, trimmed)
			}
		}
	}

	return all
}

func compareInt(v any, expected int) bool {
	switch val := v.(type) {
	case int:
		return val == expected
	case float64:
		return val == float64(expected)
	case nil:
		return expected == 0
	default:
		return false
	}
}

// migrateV0ToV1 converts a V0 (legacy, no version field) config JSON to V1 format:
//  1. Migrates legacy providers to model_list
//  2. Migrates agents.defaults.model → agents.defaults.model_name
//  3. Sets version to 1
func migrateV0ToV1(m map[string]any) error {
	if !compareInt(m["version"], 0) {
		return fmt.Errorf("migrateV0ToV1: expected version 0, got %v", m["version"])
	}

	migrateLegacyAgentDefaultsModel(m)

	// Migrate legacy providers to model_list if no model_list exists
	if _, hasModelList := m["model_list"]; !hasModelList {
		if providers, hasProviders := m["providers"]; hasProviders {
			if provMap, ok := providers.(map[string]any); ok && !isProvidersMapEmpty(provMap) {
				// Extract user's provider and model from agents.defaults
				userProvider := ""
				userModel := ""
				if agents, ok := m["agents"].(map[string]any); ok {
					if defaults, ok := agents["defaults"].(map[string]any); ok {
						if v, ok := defaults["provider"].(string); ok {
							userProvider = v
						}
						// Check both model_name (new) and model (old) fields
						if v, ok := defaults["model_name"].(string); ok && v != "" {
							userModel = v
						} else if v, ok := defaults["model"].(string); ok && v != "" {
							userModel = v
						}
					}
				}

				modelListRaw := v0ProvidersMapToModelList(provMap, userProvider, userModel)
				if len(modelListRaw) > 0 {
					m["model_list"] = modelListRaw
				}
			}
		}
	}

	// Convert model_list api_key → api_keys
	if modelList, ok := m["model_list"].([]any); ok {
		for _, model := range modelList {
			if mVal, ok := model.(map[string]any); ok {
				if ss := toUniqueStrings(mVal["api_key"], mVal["api_keys"]); len(ss) > 0 {
					mVal["api_keys"] = ss
					delete(mVal, "api_key")
				}
			}
		}
	}

	m["version"] = 1

	return nil
}

func toUniqueStrings(s any, ss any) []string {
	set := make(map[string]struct{})

	// process s
	if str, ok := s.(string); ok && str != "" {
		set[str] = struct{}{}
	}

	// process ss as []any (JSON arrays)
	if slice, ok := ss.([]any); ok {
		for _, item := range slice {
			if str, ok := item.(string); ok && str != "" {
				set[str] = struct{}{}
			}
		}
	}

	// process ss as []string
	if slice, ok := ss.([]string); ok {
		for _, item := range slice {
			if item != "" {
				set[item] = struct{}{}
			}
		}
	}

	// map to slice
	result := make([]string, 0, len(set))
	for k := range set {
		result = append(result, k)
	}

	return result
}

// migrateV1ToV2 converts a V1 config JSON to V2 format:
//  1. Migrates legacy "mention_only" to "group_trigger.mention_only"
//  2. Infers "enabled" field for models
//  3. Sets version to 2
func migrateV1ToV2(m map[string]any) error {
	if !compareInt(m["version"], 1) {
		return fmt.Errorf("migrateV1ToV2: expected version 1, got %#v", m["version"])
	}

	// Migrate channels: move "mention_only" to "group_trigger.mention_only"
	if channels, ok := m["channels"]; ok {
		if chMap, ok := channels.(map[string]any); ok {
			for _, ch := range chMap {
				if chVal, ok := ch.(map[string]any); ok {
					if mentionOnly, hasMention := chVal["mention_only"]; hasMention {
						delete(chVal, "mention_only")
						if gt, hasGT := chVal["group_trigger"].(map[string]any); hasGT {
							gt["mention_only"] = mentionOnly
						} else {
							chVal["group_trigger"] = map[string]any{"mention_only": mentionOnly}
						}
					}
				}
			}
		}
	}

	// Infer "enabled" field for models matching configV1.migrateModelEnabled behavior
	if modelList, ok := m["model_list"].([]any); ok {
		// Convert api_key → api_keys for each model
		for _, model := range modelList {
			if mVal, ok := model.(map[string]any); ok {
				if ss := toUniqueStrings(mVal["api_key"], mVal["api_keys"]); len(ss) > 0 {
					mVal["api_keys"] = ss
					delete(mVal, "api_key")
				}
			}
		}

		// Infer enabled status
		for _, model := range modelList {
			if mVal, ok := model.(map[string]any); ok {
				// Skip if explicitly set
				if _, hasEnabled := mVal["enabled"]; hasEnabled {
					continue
				}
				// Models with API keys are considered enabled
				if apiKeys, hasAPIKeys := mVal["api_keys"]; hasAPIKeys {
					// Check for []any or []string
					hasKeys := false
					if keys, ok := apiKeys.([]any); ok {
						hasKeys = len(keys) > 0
					} else if keys, ok := apiKeys.([]string); ok {
						hasKeys = len(keys) > 0
					}
					if hasKeys {
						mVal["enabled"] = true
						continue
					}
				}
				// The reserved "local-model" entry is considered enabled
				if mVal["model_name"] == "local-model" {
					mVal["enabled"] = true
				}
				logger.Infof("model: %v", mVal)
			}
		}
	} else {
		logger.Warnf("model_list is not a slice: %#v", m["model_list"])
	}

	m["version"] = 2

	return nil
}

// migrateV2ToV3 converts a V2 config JSON to V3 format:
//  1. Renames "channels" key to "channel_list"
//  2. Converts flat-format channel entries to nested format (wrapping
//     channel-specific fields in "settings")
//  3. Sets version to 3
func migrateV2ToV3(m map[string]any) error {
	if !compareInt(m["version"], 2) {
		return fmt.Errorf("migrateV2ToV3: expected version 2, got %v", m["version"])
	}

	migrateLegacyAgentDefaultsModel(m)
	delete(m, "bindings")

	// Rename channels → channel_list
	if channels, ok := m["channels"]; ok {
		delete(m, "channels")

		// Convert each channel from flat to nested format
		if chMap, ok := channels.(map[string]any); ok {
			for k, ch := range chMap {
				if chVal, ok := ch.(map[string]any); ok {
					chVal["type"] = k
					// If already has "settings" key, leave as-is
					if _, hasSettings := chVal["settings"]; hasSettings {
						continue
					}

					// Migrate Onebot "group_trigger_prefix" → "group_trigger.prefixes"
					if gtp, hasGTP := chVal["group_trigger_prefix"]; hasGTP {
						if gt, hasGT := chVal["group_trigger"].(map[string]any); hasGT {
							if _, hasPrefixes := gt["prefixes"]; !hasPrefixes {
								gt["prefixes"] = gtp
							}
						} else {
							chVal["group_trigger"] = map[string]any{"prefixes": gtp}
						}
						delete(chVal, "group_trigger_prefix")
					}

					// Separate channel-specific fields into "settings"
					settings := make(map[string]any)
					for fieldKey, v := range chVal {
						if _, exists := BaseFieldNames[fieldKey]; !exists {
							settings[fieldKey] = v
							delete(chVal, fieldKey)
						}
					}
					if len(settings) > 0 {
						chVal["settings"] = settings
					}
				}
			}
		}

		m["channel_list"] = channels
	}

	m["version"] = CurrentVersion

	return nil
}

func loadConfigMap(path string) (map[string]any, error) {
	var m1, m2 map[string]any
	data, err := os.ReadFile(path)
	if err != nil {
		if os.IsNotExist(err) {
			return m1, nil
		}
		return nil, fmt.Errorf("failed to read config: %w", err)
	}
	if err = json.Unmarshal(data, &m1); err != nil {
		return nil, wrapJSONError(data, err, "config.json")
	}
	secPath := securityPath(path)
	data, err = os.ReadFile(secPath)
	if err != nil {
		if os.IsNotExist(err) {
			return m1, nil
		}
		return nil, fmt.Errorf("failed to read security config: %w", err)
	}
	if err = yaml.Unmarshal(data, &m2); err != nil {
		return nil, fmt.Errorf("failed to parse security config: %w", err)
	}
	if m2["web"] != nil || m2["skills"] != nil {
		m3 := make(map[string]any)
		if m2["web"] != nil {
			m3["web"] = m2["web"]
			delete(m2, "web")
		}
		if m2["skills"] != nil {
			m3["skills"] = m2["skills"]
			delete(m2, "skills")
			if m, ok := m3["skills"].(map[string]any); ok {
				if m["clawhub"] != nil {
					m["registries"] = map[string]any{"clawhub": m["clawhub"]}
					delete(m, "clawhub")
				}
				if gh, ok := m["github"].(map[string]any); ok {
					registries, _ := m["registries"].(map[string]any)
					if registries == nil {
						registries = map[string]any{}
					}
					githubRegistry := map[string]any{}
					for k, v := range gh {
						githubRegistry[k] = v
					}
					if token, ok := githubRegistry["token"]; ok {
						githubRegistry["auth_token"] = token
					}
					registries["github"] = githubRegistry
					m["registries"] = registries
				}
			}
		}
		m2["tools"] = m3
	}

	// Handle model_list merging specially: m1 has array format, m2 has map format
	if mainML, hasMainML := m1["model_list"]; hasMainML {
		if secML, hasSecML := m2["model_list"]; hasSecML {
			if secMap, ok := secML.(map[string]any); ok {
				// JSON unmarshals arrays as []any, convert to []map[string]any
				var mainArr []any
				if rawArr, ok := mainML.([]any); ok {
					mainArr = make([]any, 0, len(rawArr))
					for _, item := range rawArr {
						if mVal, ok := item.(map[string]any); ok {
							mainArr = append(mainArr, mVal)
						}
					}
				}
				if len(mainArr) > 0 {
					// Merge array-style with map-style in-place
					err = mergeModelListsWithMap(mainArr, secMap)
					if err != nil {
						logger.Errorf("mergeModelListsWithMap error: %v", err)
						return nil, err
					}
					m1["model_list"] = mainArr
				}
			}
		}
	}
	// Remove model_list from m2 so mergeMap doesn't override the array with map
	delete(m2, "model_list")

	m := mergeMap(m1, m2)
	return m, nil
}

// mergeModelListsWithMap merges array-style model_list with map-style security model_list.
// It generates indexed keys from model_name (like toNameIndex) and uses them
// to look up security entries, falling back to ModelName if the indexed key doesn't exist.
func mergeModelListsWithMap(mainML []any, secML map[string]any) error {
	// Build indexed keys like toNameIndex does
	indexedKeys := make(map[string]int)
	countMap := make(map[string]int)
	for i, m := range mainML {
		if mVal, ok := m.(map[string]any); ok {
			if name, hasName := mVal["model_name"]; hasName {
				nameStr := name.(string)
				index := countMap[nameStr]
				indexedKeys[fmt.Sprintf("%s:%d", nameStr, index)] = i
				if _, ok := indexedKeys[nameStr]; !ok {
					indexedKeys[nameStr] = i
				}
				countMap[nameStr]++
			} else {
				return fmt.Errorf("model_name is required: %#v", mVal)
			}
		}
	}

	for k, v := range secML {
		if i, ok := indexedKeys[k]; ok {
			if vv, ok := v.(map[string]any); ok {
				if mVal, ok := mainML[i].(map[string]any); ok {
					mVal["api_keys"] = vv["api_keys"]
				}
			}
		} else {
			logger.Warnf("model_name not found in main config: %s", k)
		}
		delete(secML, k)
	}

	return nil
}
