package assistant

import (
	"fmt"
	"os"
	"path/filepath"
	"strings"

	jsoniter "github.com/json-iterator/go"
	"github.com/spf13/cast"
	"github.com/yaoapp/gou/application"
	"github.com/yaoapp/gou/fs"
	"github.com/yaoapp/yao/agent/context"
	"github.com/yaoapp/yao/agent/i18n"
	sandboxTypes "github.com/yaoapp/yao/agent/sandbox/v2/types"
	searchTypes "github.com/yaoapp/yao/agent/search/types"
	store "github.com/yaoapp/yao/agent/store/types"
	"github.com/yaoapp/yao/config"
	"gopkg.in/yaml.v3"
)

// loaded the loaded assistant
var loaded = NewCache(200) // 200 is the default capacity
var storage store.Store = nil
var storeSetting *store.Setting = nil            // store setting from agent.yml
var defaultConnector string = ""                 // default connector
var globalUses *context.Uses = nil               // global uses configuration from agent.yml
var globalPrompts []store.Prompt = nil           // global prompts from agent/prompts.yml
var globalKBSetting *store.KBSetting = nil       // global KB setting from agent/kb.yml
var globalSearchConfig *searchTypes.Config = nil // global search config from agent/search.yml

// LoadBuiltIn load the built-in assistants
func LoadBuiltIn() error {

	// Clear non-system agents from cache (preserve system agents loaded by LoadSystemAgents)
	loaded.ClearExcept(func(id string) bool {
		return strings.HasPrefix(id, "__yao.") // Keep system agents
	})

	root := `/assistants`
	app, err := fs.Get("app")
	if err != nil {
		return err
	}

	// Get all existing built-in assistants
	deletedBuiltIn := map[string]bool{}

	// Remove the built-in assistants (exclude system agents with __yao. prefix)
	if storage != nil {

		builtIn := true
		res, err := storage.GetAssistants(store.AssistantFilter{BuiltIn: &builtIn, Select: []string{"assistant_id", "id"}})
		if err != nil {
			return err
		}

		// Get all existing built-in assistants (exclude system agents)
		for _, assistant := range res.Data {
			// Skip system agents (they are managed by LoadSystemAgents)
			if strings.HasPrefix(assistant.ID, "__yao.") {
				continue
			}
			deletedBuiltIn[assistant.ID] = true
		}
	}

	// Check if the assistant is built-in
	if exists, _ := app.Exists(root); !exists {
		return nil
	}

	paths, err := app.ReadDir(root, true)
	if err != nil {
		return err
	}

	sort := 1
	for _, path := range paths {
		pkgfile := filepath.Join(path, "package.yao")
		if has, _ := app.Exists(pkgfile); !has {
			continue
		}

		assistant, err := LoadPath(path)
		if err != nil {
			return err
		}

		assistant.Readonly = true
		assistant.BuiltIn = true
		if assistant.Sort == 0 {
			assistant.Sort = sort
		}
		if assistant.Tags == nil {
			assistant.Tags = []string{}
		}

		// Save the assistant
		err = assistant.Save()
		if err != nil {
			return err
		}

		// Initialize the assistant
		err = assistant.initialize()
		if err != nil {
			return err
		}

		sort++
		loaded.Put(assistant)

		// Remove the built-in assistant from the store
		delete(deletedBuiltIn, assistant.ID)
	}

	// Remove deleted built-in assistants
	if len(deletedBuiltIn) > 0 {
		assistantIDs := []string{}
		for assistantID := range deletedBuiltIn {
			assistantIDs = append(assistantIDs, assistantID)
		}
		_, err := storage.DeleteAssistants(store.AssistantFilter{AssistantIDs: assistantIDs})
		if err != nil {
			return err
		}
	}

	return nil
}

// SetStorage set the storage
func SetStorage(s store.Store) {
	storage = s
}

// GetStorage returns the storage (for testing purposes)
func GetStorage() store.Store {
	return storage
}

// SetConnector set the connector
func SetConnector(c string) {
	defaultConnector = c
}

// SetGlobalUses set the global uses configuration
func SetGlobalUses(uses *context.Uses) {
	globalUses = uses
}

// SetGlobalPrompts set the global prompts from agent/prompts.yml
func SetGlobalPrompts(prompts []store.Prompt) {
	globalPrompts = prompts
}

// SetStoreSetting set the store setting from agent.yml
func SetStoreSetting(setting *store.Setting) {
	storeSetting = setting
}

// GetStoreSetting returns the store setting
func GetStoreSetting() *store.Setting {
	return storeSetting
}

// GetGlobalPrompts returns the global prompts with variables parsed
// ctx: context variables for parsing $CTX.* variables
func GetGlobalPrompts(ctx map[string]string) []store.Prompt {
	if len(globalPrompts) == 0 {
		return nil
	}
	return store.Prompts(globalPrompts).Parse(ctx)
}

// SetGlobalKBSetting set the global KB setting from agent/kb.yml
func SetGlobalKBSetting(kbSetting *store.KBSetting) {
	globalKBSetting = kbSetting
}

// GetGlobalKBSetting returns the global KB setting
func GetGlobalKBSetting() *store.KBSetting {
	return globalKBSetting
}

// SetGlobalSearchConfig set the global search config from agent/search.yml
func SetGlobalSearchConfig(config *searchTypes.Config) {
	globalSearchConfig = config
}

// GetGlobalSearchConfig returns the global search config
func GetGlobalSearchConfig() *searchTypes.Config {
	return globalSearchConfig
}

// SetCache set the cache
func SetCache(capacity int) {
	ClearCache()
	loaded = NewCache(capacity)
}

// ClearCache clear the cache
func ClearCache() {
	if loaded != nil {
		loaded.Clear()
		loaded = nil
	}
}

// GetCache returns the loaded cache
func GetCache() *Cache {
	return loaded
}

// LoadStore create a new assistant from store
func LoadStore(id string) (*Assistant, error) {

	if id == "" {
		return nil, fmt.Errorf("assistant_id is required")
	}

	assistant, exists := loaded.Get(id)
	if exists {
		return assistant, nil
	}

	if storage == nil {
		return nil, fmt.Errorf("storage is not set")
	}

	// Request all fields when loading assistant from store
	storeModel, err := storage.GetAssistant(id, store.AssistantFullFields)
	if err != nil {
		return nil, err
	}

	// Load from path
	if storeModel.Path != "" {
		assistant, err = LoadPath(storeModel.Path)
		if err != nil {
			return nil, err
		}
		loaded.Put(assistant)
		return assistant, nil
	}

	// Create assistant from store model
	assistant = &Assistant{AssistantModel: *storeModel}

	// Load script from source field if present
	if assistant.Source != "" {
		script, err := loadSource(assistant.Source, assistant.ID)
		if err != nil {
			return nil, err
		}
		assistant.HookScript = script
	}

	// Initialize the assistant
	err = assistant.initialize()
	if err != nil {
		return nil, err
	}

	loaded.Put(assistant)
	return assistant, nil
}

// loadPackage loads and parses the package.yao file
func loadPackage(path string) (map[string]interface{}, error) {
	app, err := fs.Get("app")
	if err != nil {
		return nil, err
	}

	pkgfile := filepath.Join(path, "package.yao")
	if has, _ := app.Exists(pkgfile); !has {
		return nil, fmt.Errorf("package.yao not found in %s", path)
	}

	pkgraw, err := app.ReadFile(pkgfile)
	if err != nil {
		return nil, err
	}

	var data map[string]interface{}
	err = application.Parse(pkgfile, pkgraw, &data)
	if err != nil {
		return nil, err
	}

	// Process connector environment variable
	if connector, ok := data["connector"].(string); ok {
		if strings.HasPrefix(connector, "$ENV.") {
			envKey := strings.TrimPrefix(connector, "$ENV.")
			if envValue := os.Getenv(envKey); envValue != "" {
				data["connector"] = envValue
			}
		}
	}

	return data, nil
}

// LoadPath load assistant from path
func LoadPath(path string) (*Assistant, error) {
	app, err := fs.Get("app")
	if err != nil {
		return nil, err
	}

	data, err := loadPackage(path)
	if err != nil {
		return nil, err
	}

	// assistant_id
	id := strings.ReplaceAll(strings.TrimPrefix(path, "/assistants/"), "/", ".")
	data["assistant_id"] = id
	data["path"] = path
	if _, has := data["type"]; !has {
		data["type"] = "assistant"
	}

	updatedAt := int64(0)

	// prompts (default prompts from prompts.yml)
	promptsfile := filepath.Join(path, "prompts.yml")
	if has, _ := app.Exists(promptsfile); has {
		prompts, ts, err := store.LoadPrompts(promptsfile, path)
		if err != nil {
			return nil, err
		}
		data["prompts"] = prompts
		data["updated_at"] = ts
		updatedAt = ts
	}

	// prompt_presets (from prompts directory, key is filename without extension)
	promptsDir := filepath.Join(path, "prompts")
	if has, _ := app.Exists(promptsDir); has {
		presets, ts, err := store.LoadPromptPresets(promptsDir, path)
		if err != nil {
			return nil, err
		}
		if len(presets) > 0 {
			data["prompt_presets"] = presets
			updatedAt = max(updatedAt, ts)
		}
	}

	// load scripts (hook script and other scripts) from src directory
	srcDir := filepath.Join(path, "src")
	if has, _ := app.Exists(srcDir); has {
		hookScript, scripts, err := LoadScripts(srcDir)
		if err != nil {
			return nil, err
		}

		// Set hook script and update timestamp
		if hookScript != nil {
			data["script"] = hookScript
			// Get timestamp from index.ts if exists
			scriptfile := filepath.Join(srcDir, "index.ts")
			if ts, err := app.ModTime(scriptfile); err == nil {
				data["updated_at"] = max(updatedAt, ts.UnixNano())
			}
		}

		// Set other scripts
		if len(scripts) > 0 {
			data["scripts"] = scripts
		}
	}

	// i18ns
	locales, err := i18n.GetLocales(path)
	if err != nil {
		return nil, err
	}
	data["locales"] = locales

	// V2 sandbox: load standalone sandbox.yao if present (Path A).
	sandboxFile := filepath.Join(path, "sandbox.yao")
	if has, _ := app.Exists(sandboxFile); has {
		absFile := filepath.Join(config.Conf.AppSource, sandboxFile)
		sbCfg, sbErr := store.LoadSandboxConfig(absFile)
		if sbErr != nil {
			return nil, fmt.Errorf("load sandbox.yao: %w", sbErr)
		}
		data["__sandbox_v2"] = sbCfg
		data["sandbox"] = sbCfg
	}

	ast, err := loadMap(data)
	if err != nil {
		return nil, err
	}

	// If V2 sandbox was loaded via Path A, assign it now.
	if sbCfg, ok := data["__sandbox_v2"].(*sandboxTypes.SandboxConfig); ok && sbCfg != nil {
		ast.SandboxV2 = sbCfg
	}

	// Extract Sandbox flag and ComputerFilter from V2 sandbox config.
	if ast.SandboxV2 != nil {
		ast.IsSandbox = true
		ast.ComputerFilter = ast.SandboxV2.Filter
	}

	// Compute config hash for V2 sandbox.
	if ast.SandboxV2 != nil {
		var mcpServers []store.MCPServerConfig
		if ast.MCP != nil {
			mcpServers = ast.MCP.Servers
		}
		skillsDir := ""
		if ast.Path != "" {
			dir := filepath.Join(config.Conf.AppSource, ast.Path, "skills")
			if info, e := os.Stat(dir); e == nil && info.IsDir() {
				skillsDir = dir
			}
		}
		ast.ConfigHash = store.ComputeConfigHash(ast.SandboxV2, mcpServers, skillsDir)
	}

	return ast, nil
}

func loadMap(data map[string]interface{}) (*Assistant, error) {

	assistant := &Assistant{}

	// assistant_id is required
	id, ok := data["assistant_id"].(string)
	if !ok {
		return nil, fmt.Errorf("assistant_id is required")
	}
	assistant.ID = id

	// name is required
	name, ok := data["name"].(string)
	if !ok {
		return nil, fmt.Errorf("name is required")
	}
	assistant.Name = name

	// avatar
	if avatar, ok := data["avatar"].(string); ok {
		assistant.Avatar = avatar
	}

	// Type
	if v, ok := data["type"].(string); ok {
		assistant.Type = v
	}

	// Placeholder
	if v, ok := data["placeholder"]; ok {

		switch vv := v.(type) {
		case string:
			placeholder, err := jsoniter.Marshal(vv)
			if err != nil {
				return nil, err
			}
			assistant.Placeholder = &store.Placeholder{}
			err = jsoniter.Unmarshal(placeholder, assistant.Placeholder)
			if err != nil {
				return nil, err
			}

		case map[string]interface{}:
			raw, err := jsoniter.Marshal(vv)
			if err != nil {
				return nil, err
			}

			assistant.Placeholder = &store.Placeholder{}
			err = jsoniter.Unmarshal(raw, assistant.Placeholder)
			if err != nil {
				return nil, err
			}

		case *store.Placeholder:
			assistant.Placeholder = vv

		case nil:
			assistant.Placeholder = nil
		}
	}

	// Mentionable
	if v, ok := data["mentionable"].(bool); ok {
		assistant.Mentionable = v
	}

	// Automated
	if v, ok := data["automated"].(bool); ok {
		assistant.Automated = v
	}

	// modes
	if v, has := data["modes"]; has {
		modes, err := store.ToModes(v)
		if err != nil {
			return nil, err
		}
		assistant.Modes = modes
	}

	// default_mode
	if v, ok := data["default_mode"].(string); ok {
		assistant.DefaultMode = v
	}

	// DisableGlobalPrompts
	if v, ok := data["disable_global_prompts"].(bool); ok {
		assistant.DisableGlobalPrompts = v
	}

	// Readonly
	if v, ok := data["readonly"].(bool); ok {
		assistant.Readonly = v
	}

	// Public
	if v, ok := data["public"].(bool); ok {
		assistant.Public = v
	}

	// Share
	if v, ok := data["share"].(string); ok {
		assistant.Share = v
	}

	// built_in
	if v, ok := data["built_in"].(bool); ok {
		assistant.BuiltIn = v
	}

	// sort
	if v, has := data["sort"]; has {
		assistant.Sort = cast.ToInt(v)
	}

	// path
	if v, ok := data["path"].(string); ok {
		assistant.Path = v
	}

	// connector
	if connector, ok := data["connector"].(string); ok {
		assistant.Connector = connector
	}

	// connector_options
	if connOpts, has := data["connector_options"]; has {
		opts, err := store.ToConnectorOptions(connOpts)
		if err != nil {
			return nil, err
		}
		assistant.ConnectorOptions = opts
	}

	// tags
	if v, has := data["tags"]; has {
		switch vv := v.(type) {
		case []string:
			assistant.Tags = vv
		case []interface{}:
			var tags []string
			for _, tag := range vv {
				tags = append(tags, cast.ToString(tag))
			}
			assistant.Tags = tags

		case string:
			assistant.Tags = []string{vv}

		case interface{}:
			raw, err := jsoniter.Marshal(vv)
			if err != nil {
				return nil, err
			}
			var tags []string
			err = jsoniter.Unmarshal(raw, &tags)
			if err != nil {
				return nil, err
			}
			assistant.Tags = tags

		}
	}

	// options
	if v, ok := data["options"].(map[string]interface{}); ok {
		assistant.Options = v
	}

	// description
	if v, ok := data["description"].(string); ok {
		assistant.Description = v
	}

	// capabilities
	if v, ok := data["capabilities"].(string); ok {
		assistant.Capabilities = v
	}

	// locales
	if locales, ok := data["locales"].(i18n.Map); ok {
		assistant.Locales = locales
		flattened := locales.FlattenWithGlobal()

		// Auto-inject assistant name and description into all locales
		// so that {{name}} and {{description}} templates can be resolved
		for locale, i18nObj := range flattened {
			if i18nObj.Messages == nil {
				i18nObj.Messages = make(map[string]any)
			}
			// Add name, description, and capabilities if not already present
			if _, exists := i18nObj.Messages["name"]; !exists && assistant.Name != "" {
				i18nObj.Messages["name"] = assistant.Name
			}
			if _, exists := i18nObj.Messages["description"]; !exists && assistant.Description != "" {
				i18nObj.Messages["description"] = assistant.Description
			}
			if _, exists := i18nObj.Messages["capabilities"]; !exists && assistant.Capabilities != "" {
				i18nObj.Messages["capabilities"] = assistant.Capabilities
			}
			flattened[locale] = i18nObj
		}

		i18n.Locales[id] = flattened
	} else {
		// No locales defined, create default with name, description, and capabilities for all common locales
		if assistant.Name != "" || assistant.Description != "" || assistant.Capabilities != "" {
			defaultLocales := make(map[string]i18n.I18n)
			commonLocales := []string{"en", "en-us", "zh", "zh-cn", "zh-tw"}
			for _, locale := range commonLocales {
				messages := map[string]any{}
				if assistant.Name != "" {
					messages["name"] = assistant.Name
				}
				if assistant.Description != "" {
					messages["description"] = assistant.Description
				}
				if assistant.Capabilities != "" {
					messages["capabilities"] = assistant.Capabilities
				}
				defaultLocales[locale] = i18n.I18n{
					Locale:   locale,
					Messages: messages,
				}
			}
			i18n.Locales[id] = defaultLocales
		}
	}

	// Search configuration (from package.yao search block)
	// This contains search options like web.max_results, kb.threshold, citation.format, etc.
	// Merge hierarchy: global config < assistant config
	switch v := data["search"].(type) {

	case *searchTypes.Config:
		assistant.Search = v

	case searchTypes.Config:
		assistant.Search = &v

	case map[string]interface{}:
		var assistantSearch searchTypes.Config
		raw, err := jsoniter.Marshal(v)
		if err != nil {
			return nil, err
		}
		err = jsoniter.Unmarshal(raw, &assistantSearch)
		if err != nil {
			return nil, err
		}
		// Merge with global search config
		assistant.Search = mergeSearchConfig(globalSearchConfig, &assistantSearch)

	default:
		assistant.Search = globalSearchConfig
	}

	// prompts
	if prompts, has := data["prompts"]; has {

		switch v := prompts.(type) {
		case []store.Prompt:
			assistant.Prompts = v

		case string:
			var prompts []store.Prompt
			err := yaml.Unmarshal([]byte(v), &prompts)
			if err != nil {
				return nil, err
			}
			assistant.Prompts = prompts

		default:
			raw, err := jsoniter.Marshal(v)
			if err != nil {
				return nil, err
			}

			var prompts []store.Prompt
			err = jsoniter.Unmarshal(raw, &prompts)
			if err != nil {
				return nil, err
			}
			assistant.Prompts = prompts
		}
	}

	// prompt_presets
	if presets, has := data["prompt_presets"]; has {
		promptPresets, err := store.ToPromptPresets(presets)
		if err != nil {
			return nil, err
		}
		assistant.PromptPresets = promptPresets
	}

	// source (hook script code) - store the source code
	if source, ok := data["source"].(string); ok {
		assistant.Source = source
	}

	// kb
	if kb, has := data["kb"]; has {
		knowledgeBase, err := store.ToKnowledgeBase(kb)
		if err != nil {
			return nil, err
		}
		assistant.KB = knowledgeBase
	}

	// db
	if db, has := data["db"]; has {
		database, err := store.ToDatabase(db)
		if err != nil {
			return nil, err
		}
		assistant.DB = database
	}

	// mcp
	if mcp, has := data["mcp"]; has {
		mcpServers, err := store.ToMCPServers(mcp)
		if err != nil {
			return nil, err
		}
		assistant.MCP = mcpServers
	}

	// workflow
	if workflow, has := data["workflow"]; has {
		wf, err := store.ToWorkflow(workflow)
		if err != nil {
			return nil, err
		}
		assistant.Workflow = wf
	}

	// sandbox (for coding agents like Claude CLI, Cursor CLI)
	// V2 sandbox via independent sandbox.yao is loaded in LoadPath (below).
	// This block handles the package.yao embedded "sandbox" field with version dispatch.
	if assistant.SandboxV2 == nil {
		if sandbox, has := data["sandbox"]; has {
			version := extractSandboxVersion(sandbox)
			if version == sandboxTypes.SandboxVersionV2 {
				sb, err := store.ToSandboxV2(sandbox)
				if err != nil {
					return nil, err
				}
				assistant.SandboxV2 = sb
				assistant.IsSandbox = true
				assistant.ComputerFilter = sb.Filter
			} else {
				sb, err := store.ToSandbox(sandbox)
				if err != nil {
					return nil, err
				}
				assistant.Sandbox = sb
			}
		}
	}

	// dependencies (name -> version constraint, like npm dependencies)
	if deps, has := data["dependencies"]; has {
		switch v := deps.(type) {
		case map[string]string:
			assistant.Dependencies = v
		case map[string]interface{}:
			d := make(map[string]string, len(v))
			for k, val := range v {
				d[k] = cast.ToString(val)
			}
			assistant.Dependencies = d
		default:
			raw, err := jsoniter.Marshal(v)
			if err != nil {
				return nil, err
			}
			var d map[string]string
			if err := jsoniter.Unmarshal(raw, &d); err != nil {
				return nil, err
			}
			assistant.Dependencies = d
		}
	}

	// uses (wrapper configurations for vision, audio, etc.)
	// Merge hierarchy: global uses < assistant uses
	if uses, has := data["uses"]; has {
		var assistantUses *context.Uses
		switch v := uses.(type) {
		case *context.Uses:
			assistantUses = v
		case context.Uses:
			assistantUses = &v
		default:
			raw, err := jsoniter.Marshal(v)
			if err != nil {
				return nil, err
			}
			var usesConfig context.Uses
			err = jsoniter.Unmarshal(raw, &usesConfig)
			if err != nil {
				return nil, err
			}
			assistantUses = &usesConfig
		}
		// Merge with global uses
		assistant.Uses = mergeUses(globalUses, assistantUses)
	} else if globalUses != nil {
		// No assistant-specific uses, use global
		assistant.Uses = globalUses
	}

	// Load scripts (hook script and other scripts)
	hookScript, scripts, scriptErr := LoadScriptsFromData(data, assistant.ID)
	if scriptErr != nil {
		return nil, scriptErr
	}
	assistant.HookScript = hookScript
	assistant.Scripts = scripts

	// created_at
	if v, has := data["created_at"]; has {
		ts, err := getTimestamp(v)
		if err != nil {
			return nil, err
		}
		assistant.CreatedAt = ts
	}

	// updated_at
	if v, has := data["updated_at"]; has {
		ts, err := getTimestamp(v)
		if err != nil {
			return nil, err
		}
		assistant.UpdatedAt = ts
	}

	// Initialize the assistant
	err := assistant.initialize()
	if err != nil {
		return nil, err
	}

	return assistant, nil
}

// Init init the assistant
// Choose the connector and initialize the assistant
func (ast *Assistant) initialize() error {

	conn := defaultConnector
	if ast.Connector != "" {
		conn = ast.Connector
	}
	ast.Connector = conn

	// Register scripts as process handlers
	if len(ast.Scripts) > 0 {
		if err := ast.RegisterScripts(); err != nil {
			return fmt.Errorf("failed to register scripts: %w", err)
		}
	}

	return nil
}

// mergeUses merges two Uses configs (base < override)
func mergeUses(base, override *context.Uses) *context.Uses {
	if base == nil {
		return override
	}
	if override == nil {
		return base
	}

	result := *base // Copy base

	// Override with non-empty values
	if override.Vision != "" {
		result.Vision = override.Vision
	}
	if override.Audio != "" {
		result.Audio = override.Audio
	}
	if override.Search != "" {
		result.Search = override.Search
	}
	if override.Fetch != "" {
		result.Fetch = override.Fetch
	}
	if override.Web != "" {
		result.Web = override.Web
	}
	if override.Keyword != "" {
		result.Keyword = override.Keyword
	}
	if override.QueryDSL != "" {
		result.QueryDSL = override.QueryDSL
	}
	if override.Rerank != "" {
		result.Rerank = override.Rerank
	}

	return &result
}

// mergeSearchConfig merges two search configs (base < override)
func mergeSearchConfig(base, override *searchTypes.Config) *searchTypes.Config {
	if base == nil {
		return override
	}
	if override == nil {
		return base
	}

	result := *base // Copy base

	// Merge Web config
	if override.Web != nil {
		if result.Web == nil {
			result.Web = override.Web
		} else {
			merged := *result.Web
			if override.Web.Provider != "" {
				merged.Provider = override.Web.Provider
			}
			if override.Web.APIKeyEnv != "" {
				merged.APIKeyEnv = override.Web.APIKeyEnv
			}
			if override.Web.MaxResults > 0 {
				merged.MaxResults = override.Web.MaxResults
			}
			result.Web = &merged
		}
	}

	// Merge KB config
	if override.KB != nil {
		if result.KB == nil {
			result.KB = override.KB
		} else {
			merged := *result.KB
			if len(override.KB.Collections) > 0 {
				merged.Collections = override.KB.Collections
			}
			if override.KB.Threshold > 0 {
				merged.Threshold = override.KB.Threshold
			}
			if override.KB.Graph {
				merged.Graph = override.KB.Graph
			}
			result.KB = &merged
		}
	}

	// Merge DB config
	if override.DB != nil {
		if result.DB == nil {
			result.DB = override.DB
		} else {
			merged := *result.DB
			if len(override.DB.Models) > 0 {
				merged.Models = override.DB.Models
			}
			if override.DB.MaxResults > 0 {
				merged.MaxResults = override.DB.MaxResults
			}
			result.DB = &merged
		}
	}

	// Merge Keyword config
	if override.Keyword != nil {
		if result.Keyword == nil {
			result.Keyword = override.Keyword
		} else {
			merged := *result.Keyword
			if override.Keyword.MaxKeywords > 0 {
				merged.MaxKeywords = override.Keyword.MaxKeywords
			}
			if override.Keyword.Language != "" {
				merged.Language = override.Keyword.Language
			}
			result.Keyword = &merged
		}
	}

	// Merge QueryDSL config
	if override.QueryDSL != nil {
		if result.QueryDSL == nil {
			result.QueryDSL = override.QueryDSL
		} else {
			merged := *result.QueryDSL
			if override.QueryDSL.Strict {
				merged.Strict = override.QueryDSL.Strict
			}
			result.QueryDSL = &merged
		}
	}

	// Merge Rerank config
	if override.Rerank != nil {
		if result.Rerank == nil {
			result.Rerank = override.Rerank
		} else {
			merged := *result.Rerank
			if override.Rerank.TopN > 0 {
				merged.TopN = override.Rerank.TopN
			}
			result.Rerank = &merged
		}
	}

	// Merge Citation config
	if override.Citation != nil {
		if result.Citation == nil {
			result.Citation = override.Citation
		} else {
			merged := *result.Citation
			if override.Citation.Format != "" {
				merged.Format = override.Citation.Format
			}
			// AutoInjectPrompt is a bool, so we check if it's explicitly set
			// by checking if the whole Citation block was provided
			merged.AutoInjectPrompt = override.Citation.AutoInjectPrompt
			if override.Citation.CustomPrompt != "" {
				merged.CustomPrompt = override.Citation.CustomPrompt
			}
			result.Citation = &merged
		}
	}

	// Merge Weights config
	if override.Weights != nil {
		if result.Weights == nil {
			result.Weights = override.Weights
		} else {
			merged := *result.Weights
			if override.Weights.User > 0 {
				merged.User = override.Weights.User
			}
			if override.Weights.Hook > 0 {
				merged.Hook = override.Weights.Hook
			}
			if override.Weights.Auto > 0 {
				merged.Auto = override.Weights.Auto
			}
			result.Weights = &merged
		}
	}

	// Merge Options config
	if override.Options != nil {
		if result.Options == nil {
			result.Options = override.Options
		} else {
			merged := *result.Options
			if override.Options.SkipThreshold > 0 {
				merged.SkipThreshold = override.Options.SkipThreshold
			}
			result.Options = &merged
		}
	}

	return &result
}

// extractSandboxVersion tries to read the "version" field from a sandbox config value.
func extractSandboxVersion(v any) string {
	switch sb := v.(type) {
	case *sandboxTypes.SandboxConfig:
		if sb != nil {
			return sb.Version
		}
	case sandboxTypes.SandboxConfig:
		return sb.Version
	case map[string]any:
		if ver, ok := sb["version"].(string); ok {
			return ver
		}
	}
	return ""
}
