package config

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"sync/atomic"

	"github.com/caarlos0/env/v11"

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

// rrCounter is a global counter for round-robin load balancing across models.
var rrCounter atomic.Uint64

// FlexibleStringSlice is a []string that also accepts JSON numbers,
// so allow_from can contain both "123" and 123.
// It also supports parsing comma-separated strings from environment variables,
// including both English (,) and Chinese (，) commas.
type FlexibleStringSlice []string

func (f *FlexibleStringSlice) UnmarshalJSON(data []byte) error {
	// Try []string first
	var ss []string
	if err := json.Unmarshal(data, &ss); err == nil {
		*f = ss
		return nil
	}

	// Try []interface{} to handle mixed types
	var raw []any
	if err := json.Unmarshal(data, &raw); err != nil {
		return err
	}

	result := make([]string, 0, len(raw))
	for _, v := range raw {
		switch val := v.(type) {
		case string:
			result = append(result, val)
		case float64:
			result = append(result, fmt.Sprintf("%.0f", val))
		default:
			result = append(result, fmt.Sprintf("%v", val))
		}
	}
	*f = result
	return nil
}

// UnmarshalText implements encoding.TextUnmarshaler to support env variable parsing.
// It handles comma-separated values with both English (,) and Chinese (，) commas.
func (f *FlexibleStringSlice) UnmarshalText(text []byte) error {
	if len(text) == 0 {
		*f = nil
		return nil
	}

	s := string(text)
	// Replace Chinese comma with English comma, then split
	s = strings.ReplaceAll(s, "，", ",")
	parts := strings.Split(s, ",")

	result := make([]string, 0, len(parts))
	for _, part := range parts {
		part = strings.TrimSpace(part)
		if part != "" {
			result = append(result, part)
		}
	}
	*f = result
	return nil
}

// CurrentVersion is the latest config schema version
const CurrentVersion = 1

// Config is the current config structure with version support
type Config struct {
	Version   int             `json:"version"` // Config schema version for migration
	Agents    AgentsConfig    `json:"agents"`
	Bindings  []AgentBinding  `json:"bindings,omitempty"`
	Session   SessionConfig   `json:"session,omitempty"`
	Channels  ChannelsConfig  `json:"channels"`
	ModelList []*ModelConfig  `json:"model_list"` // New model-centric provider configuration
	Gateway   GatewayConfig   `json:"gateway"`
	Hooks     HooksConfig     `json:"hooks,omitempty"`
	Tools     ToolsConfig     `json:"tools"`
	Heartbeat HeartbeatConfig `json:"heartbeat"`
	Devices   DevicesConfig   `json:"devices"`
	Voice     VoiceConfig     `json:"voice"`
	// BuildInfo contains build-time version information
	BuildInfo BuildInfo `json:"build_info,omitempty"`

	security *SecurityConfig
}

func (c *Config) WithSecurity(sec *SecurityConfig) *Config {
	if sec == nil {
		c.security = sec
		return c
	}
	sec = normalizeSecurityConfig(sec)
	err := applySecurityConfig(c, sec)
	if err != nil {
		return nil
	}
	c.security = sec
	return c
}

// FilterSensitiveData filters sensitive values from content before sending to LLM.
// This prevents the LLM from seeing its own credentials.
// Uses strings.Replacer for O(n+m) performance (computed once per SecurityConfig).
// Short content (below FilterMinLength) is returned unchanged for performance.
func (c *Config) FilterSensitiveData(content string) string {
	if c.security == nil || content == "" {
		return content
	}
	// Check if filtering is enabled (default: true)
	if !c.Tools.IsFilterSensitiveDataEnabled() {
		return content
	}
	// Fast path: skip filtering for short content
	if len(content) < c.Tools.GetFilterMinLength() {
		return content
	}
	return c.security.SensitiveDataReplacer().Replace(content)
}

type HooksConfig struct {
	Enabled   bool                         `json:"enabled"`
	Defaults  HookDefaultsConfig           `json:"defaults,omitempty"`
	Builtins  map[string]BuiltinHookConfig `json:"builtins,omitempty"`
	Processes map[string]ProcessHookConfig `json:"processes,omitempty"`
}

type HookDefaultsConfig struct {
	ObserverTimeoutMS    int `json:"observer_timeout_ms,omitempty"`
	InterceptorTimeoutMS int `json:"interceptor_timeout_ms,omitempty"`
	ApprovalTimeoutMS    int `json:"approval_timeout_ms,omitempty"`
}

type BuiltinHookConfig struct {
	Enabled  bool            `json:"enabled"`
	Priority int             `json:"priority,omitempty"`
	Config   json.RawMessage `json:"config,omitempty"`
}

type ProcessHookConfig struct {
	Enabled   bool              `json:"enabled"`
	Priority  int               `json:"priority,omitempty"`
	Transport string            `json:"transport,omitempty"`
	Command   []string          `json:"command,omitempty"`
	Dir       string            `json:"dir,omitempty"`
	Env       map[string]string `json:"env,omitempty"`
	Observe   []string          `json:"observe,omitempty"`
	Intercept []string          `json:"intercept,omitempty"`
}

// BuildInfo contains build-time version information
type BuildInfo struct {
	Version   string `json:"version"`
	GitCommit string `json:"git_commit"`
	BuildTime string `json:"build_time"`
	GoVersion string `json:"go_version"`
}

// MarshalJSON implements custom JSON marshaling for Config
// to omit providers section when empty and session when empty
func (c *Config) MarshalJSON() ([]byte, error) {
	type Alias Config
	aux := &struct {
		Session *SessionConfig `json:"session,omitempty"`
		*Alias
	}{
		Alias: (*Alias)(c),
	}

	// Only include session if not empty
	if c.Session.DMScope != "" || len(c.Session.IdentityLinks) > 0 {
		aux.Session = &c.Session
	}

	return json.Marshal(aux)
}

type AgentsConfig struct {
	Defaults AgentDefaults `json:"defaults"`
	List     []AgentConfig `json:"list,omitempty"`
}

// AgentModelConfig supports both string and structured model config.
// String format: "gpt-4" (just primary, no fallbacks)
// Object format: {"primary": "gpt-4", "fallbacks": ["claude-haiku"]}
type AgentModelConfig struct {
	Primary   string   `json:"primary,omitempty"`
	Fallbacks []string `json:"fallbacks,omitempty"`
}

func (m *AgentModelConfig) UnmarshalJSON(data []byte) error {
	var s string
	if err := json.Unmarshal(data, &s); err == nil {
		m.Primary = s
		m.Fallbacks = nil
		return nil
	}
	type raw struct {
		Primary   string   `json:"primary"`
		Fallbacks []string `json:"fallbacks"`
	}
	var r raw
	if err := json.Unmarshal(data, &r); err != nil {
		return err
	}
	m.Primary = r.Primary
	m.Fallbacks = r.Fallbacks
	return nil
}

func (m AgentModelConfig) MarshalJSON() ([]byte, error) {
	if len(m.Fallbacks) == 0 && m.Primary != "" {
		return json.Marshal(m.Primary)
	}
	type raw struct {
		Primary   string   `json:"primary,omitempty"`
		Fallbacks []string `json:"fallbacks,omitempty"`
	}
	return json.Marshal(raw{Primary: m.Primary, Fallbacks: m.Fallbacks})
}

type AgentConfig struct {
	ID        string            `json:"id"`
	Default   bool              `json:"default,omitempty"`
	Name      string            `json:"name,omitempty"`
	Workspace string            `json:"workspace,omitempty"`
	Model     *AgentModelConfig `json:"model,omitempty"`
	Skills    []string          `json:"skills,omitempty"`
	Subagents *SubagentsConfig  `json:"subagents,omitempty"`
}

type SubagentsConfig struct {
	AllowAgents []string          `json:"allow_agents,omitempty"`
	Model       *AgentModelConfig `json:"model,omitempty"`
}

type PeerMatch struct {
	Kind string `json:"kind"`
	ID   string `json:"id"`
}

type BindingMatch struct {
	Channel   string     `json:"channel"`
	AccountID string     `json:"account_id,omitempty"`
	Peer      *PeerMatch `json:"peer,omitempty"`
	GuildID   string     `json:"guild_id,omitempty"`
	TeamID    string     `json:"team_id,omitempty"`
}

type AgentBinding struct {
	AgentID string       `json:"agent_id"`
	Match   BindingMatch `json:"match"`
}

type SessionConfig struct {
	DMScope       string              `json:"dm_scope,omitempty"`
	IdentityLinks map[string][]string `json:"identity_links,omitempty"`
}

// RoutingConfig controls the intelligent model routing feature.
// When enabled, each incoming message is scored against structural features
// (message length, code blocks, tool call history, conversation depth, attachments).
// Messages scoring below Threshold are sent to LightModel; all others use the
// agent's primary model. This reduces cost and latency for simple tasks without
// requiring any keyword matching — all scoring is language-agnostic.
type RoutingConfig struct {
	Enabled    bool    `json:"enabled"`
	LightModel string  `json:"light_model"` // model_name from model_list to use for simple tasks
	Threshold  float64 `json:"threshold"`   // complexity score in [0,1]; score >= threshold → primary model
}

// SubTurnConfig configures the SubTurn execution system.
type SubTurnConfig struct {
	MaxDepth              int `json:"max_depth"               env:"PICOCLAW_AGENTS_DEFAULTS_SUBTURN_MAX_DEPTH"`
	MaxConcurrent         int `json:"max_concurrent"          env:"PICOCLAW_AGENTS_DEFAULTS_SUBTURN_MAX_CONCURRENT"`
	DefaultTimeoutMinutes int `json:"default_timeout_minutes" env:"PICOCLAW_AGENTS_DEFAULTS_SUBTURN_DEFAULT_TIMEOUT_MINUTES"`
	DefaultTokenBudget    int `json:"default_token_budget"    env:"PICOCLAW_AGENTS_DEFAULTS_SUBTURN_DEFAULT_TOKEN_BUDGET"`
	ConcurrencyTimeoutSec int `json:"concurrency_timeout_sec" env:"PICOCLAW_AGENTS_DEFAULTS_SUBTURN_CONCURRENCY_TIMEOUT_SEC"`
}

type ToolFeedbackConfig struct {
	Enabled       bool `json:"enabled"         env:"PICOCLAW_AGENTS_DEFAULTS_TOOL_FEEDBACK_ENABLED"`
	MaxArgsLength int  `json:"max_args_length" env:"PICOCLAW_AGENTS_DEFAULTS_TOOL_FEEDBACK_MAX_ARGS_LENGTH"`
}

type AgentDefaults struct {
	Workspace                 string             `json:"workspace"                       env:"PICOCLAW_AGENTS_DEFAULTS_WORKSPACE"`
	RestrictToWorkspace       bool               `json:"restrict_to_workspace"           env:"PICOCLAW_AGENTS_DEFAULTS_RESTRICT_TO_WORKSPACE"`
	AllowReadOutsideWorkspace bool               `json:"allow_read_outside_workspace"    env:"PICOCLAW_AGENTS_DEFAULTS_ALLOW_READ_OUTSIDE_WORKSPACE"`
	Provider                  string             `json:"provider"                        env:"PICOCLAW_AGENTS_DEFAULTS_PROVIDER"`
	ModelName                 string             `json:"model_name"                      env:"PICOCLAW_AGENTS_DEFAULTS_MODEL_NAME"`
	ModelFallbacks            []string           `json:"model_fallbacks,omitempty"`
	ImageModel                string             `json:"image_model,omitempty"           env:"PICOCLAW_AGENTS_DEFAULTS_IMAGE_MODEL"`
	ImageModelFallbacks       []string           `json:"image_model_fallbacks,omitempty"`
	MaxTokens                 int                `json:"max_tokens"                      env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOKENS"`
	ContextWindow             int                `json:"context_window,omitempty"        env:"PICOCLAW_AGENTS_DEFAULTS_CONTEXT_WINDOW"`
	Temperature               *float64           `json:"temperature,omitempty"           env:"PICOCLAW_AGENTS_DEFAULTS_TEMPERATURE"`
	MaxToolIterations         int                `json:"max_tool_iterations"             env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOOL_ITERATIONS"`
	SummarizeMessageThreshold int                `json:"summarize_message_threshold"     env:"PICOCLAW_AGENTS_DEFAULTS_SUMMARIZE_MESSAGE_THRESHOLD"`
	SummarizeTokenPercent     int                `json:"summarize_token_percent"         env:"PICOCLAW_AGENTS_DEFAULTS_SUMMARIZE_TOKEN_PERCENT"`
	MaxMediaSize              int                `json:"max_media_size,omitempty"        env:"PICOCLAW_AGENTS_DEFAULTS_MAX_MEDIA_SIZE"`
	Routing                   *RoutingConfig     `json:"routing,omitempty"`
	SteeringMode              string             `json:"steering_mode,omitempty"         env:"PICOCLAW_AGENTS_DEFAULTS_STEERING_MODE"` // "one-at-a-time" (default) or "all"
	SubTurn                   SubTurnConfig      `json:"subturn"                                                                                     envPrefix:"PICOCLAW_AGENTS_DEFAULTS_SUBTURN_"`
	ToolFeedback              ToolFeedbackConfig `json:"tool_feedback,omitempty"`
}

const DefaultMaxMediaSize = 20 * 1024 * 1024 // 20 MB

func (d *AgentDefaults) GetMaxMediaSize() int {
	if d.MaxMediaSize > 0 {
		return d.MaxMediaSize
	}
	return DefaultMaxMediaSize
}

// GetToolFeedbackMaxArgsLength returns the max args preview length for tool feedback messages.
func (d *AgentDefaults) GetToolFeedbackMaxArgsLength() int {
	if d.ToolFeedback.MaxArgsLength > 0 {
		return d.ToolFeedback.MaxArgsLength
	}
	return 300
}

// IsToolFeedbackEnabled returns true when tool feedback messages should be sent to the chat.
func (d *AgentDefaults) IsToolFeedbackEnabled() bool {
	return d.ToolFeedback.Enabled
}

// GetModelName returns the effective model name for the agent defaults.
// It prefers the new "model_name" field but falls back to "model" for backward compatibility.
func (d *AgentDefaults) GetModelName() string {
	return d.ModelName
}

type ChannelsConfig struct {
	WhatsApp   WhatsAppConfig   `json:"whatsapp"`
	Telegram   TelegramConfig   `json:"telegram"`
	Feishu     FeishuConfig     `json:"feishu"`
	Discord    DiscordConfig    `json:"discord"`
	MaixCam    MaixCamConfig    `json:"maixcam"`
	QQ         QQConfig         `json:"qq"`
	DingTalk   DingTalkConfig   `json:"dingtalk"`
	Slack      SlackConfig      `json:"slack"`
	Matrix     MatrixConfig     `json:"matrix"`
	LINE       LINEConfig       `json:"line"`
	OneBot     OneBotConfig     `json:"onebot"`
	WeCom      WeComConfig      `json:"wecom"       envPrefix:"PICOCLAW_CHANNELS_WECOM_"`
	Weixin     WeixinConfig     `json:"weixin"`
	Pico       PicoConfig       `json:"pico"`
	PicoClient PicoClientConfig `json:"pico_client"`
	IRC        IRCConfig        `json:"irc"`
}

// GroupTriggerConfig controls when the bot responds in group chats.
type GroupTriggerConfig struct {
	MentionOnly bool     `json:"mention_only,omitempty"`
	Prefixes    []string `json:"prefixes,omitempty"`
}

// TypingConfig controls typing indicator behavior (Phase 10).
type TypingConfig struct {
	Enabled bool `json:"enabled,omitempty"`
}

// PlaceholderConfig controls placeholder message behavior (Phase 10).
type PlaceholderConfig struct {
	Enabled bool   `json:"enabled"`
	Text    string `json:"text,omitempty"`
}

type StreamingConfig struct {
	Enabled         bool `json:"enabled,omitempty"          env:"PICOCLAW_CHANNELS_TELEGRAM_STREAMING_ENABLED"`
	ThrottleSeconds int  `json:"throttle_seconds,omitempty" env:"PICOCLAW_CHANNELS_TELEGRAM_STREAMING_THROTTLE_SECONDS"`
	MinGrowthChars  int  `json:"min_growth_chars,omitempty" env:"PICOCLAW_CHANNELS_TELEGRAM_STREAMING_MIN_GROWTH_CHARS"`
}

type WhatsAppConfig struct {
	Enabled            bool                `json:"enabled"              env:"PICOCLAW_CHANNELS_WHATSAPP_ENABLED"`
	BridgeURL          string              `json:"bridge_url"           env:"PICOCLAW_CHANNELS_WHATSAPP_BRIDGE_URL"`
	UseNative          bool                `json:"use_native"           env:"PICOCLAW_CHANNELS_WHATSAPP_USE_NATIVE"`
	SessionStorePath   string              `json:"session_store_path"   env:"PICOCLAW_CHANNELS_WHATSAPP_SESSION_STORE_PATH"`
	AllowFrom          FlexibleStringSlice `json:"allow_from"           env:"PICOCLAW_CHANNELS_WHATSAPP_ALLOW_FROM"`
	ReasoningChannelID string              `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_WHATSAPP_REASONING_CHANNEL_ID"`
}

type TelegramConfig struct {
	Enabled            bool `json:"enabled"                 env:"PICOCLAW_CHANNELS_TELEGRAM_ENABLED"`
	token              string
	BaseURL            string              `json:"base_url"                env:"PICOCLAW_CHANNELS_TELEGRAM_BASE_URL"`
	Proxy              string              `json:"proxy"                   env:"PICOCLAW_CHANNELS_TELEGRAM_PROXY"`
	AllowFrom          FlexibleStringSlice `json:"allow_from"              env:"PICOCLAW_CHANNELS_TELEGRAM_ALLOW_FROM"`
	GroupTrigger       GroupTriggerConfig  `json:"group_trigger,omitempty"`
	Typing             TypingConfig        `json:"typing,omitempty"`
	Placeholder        PlaceholderConfig   `json:"placeholder,omitempty"`
	Streaming          StreamingConfig     `json:"streaming,omitempty"`
	ReasoningChannelID string              `json:"reasoning_channel_id"    env:"PICOCLAW_CHANNELS_TELEGRAM_REASONING_CHANNEL_ID"`
	UseMarkdownV2      bool                `json:"use_markdown_v2"         env:"PICOCLAW_CHANNELS_TELEGRAM_USE_MARKDOWN_V2"`
	secDirty           bool
}

// Token returns the Telegram bot token
func (c *TelegramConfig) Token() string {
	return c.token
}

// SetToken sets the Telegram bot token
func (c *TelegramConfig) SetToken(token string) {
	c.token = token
	c.secDirty = true
}

type FeishuConfig struct {
	Enabled             bool   `json:"enabled"                 env:"PICOCLAW_CHANNELS_FEISHU_ENABLED"`
	AppID               string `json:"app_id"                  env:"PICOCLAW_CHANNELS_FEISHU_APP_ID"`
	appSecret           string
	encryptKey          string
	verificationToken   string
	AllowFrom           FlexibleStringSlice `json:"allow_from"              env:"PICOCLAW_CHANNELS_FEISHU_ALLOW_FROM"`
	GroupTrigger        GroupTriggerConfig  `json:"group_trigger,omitempty"`
	Placeholder         PlaceholderConfig   `json:"placeholder,omitempty"`
	ReasoningChannelID  string              `json:"reasoning_channel_id"    env:"PICOCLAW_CHANNELS_FEISHU_REASONING_CHANNEL_ID"`
	RandomReactionEmoji FlexibleStringSlice `json:"random_reaction_emoji"   env:"PICOCLAW_CHANNELS_FEISHU_RANDOM_REACTION_EMOJI"`
	IsLark              bool                `json:"is_lark"                 env:"PICOCLAW_CHANNELS_FEISHU_IS_LARK"`
	secDirty            bool
}

// AppSecret returns the Feishu app secret
func (c *FeishuConfig) AppSecret() string {
	return c.appSecret
}

// SetAppSecret sets the Feishu app secret
func (c *FeishuConfig) SetAppSecret(secret string) {
	c.appSecret = secret
	c.secDirty = true
}

// EncryptKey returns the Feishu encrypt key
func (c *FeishuConfig) EncryptKey() string {
	return c.encryptKey
}

// SetEncryptKey sets the Feishu encrypt key
func (c *FeishuConfig) SetEncryptKey(key string) {
	c.encryptKey = key
	c.secDirty = true
}

// VerificationToken returns the Feishu verification token
func (c *FeishuConfig) VerificationToken() string {
	return c.verificationToken
}

// SetVerificationToken sets the Feishu verification token
func (c *FeishuConfig) SetVerificationToken(token string) {
	c.verificationToken = token
	c.secDirty = true
}

type DiscordConfig struct {
	Enabled            bool `json:"enabled"                 env:"PICOCLAW_CHANNELS_DISCORD_ENABLED"`
	token              string
	Proxy              string              `json:"proxy"                   env:"PICOCLAW_CHANNELS_DISCORD_PROXY"`
	AllowFrom          FlexibleStringSlice `json:"allow_from"              env:"PICOCLAW_CHANNELS_DISCORD_ALLOW_FROM"`
	MentionOnly        bool                `json:"mention_only"            env:"PICOCLAW_CHANNELS_DISCORD_MENTION_ONLY"`
	GroupTrigger       GroupTriggerConfig  `json:"group_trigger,omitempty"`
	Typing             TypingConfig        `json:"typing,omitempty"`
	Placeholder        PlaceholderConfig   `json:"placeholder,omitempty"`
	ReasoningChannelID string              `json:"reasoning_channel_id"    env:"PICOCLAW_CHANNELS_DISCORD_REASONING_CHANNEL_ID"`
	secDirty           bool
}

// Token returns the Discord bot token
func (c *DiscordConfig) Token() string {
	return c.token
}

// SetToken sets the Discord bot token
func (c *DiscordConfig) SetToken(token string) {
	c.token = token
	c.secDirty = true
}

type MaixCamConfig struct {
	Enabled            bool                `json:"enabled"              env:"PICOCLAW_CHANNELS_MAIXCAM_ENABLED"`
	Host               string              `json:"host"                 env:"PICOCLAW_CHANNELS_MAIXCAM_HOST"`
	Port               int                 `json:"port"                 env:"PICOCLAW_CHANNELS_MAIXCAM_PORT"`
	AllowFrom          FlexibleStringSlice `json:"allow_from"           env:"PICOCLAW_CHANNELS_MAIXCAM_ALLOW_FROM"`
	ReasoningChannelID string              `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_MAIXCAM_REASONING_CHANNEL_ID"`
}

type QQConfig struct {
	Enabled              bool   `json:"enabled"                  env:"PICOCLAW_CHANNELS_QQ_ENABLED"`
	AppID                string `json:"app_id"                   env:"PICOCLAW_CHANNELS_QQ_APP_ID"`
	appSecret            string
	AllowFrom            FlexibleStringSlice `json:"allow_from"               env:"PICOCLAW_CHANNELS_QQ_ALLOW_FROM"`
	GroupTrigger         GroupTriggerConfig  `json:"group_trigger,omitempty"`
	MaxMessageLength     int                 `json:"max_message_length"       env:"PICOCLAW_CHANNELS_QQ_MAX_MESSAGE_LENGTH"`
	MaxBase64FileSizeMiB int64               `json:"max_base64_file_size_mib" env:"PICOCLAW_CHANNELS_QQ_MAX_BASE64_FILE_SIZE_MIB"`
	SendMarkdown         bool                `json:"send_markdown"            env:"PICOCLAW_CHANNELS_QQ_SEND_MARKDOWN"`
	ReasoningChannelID   string              `json:"reasoning_channel_id"     env:"PICOCLAW_CHANNELS_QQ_REASONING_CHANNEL_ID"`
	secDirty             bool
}

// AppSecret returns the QQ app secret
func (c *QQConfig) AppSecret() string {
	return c.appSecret
}

// SetAppSecret sets the QQ app secret
func (c *QQConfig) SetAppSecret(secret string) {
	c.appSecret = secret
	c.secDirty = true
}

type DingTalkConfig struct {
	Enabled            bool   `json:"enabled"                 env:"PICOCLAW_CHANNELS_DINGTALK_ENABLED"`
	ClientID           string `json:"client_id"               env:"PICOCLAW_CHANNELS_DINGTALK_CLIENT_ID"`
	clientSecret       string
	AllowFrom          FlexibleStringSlice `json:"allow_from"              env:"PICOCLAW_CHANNELS_DINGTALK_ALLOW_FROM"`
	GroupTrigger       GroupTriggerConfig  `json:"group_trigger,omitempty"`
	ReasoningChannelID string              `json:"reasoning_channel_id"    env:"PICOCLAW_CHANNELS_DINGTALK_REASONING_CHANNEL_ID"`
	secDirty           bool
}

// ClientSecret returns the DingTalk client secret
func (c *DingTalkConfig) ClientSecret() string {
	return c.clientSecret
}

// SetClientSecret sets the DingTalk client secret
func (c *DingTalkConfig) SetClientSecret(secret string) {
	c.clientSecret = secret
	c.secDirty = true
}

type SlackConfig struct {
	Enabled            bool `json:"enabled"                 env:"PICOCLAW_CHANNELS_SLACK_ENABLED"`
	botToken           string
	appToken           string
	AllowFrom          FlexibleStringSlice `json:"allow_from"              env:"PICOCLAW_CHANNELS_SLACK_ALLOW_FROM"`
	GroupTrigger       GroupTriggerConfig  `json:"group_trigger,omitempty"`
	Typing             TypingConfig        `json:"typing,omitempty"`
	Placeholder        PlaceholderConfig   `json:"placeholder,omitempty"`
	ReasoningChannelID string              `json:"reasoning_channel_id"    env:"PICOCLAW_CHANNELS_SLACK_REASONING_CHANNEL_ID"`
	secDirty           bool
}

// BotToken returns the Slack bot token
func (c *SlackConfig) BotToken() string {
	return c.botToken
}

// SetBotToken sets the Slack bot token
func (c *SlackConfig) SetBotToken(token string) {
	c.botToken = token
	c.secDirty = true
}

// AppToken returns the Slack app token
func (c *SlackConfig) AppToken() string {
	return c.appToken
}

// SetAppToken sets the Slack app token
func (c *SlackConfig) SetAppToken(token string) {
	c.appToken = token
	c.secDirty = true
}

type MatrixConfig struct {
	Enabled            bool   `json:"enabled"                        env:"PICOCLAW_CHANNELS_MATRIX_ENABLED"`
	Homeserver         string `json:"homeserver"                     env:"PICOCLAW_CHANNELS_MATRIX_HOMESERVER"`
	UserID             string `json:"user_id"                        env:"PICOCLAW_CHANNELS_MATRIX_USER_ID"`
	accessToken        string
	DeviceID           string              `json:"device_id,omitempty"            env:"PICOCLAW_CHANNELS_MATRIX_DEVICE_ID"`
	JoinOnInvite       bool                `json:"join_on_invite"                 env:"PICOCLAW_CHANNELS_MATRIX_JOIN_ON_INVITE"`
	MessageFormat      string              `json:"message_format,omitempty"       env:"PICOCLAW_CHANNELS_MATRIX_MESSAGE_FORMAT"`
	AllowFrom          FlexibleStringSlice `json:"allow_from"                     env:"PICOCLAW_CHANNELS_MATRIX_ALLOW_FROM"`
	GroupTrigger       GroupTriggerConfig  `json:"group_trigger,omitempty"`
	Placeholder        PlaceholderConfig   `json:"placeholder,omitempty"`
	ReasoningChannelID string              `json:"reasoning_channel_id"           env:"PICOCLAW_CHANNELS_MATRIX_REASONING_CHANNEL_ID"`
	secDirty           bool
	CryptoDatabasePath string `json:"crypto_database_path,omitempty" env:"PICOCLAW_CHANNELS_MATRIX_CRYPTO_DATABASE_PATH"`
	CryptoPassphrase   string `json:"crypto_passphrase,omitempty"    env:"PICOCLAW_CHANNELS_MATRIX_CRYPTO_PASSPHRASE"`
}

// AccessToken returns the Matrix access token
func (c *MatrixConfig) AccessToken() string {
	return c.accessToken
}

// SetAccessToken sets the Matrix access token
func (c *MatrixConfig) SetAccessToken(token string) {
	c.accessToken = token
	c.secDirty = true
}

type LINEConfig struct {
	Enabled            bool `json:"enabled"                 env:"PICOCLAW_CHANNELS_LINE_ENABLED"`
	channelSecret      string
	channelAccessToken string
	WebhookHost        string              `json:"webhook_host"            env:"PICOCLAW_CHANNELS_LINE_WEBHOOK_HOST"`
	WebhookPort        int                 `json:"webhook_port"            env:"PICOCLAW_CHANNELS_LINE_WEBHOOK_PORT"`
	WebhookPath        string              `json:"webhook_path"            env:"PICOCLAW_CHANNELS_LINE_WEBHOOK_PATH"`
	AllowFrom          FlexibleStringSlice `json:"allow_from"              env:"PICOCLAW_CHANNELS_LINE_ALLOW_FROM"`
	GroupTrigger       GroupTriggerConfig  `json:"group_trigger,omitempty"`
	Typing             TypingConfig        `json:"typing,omitempty"`
	Placeholder        PlaceholderConfig   `json:"placeholder,omitempty"`
	ReasoningChannelID string              `json:"reasoning_channel_id"    env:"PICOCLAW_CHANNELS_LINE_REASONING_CHANNEL_ID"`
	secDirty           bool
}

// ChannelSecret returns the LINE channel secret
func (c *LINEConfig) ChannelSecret() string {
	return c.channelSecret
}

// SetChannelSecret sets the LINE channel secret
func (c *LINEConfig) SetChannelSecret(secret string) {
	c.channelSecret = secret
	c.secDirty = true
}

// ChannelAccessToken returns the LINE channel access token
func (c *LINEConfig) ChannelAccessToken() string {
	return c.channelAccessToken
}

// SetChannelAccessToken sets the LINE channel access token
func (c *LINEConfig) SetChannelAccessToken(token string) {
	c.channelAccessToken = token
	c.secDirty = true
}

type OneBotConfig struct {
	Enabled            bool   `json:"enabled"                 env:"PICOCLAW_CHANNELS_ONEBOT_ENABLED"`
	WSUrl              string `json:"ws_url"                  env:"PICOCLAW_CHANNELS_ONEBOT_WS_URL"`
	accessToken        string
	ReconnectInterval  int                 `json:"reconnect_interval"      env:"PICOCLAW_CHANNELS_ONEBOT_RECONNECT_INTERVAL"`
	GroupTriggerPrefix []string            `json:"group_trigger_prefix"    env:"PICOCLAW_CHANNELS_ONEBOT_GROUP_TRIGGER_PREFIX"`
	AllowFrom          FlexibleStringSlice `json:"allow_from"              env:"PICOCLAW_CHANNELS_ONEBOT_ALLOW_FROM"`
	GroupTrigger       GroupTriggerConfig  `json:"group_trigger,omitempty"`
	Typing             TypingConfig        `json:"typing,omitempty"`
	Placeholder        PlaceholderConfig   `json:"placeholder,omitempty"`
	ReasoningChannelID string              `json:"reasoning_channel_id"    env:"PICOCLAW_CHANNELS_ONEBOT_REASONING_CHANNEL_ID"`
	secDirty           bool
}

// AccessToken returns the OneBot access token
func (c *OneBotConfig) AccessToken() string {
	return c.accessToken
}

// SetAccessToken sets the OneBot access token
func (c *OneBotConfig) SetAccessToken(token string) {
	c.accessToken = token
	c.secDirty = true
}

type WeComGroupConfig struct {
	AllowFrom FlexibleStringSlice `json:"allow_from,omitempty"`
}

type WeComConfig struct {
	Enabled             bool   `json:"enabled"                 env:"ENABLED"`
	BotID               string `json:"bot_id"                  env:"BOT_ID"`
	secret              string
	WebSocketURL        string              `json:"websocket_url,omitempty" env:"WEBSOCKET_URL"`
	SendThinkingMessage bool                `json:"send_thinking_message"   env:"SEND_THINKING_MESSAGE"`
	AllowFrom           FlexibleStringSlice `json:"allow_from"              env:"ALLOW_FROM"`
	ReasoningChannelID  string              `json:"reasoning_channel_id"    env:"REASONING_CHANNEL_ID"`
	secDirty            bool
}

// Secret returns the WeCom bot secret.
func (c *WeComConfig) Secret() string {
	return c.secret
}

// SetSecret sets the WeCom bot secret.
func (c *WeComConfig) SetSecret(secret string) {
	c.secret = secret
	c.secDirty = true
}

type WeixinConfig struct {
	Enabled            bool `json:"enabled"              env:"PICOCLAW_CHANNELS_WEIXIN_ENABLED"`
	token              string
	AccountID          string              `json:"account_id,omitempty" env:"PICOCLAW_CHANNELS_WEIXIN_ACCOUNT_ID"`
	BaseURL            string              `json:"base_url"             env:"PICOCLAW_CHANNELS_WEIXIN_BASE_URL"`
	CDNBaseURL         string              `json:"cdn_base_url"         env:"PICOCLAW_CHANNELS_WEIXIN_CDN_BASE_URL"`
	Proxy              string              `json:"proxy"                env:"PICOCLAW_CHANNELS_WEIXIN_PROXY"`
	AllowFrom          FlexibleStringSlice `json:"allow_from"           env:"PICOCLAW_CHANNELS_WEIXIN_ALLOW_FROM"`
	ReasoningChannelID string              `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_WEIXIN_REASONING_CHANNEL_ID"`
	secDirty           bool
}

func (c *WeixinConfig) Token() string {
	return c.token
}

func (c *WeixinConfig) SetToken(token string) *WeixinConfig {
	c.token = token
	c.secDirty = true
	return c
}

type PicoConfig struct {
	Enabled         bool `json:"enabled"                     env:"PICOCLAW_CHANNELS_PICO_ENABLED"`
	token           string
	AllowTokenQuery bool                `json:"allow_token_query,omitempty"`
	AllowOrigins    []string            `json:"allow_origins,omitempty"`
	PingInterval    int                 `json:"ping_interval,omitempty"`
	ReadTimeout     int                 `json:"read_timeout,omitempty"`
	WriteTimeout    int                 `json:"write_timeout,omitempty"`
	MaxConnections  int                 `json:"max_connections,omitempty"`
	AllowFrom       FlexibleStringSlice `json:"allow_from"                  env:"PICOCLAW_CHANNELS_PICO_ALLOW_FROM"`
	Placeholder     PlaceholderConfig   `json:"placeholder,omitempty"`
	secDirty        bool
}

// Token returns the Pico channel token
func (c *PicoConfig) Token() string {
	return c.token
}

// SetToken sets the Pico channel token
func (c *PicoConfig) SetToken(token string) {
	c.token = token
	c.secDirty = true
}

type PicoClientConfig struct {
	Enabled      bool                `json:"enabled"                 env:"PICOCLAW_CHANNELS_PICO_CLIENT_ENABLED"`
	URL          string              `json:"url"                     env:"PICOCLAW_CHANNELS_PICO_CLIENT_URL"`
	Token        string              `json:"token"                   env:"PICOCLAW_CHANNELS_PICO_CLIENT_TOKEN"`
	SessionID    string              `json:"session_id,omitempty"`
	PingInterval int                 `json:"ping_interval,omitempty"`
	ReadTimeout  int                 `json:"read_timeout,omitempty"`
	AllowFrom    FlexibleStringSlice `json:"allow_from"              env:"PICOCLAW_CHANNELS_PICO_CLIENT_ALLOW_FROM"`
}

type IRCConfig struct {
	Enabled            bool   `json:"enabled"                 env:"PICOCLAW_CHANNELS_IRC_ENABLED"`
	Server             string `json:"server"                  env:"PICOCLAW_CHANNELS_IRC_SERVER"`
	TLS                bool   `json:"tls"                     env:"PICOCLAW_CHANNELS_IRC_TLS"`
	Nick               string `json:"nick"                    env:"PICOCLAW_CHANNELS_IRC_NICK"`
	User               string `json:"user,omitempty"          env:"PICOCLAW_CHANNELS_IRC_USER"`
	RealName           string `json:"real_name,omitempty"     env:"PICOCLAW_CHANNELS_IRC_REAL_NAME"`
	password           string
	nickServPassword   string
	SASLUser           string `json:"sasl_user"               env:"PICOCLAW_CHANNELS_IRC_SASL_USER"`
	saslPassword       string
	Channels           FlexibleStringSlice `json:"channels"                env:"PICOCLAW_CHANNELS_IRC_CHANNELS"`
	RequestCaps        FlexibleStringSlice `json:"request_caps,omitempty"  env:"PICOCLAW_CHANNELS_IRC_REQUEST_CAPS"`
	AllowFrom          FlexibleStringSlice `json:"allow_from"              env:"PICOCLAW_CHANNELS_IRC_ALLOW_FROM"`
	GroupTrigger       GroupTriggerConfig  `json:"group_trigger,omitempty"`
	Typing             TypingConfig        `json:"typing,omitempty"`
	ReasoningChannelID string              `json:"reasoning_channel_id"    env:"PICOCLAW_CHANNELS_IRC_REASONING_CHANNEL_ID"`
	secDirty           bool
}

// Password returns the IRC password
func (c *IRCConfig) Password() string {
	return c.password
}

// NickServPassword returns the NickServ password
func (c *IRCConfig) NickServPassword() string {
	return c.nickServPassword
}

// SASLPassword returns the SASL password
func (c *IRCConfig) SASLPassword() string {
	return c.saslPassword
}

func (c *IRCConfig) SetPassword(password string) {
	c.password = password
	c.secDirty = true
}

func (c *IRCConfig) SetNickServPassword(password string) {
	c.nickServPassword = password
	c.secDirty = true
}

func (c *IRCConfig) SetSASLPassword(password string) {
	c.saslPassword = password
	c.secDirty = true
}

type HeartbeatConfig struct {
	Enabled  bool `json:"enabled"  env:"PICOCLAW_HEARTBEAT_ENABLED"`
	Interval int  `json:"interval" env:"PICOCLAW_HEARTBEAT_INTERVAL"` // minutes, min 5
}

type DevicesConfig struct {
	Enabled    bool `json:"enabled"     env:"PICOCLAW_DEVICES_ENABLED"`
	MonitorUSB bool `json:"monitor_usb" env:"PICOCLAW_DEVICES_MONITOR_USB"`
}

type VoiceConfig struct {
	ModelName         string `json:"model_name,omitempty"         env:"PICOCLAW_VOICE_MODEL_NAME"`
	EchoTranscription bool   `json:"echo_transcription"           env:"PICOCLAW_VOICE_ECHO_TRANSCRIPTION"`
	ElevenLabsAPIKey  string `json:"elevenlabs_api_key,omitempty" env:"PICOCLAW_VOICE_ELEVENLABS_API_KEY"`
}

// ModelConfig represents a model-centric provider configuration.
// It allows adding new providers (especially OpenAI-compatible ones) via configuration only.
// The model field uses protocol prefix format: [protocol/]model-identifier
// Supported protocols include openai, anthropic, antigravity, claude-cli,
// codex-cli, github-copilot, and named OpenAI-compatible protocols such as
// groq, deepseek, modelscope, and novita.
// Default protocol is "openai" if no prefix is specified.
type ModelConfig struct {
	// Required fields
	ModelName string `json:"model_name"` // User-facing alias for the model
	Model     string `json:"model"`      // Protocol/model-identifier (e.g., "openai/gpt-4o", "anthropic/claude-sonnet-4.6")

	// HTTP-based providers
	APIBase   string   `json:"api_base,omitempty"`  // API endpoint URL
	Proxy     string   `json:"proxy,omitempty"`     // HTTP proxy URL
	Fallbacks []string `json:"fallbacks,omitempty"` // Fallback model names for failover

	// Special providers (CLI-based, OAuth, etc.)
	AuthMethod  string `json:"auth_method,omitempty"`  // Authentication method: oauth, token
	ConnectMode string `json:"connect_mode,omitempty"` // Connection mode: stdio, grpc
	Workspace   string `json:"workspace,omitempty"`    // Workspace path for CLI-based providers

	// Optional optimizations
	RPM            int            `json:"rpm,omitempty"`              // Requests per minute limit
	MaxTokensField string         `json:"max_tokens_field,omitempty"` // Field name for max tokens (e.g., "max_completion_tokens")
	RequestTimeout int            `json:"request_timeout,omitempty"`
	ThinkingLevel  string         `json:"thinking_level,omitempty"` // Extended thinking: off|low|medium|high|xhigh|adaptive
	ExtraBody      map[string]any `json:"extra_body,omitempty"`     // Additional fields to inject into request body

	// from security
	secModelName string
	apiKeys      []string
	secDirty     bool

	// isVirtual marks this model as a virtual model generated from multi-key expansion.
	// Virtual models should not be persisted to config files.
	isVirtual bool
}

// APIKey returns the first API key from apiKeys
func (c *ModelConfig) APIKey() string {
	if len(c.apiKeys) > 0 {
		return c.apiKeys[0]
	}
	return ""
}

// IsVirtual returns true if this model was generated from multi-key expansion.
func (c *ModelConfig) IsVirtual() bool {
	return c.isVirtual
}

// Validate checks if the ModelConfig has all required fields.
func (c *ModelConfig) Validate() error {
	if c.ModelName == "" {
		return fmt.Errorf("model_name is required")
	}
	if c.Model == "" {
		return fmt.Errorf("model is required")
	}
	return nil
}

func (c *ModelConfig) SetAPIKey(value string) {
	if len(c.apiKeys) > 0 {
		c.apiKeys[0] = value
	} else {
		c.apiKeys = append(c.apiKeys, value)
	}
	c.secDirty = true
}

type GatewayConfig struct {
	Host      string `json:"host"                env:"PICOCLAW_GATEWAY_HOST"`
	Port      int    `json:"port"                env:"PICOCLAW_GATEWAY_PORT"`
	HotReload bool   `json:"hot_reload"          env:"PICOCLAW_GATEWAY_HOT_RELOAD"`
	LogLevel  string `json:"log_level,omitempty" env:"PICOCLAW_LOG_LEVEL"`
}

type ToolDiscoveryConfig struct {
	Enabled          bool `json:"enabled"            env:"PICOCLAW_TOOLS_DISCOVERY_ENABLED"`
	TTL              int  `json:"ttl"                env:"PICOCLAW_TOOLS_DISCOVERY_TTL"`
	MaxSearchResults int  `json:"max_search_results" env:"PICOCLAW_MAX_SEARCH_RESULTS"`
	UseBM25          bool `json:"use_bm25"           env:"PICOCLAW_TOOLS_DISCOVERY_USE_BM25"`
	UseRegex         bool `json:"use_regex"          env:"PICOCLAW_TOOLS_DISCOVERY_USE_REGEX"`
}

type ToolConfig struct {
	Enabled bool `json:"enabled" env:"ENABLED"`
}

type BraveConfig struct {
	Enabled    bool `json:"enabled"     env:"PICOCLAW_TOOLS_WEB_BRAVE_ENABLED"`
	apiKeys    []string
	secDirty   bool
	MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_BRAVE_MAX_RESULTS"`
}

// APIKey returns the Brave API key
func (c *BraveConfig) APIKey() string {
	if len(c.apiKeys) == 0 {
		return ""
	}
	return c.apiKeys[0]
}

// APIKeys returns the Brave API keys
func (c *BraveConfig) APIKeys() []string {
	return c.apiKeys
}

// SetAPIKey sets the Brave API key
func (c *BraveConfig) SetAPIKey(key string) {
	c.apiKeys = []string{key}
	c.secDirty = true
}

// SetAPIKeys sets the Brave API keys
func (c *BraveConfig) SetAPIKeys(keys []string) {
	c.apiKeys = keys
	c.secDirty = true
}

type TavilyConfig struct {
	Enabled    bool `json:"enabled"     env:"PICOCLAW_TOOLS_WEB_TAVILY_ENABLED"`
	apiKeys    []string
	secDirty   bool
	BaseURL    string `json:"base_url"    env:"PICOCLAW_TOOLS_WEB_TAVILY_BASE_URL"`
	MaxResults int    `json:"max_results" env:"PICOCLAW_TOOLS_WEB_TAVILY_MAX_RESULTS"`
}

// APIKey returns the Tavily API key
func (c *TavilyConfig) APIKey() string {
	if len(c.apiKeys) == 0 {
		return ""
	}
	return c.apiKeys[0]
}

// APIKeys returns the Tavily API keys
func (c *TavilyConfig) APIKeys() []string {
	return c.apiKeys
}

// SetAPIKey sets the Tavily API key
func (c *TavilyConfig) SetAPIKey(key string) {
	c.apiKeys = []string{key}
	c.secDirty = true
}

// SetAPIKeys sets the Tavily API keys
func (c *TavilyConfig) SetAPIKeys(keys []string) {
	c.apiKeys = keys
	c.secDirty = true
}

type DuckDuckGoConfig struct {
	Enabled    bool `json:"enabled"     env:"PICOCLAW_TOOLS_WEB_DUCKDUCKGO_ENABLED"`
	MaxResults int  `json:"max_results" env:"PICOCLAW_TOOLS_WEB_DUCKDUCKGO_MAX_RESULTS"`
}

type PerplexityConfig struct {
	Enabled    bool `json:"enabled"     env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_ENABLED"`
	apiKeys    []string
	secDirty   bool
	MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_MAX_RESULTS"`
}

// APIKey returns the Perplexity API key
func (c *PerplexityConfig) APIKey() string {
	if len(c.apiKeys) == 0 {
		return ""
	}
	return c.apiKeys[0]
}

// SetAPIKey sets the Perplexity API key
func (c *PerplexityConfig) SetAPIKey(key string) {
	c.apiKeys = []string{key}
	c.secDirty = true
}

// APIKeys returns the Perplexity API keys
func (c *PerplexityConfig) APIKeys() []string {
	return c.apiKeys
}

// SetAPIKeys sets the Perplexity API keys
func (c *PerplexityConfig) SetAPIKeys(keys []string) {
	c.apiKeys = keys
	c.secDirty = true
}

type SearXNGConfig struct {
	Enabled    bool   `json:"enabled"     env:"PICOCLAW_TOOLS_WEB_SEARXNG_ENABLED"`
	BaseURL    string `json:"base_url"    env:"PICOCLAW_TOOLS_WEB_SEARXNG_BASE_URL"`
	MaxResults int    `json:"max_results" env:"PICOCLAW_TOOLS_WEB_SEARXNG_MAX_RESULTS"`
}

type GLMSearchConfig struct {
	Enabled  bool `json:"enabled"  env:"PICOCLAW_TOOLS_WEB_GLM_ENABLED"`
	apiKey   string
	secDirty bool
	BaseURL  string `json:"base_url" env:"PICOCLAW_TOOLS_WEB_GLM_BASE_URL"`
	// SearchEngine specifies the search backend: "search_std" (default),
	// "search_pro", "search_pro_sogou", or "search_pro_quark".
	SearchEngine string `json:"search_engine" env:"PICOCLAW_TOOLS_WEB_GLM_SEARCH_ENGINE"`
	MaxResults   int    `json:"max_results"   env:"PICOCLAW_TOOLS_WEB_GLM_MAX_RESULTS"`
}

// APIKey returns the GLM search API key
func (c *GLMSearchConfig) APIKey() string {
	return c.apiKey
}

// SetAPIKey sets the GLM search API key (internal use only)
func (c *GLMSearchConfig) SetAPIKey(key string) {
	c.apiKey = key
	c.secDirty = true
}

type BaiduSearchConfig struct {
	Enabled    bool   `json:"enabled"     env:"PICOCLAW_TOOLS_WEB_BAIDU_ENABLED"`
	BaseURL    string `json:"base_url"    env:"PICOCLAW_TOOLS_WEB_BAIDU_BASE_URL"`
	MaxResults int    `json:"max_results" env:"PICOCLAW_TOOLS_WEB_BAIDU_MAX_RESULTS"`
	apiKey     string
	secDirty   bool
}

// APIKey returns the Baidu search API key
func (c *BaiduSearchConfig) APIKey() string {
	return c.apiKey
}

func (c *BaiduSearchConfig) SetAPIKey(key string) {
	c.apiKey = key
	c.secDirty = true
}

type WebToolsConfig struct {
	ToolConfig  `                  envPrefix:"PICOCLAW_TOOLS_WEB_"`
	Brave       BraveConfig       `                                json:"brave"`
	Tavily      TavilyConfig      `                                json:"tavily"`
	DuckDuckGo  DuckDuckGoConfig  `                                json:"duckduckgo"`
	Perplexity  PerplexityConfig  `                                json:"perplexity"`
	SearXNG     SearXNGConfig     `                                json:"searxng"`
	GLMSearch   GLMSearchConfig   `                                json:"glm_search"`
	BaiduSearch BaiduSearchConfig `                                json:"baidu_search"`
	// PreferNative controls whether to use provider-native web search when
	// the active LLM supports it (e.g. OpenAI web_search_preview). When true,
	// the client-side web_search tool is hidden to avoid duplicate search surfaces,
	// and the provider's built-in search is used instead. Falls back to client-side
	// search when the provider does not support native search.
	PreferNative bool `json:"prefer_native" env:"PICOCLAW_TOOLS_WEB_PREFER_NATIVE"`
	// Proxy is an optional proxy URL for web tools (http/https/socks5/socks5h).
	// For authenticated proxies, prefer HTTP_PROXY/HTTPS_PROXY env vars instead of embedding credentials in config.
	Proxy                string              `json:"proxy,omitempty"                  env:"PICOCLAW_TOOLS_WEB_PROXY"`
	FetchLimitBytes      int64               `json:"fetch_limit_bytes,omitempty"      env:"PICOCLAW_TOOLS_WEB_FETCH_LIMIT_BYTES"`
	Format               string              `json:"format,omitempty"                 env:"PICOCLAW_TOOLS_WEB_FORMAT"`
	PrivateHostWhitelist FlexibleStringSlice `json:"private_host_whitelist,omitempty" env:"PICOCLAW_TOOLS_WEB_PRIVATE_HOST_WHITELIST"`
}

type CronToolsConfig struct {
	ToolConfig         `     envPrefix:"PICOCLAW_TOOLS_CRON_"`
	ExecTimeoutMinutes int  `                                 env:"PICOCLAW_TOOLS_CRON_EXEC_TIMEOUT_MINUTES" json:"exec_timeout_minutes"` // 0 means no timeout
	AllowCommand       bool `                                 env:"PICOCLAW_TOOLS_CRON_ALLOW_COMMAND"        json:"allow_command"`
}

type ExecConfig struct {
	ToolConfig          `         envPrefix:"PICOCLAW_TOOLS_EXEC_"`
	EnableDenyPatterns  bool     `                                 env:"PICOCLAW_TOOLS_EXEC_ENABLE_DENY_PATTERNS"  json:"enable_deny_patterns"`
	AllowRemote         bool     `                                 env:"PICOCLAW_TOOLS_EXEC_ALLOW_REMOTE"          json:"allow_remote"`
	CustomDenyPatterns  []string `                                 env:"PICOCLAW_TOOLS_EXEC_CUSTOM_DENY_PATTERNS"  json:"custom_deny_patterns"`
	CustomAllowPatterns []string `                                 env:"PICOCLAW_TOOLS_EXEC_CUSTOM_ALLOW_PATTERNS" json:"custom_allow_patterns"`
	TimeoutSeconds      int      `                                 env:"PICOCLAW_TOOLS_EXEC_TIMEOUT_SECONDS"       json:"timeout_seconds"` // 0 means use default (60s)
}

type SkillsToolsConfig struct {
	ToolConfig            `                       envPrefix:"PICOCLAW_TOOLS_SKILLS_"`
	Registries            SkillsRegistriesConfig `                                   json:"registries"`
	Github                SkillsGithubConfig     `                                   json:"github"`
	MaxConcurrentSearches int                    `                                   json:"max_concurrent_searches" env:"PICOCLAW_TOOLS_SKILLS_MAX_CONCURRENT_SEARCHES"`
	SearchCache           SearchCacheConfig      `                                   json:"search_cache"`
}

type MediaCleanupConfig struct {
	ToolConfig `    envPrefix:"PICOCLAW_MEDIA_CLEANUP_"`
	MaxAge     int `                                    env:"PICOCLAW_MEDIA_CLEANUP_MAX_AGE"  json:"max_age_minutes"`
	Interval   int `                                    env:"PICOCLAW_MEDIA_CLEANUP_INTERVAL" json:"interval_minutes"`
}

type ReadFileToolConfig struct {
	Enabled         bool `json:"enabled"`
	MaxReadFileSize int  `json:"max_read_file_size"`
}

type ToolsConfig struct {
	AllowReadPaths  []string `json:"allow_read_paths"  env:"PICOCLAW_TOOLS_ALLOW_READ_PATHS"`
	AllowWritePaths []string `json:"allow_write_paths" env:"PICOCLAW_TOOLS_ALLOW_WRITE_PATHS"`
	// FilterSensitiveData controls whether to filter sensitive values (API keys,
	// tokens, secrets) from tool results before sending to the LLM.
	// Default: true (enabled)
	FilterSensitiveData bool `json:"filter_sensitive_data" env:"PICOCLAW_TOOLS_FILTER_SENSITIVE_DATA"`
	// FilterMinLength is the minimum content length required for filtering.
	// Content shorter than this will be returned unchanged for performance.
	// Default: 8
	FilterMinLength int                `json:"filter_min_length" env:"PICOCLAW_TOOLS_FILTER_MIN_LENGTH"`
	Web             WebToolsConfig     `json:"web"`
	Cron            CronToolsConfig    `json:"cron"`
	Exec            ExecConfig         `json:"exec"`
	Skills          SkillsToolsConfig  `json:"skills"`
	MediaCleanup    MediaCleanupConfig `json:"media_cleanup"`
	MCP             MCPConfig          `json:"mcp"`
	AppendFile      ToolConfig         `json:"append_file"                                              envPrefix:"PICOCLAW_TOOLS_APPEND_FILE_"`
	EditFile        ToolConfig         `json:"edit_file"                                                envPrefix:"PICOCLAW_TOOLS_EDIT_FILE_"`
	FindSkills      ToolConfig         `json:"find_skills"                                              envPrefix:"PICOCLAW_TOOLS_FIND_SKILLS_"`
	I2C             ToolConfig         `json:"i2c"                                                      envPrefix:"PICOCLAW_TOOLS_I2C_"`
	InstallSkill    ToolConfig         `json:"install_skill"                                            envPrefix:"PICOCLAW_TOOLS_INSTALL_SKILL_"`
	ListDir         ToolConfig         `json:"list_dir"                                                 envPrefix:"PICOCLAW_TOOLS_LIST_DIR_"`
	Message         ToolConfig         `json:"message"                                                  envPrefix:"PICOCLAW_TOOLS_MESSAGE_"`
	ReadFile        ReadFileToolConfig `json:"read_file"                                                envPrefix:"PICOCLAW_TOOLS_READ_FILE_"`
	SendFile        ToolConfig         `json:"send_file"                                                envPrefix:"PICOCLAW_TOOLS_SEND_FILE_"`
	Spawn           ToolConfig         `json:"spawn"                                                    envPrefix:"PICOCLAW_TOOLS_SPAWN_"`
	SpawnStatus     ToolConfig         `json:"spawn_status"                                             envPrefix:"PICOCLAW_TOOLS_SPAWN_STATUS_"`
	SPI             ToolConfig         `json:"spi"                                                      envPrefix:"PICOCLAW_TOOLS_SPI_"`
	Subagent        ToolConfig         `json:"subagent"                                                 envPrefix:"PICOCLAW_TOOLS_SUBAGENT_"`
	WebFetch        ToolConfig         `json:"web_fetch"                                                envPrefix:"PICOCLAW_TOOLS_WEB_FETCH_"`
	WriteFile       ToolConfig         `json:"write_file"                                               envPrefix:"PICOCLAW_TOOLS_WRITE_FILE_"`
}

// IsFilterSensitiveDataEnabled returns true if sensitive data filtering is enabled
func (c *ToolsConfig) IsFilterSensitiveDataEnabled() bool {
	return c.FilterSensitiveData
}

// GetFilterMinLength returns the minimum content length for filtering (default: 8)
func (c *ToolsConfig) GetFilterMinLength() int {
	if c.FilterMinLength <= 0 {
		return 8
	}
	return c.FilterMinLength
}

type SearchCacheConfig struct {
	MaxSize    int `json:"max_size"    env:"PICOCLAW_SKILLS_SEARCH_CACHE_MAX_SIZE"`
	TTLSeconds int `json:"ttl_seconds" env:"PICOCLAW_SKILLS_SEARCH_CACHE_TTL_SECONDS"`
}

type SkillsRegistriesConfig struct {
	ClawHub ClawHubRegistryConfig `json:"clawhub"`
}

type SkillsGithubConfig struct {
	token    string
	secDirty bool
	Proxy    string `json:"proxy,omitempty" env:"PICOCLAW_TOOLS_SKILLS_GITHUB_PROXY"`
}

// Token returns the GitHub token
func (c *SkillsGithubConfig) Token() string {
	return c.token
}

// SetToken sets the GitHub token
func (c *SkillsGithubConfig) SetToken(token string) {
	c.token = token
	c.secDirty = true
}

type ClawHubRegistryConfig struct {
	Enabled         bool   `json:"enabled"           env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_ENABLED"`
	BaseURL         string `json:"base_url"          env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_BASE_URL"`
	authToken       string
	secDirty        bool
	SearchPath      string `json:"search_path"       env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_SEARCH_PATH"`
	SkillsPath      string `json:"skills_path"       env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_SKILLS_PATH"`
	DownloadPath    string `json:"download_path"     env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_DOWNLOAD_PATH"`
	Timeout         int    `json:"timeout"           env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_TIMEOUT"`
	MaxZipSize      int    `json:"max_zip_size"      env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_MAX_ZIP_SIZE"`
	MaxResponseSize int    `json:"max_response_size" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_MAX_RESPONSE_SIZE"`
}

// AuthToken returns the ClawHub auth token
func (c *ClawHubRegistryConfig) AuthToken() string {
	return c.authToken
}

// SetAuthToken sets the ClawHub auth token
func (c *ClawHubRegistryConfig) SetAuthToken(token string) {
	c.authToken = token
	c.secDirty = true
}

// MCPServerConfig defines configuration for a single MCP server
type MCPServerConfig struct {
	// Enabled indicates whether this MCP server is active
	Enabled bool `json:"enabled"`
	// Deferred controls whether this server's tools are registered as hidden (deferred/discovery mode).
	// When nil, the global Discovery.Enabled setting applies.
	// When explicitly set to true or false, it overrides the global setting for this server only.
	Deferred *bool `json:"deferred,omitempty"`
	// Command is the executable to run (e.g., "npx", "python", "/path/to/server")
	Command string `json:"command"`
	// Args are the arguments to pass to the command
	Args []string `json:"args,omitempty"`
	// Env are environment variables to set for the server process (stdio only)
	Env map[string]string `json:"env,omitempty"`
	// EnvFile is the path to a file containing environment variables (stdio only)
	EnvFile string `json:"env_file,omitempty"`
	// Type is "stdio", "sse", or "http" (default: stdio if command is set, sse if url is set)
	Type string `json:"type,omitempty"`
	// URL is used for SSE/HTTP transport
	URL string `json:"url,omitempty"`
	// Headers are HTTP headers to send with requests (sse/http only)
	Headers map[string]string `json:"headers,omitempty"`
}

// MCPConfig defines configuration for all MCP servers
type MCPConfig struct {
	ToolConfig `                    envPrefix:"PICOCLAW_TOOLS_MCP_"`
	Discovery  ToolDiscoveryConfig `                                json:"discovery"`
	// Servers is a map of server name to server configuration
	Servers map[string]MCPServerConfig `json:"servers,omitempty"`
}

func LoadConfig(path string) (*Config, error) {
	logger.Debugf("loading config from %s", path)
	data, err := os.ReadFile(path)
	if err != nil {
		if os.IsNotExist(err) {
			logger.WarnF("config file not found, using default config", map[string]any{"path": path})
			return DefaultConfig(), nil
		}
		logger.Errorf("failed to read config file: %v", err)
		return nil, err
	}

	// First, try to detect config version by reading the version field
	var versionInfo struct {
		Version int `json:"version"`
	}
	if e := json.Unmarshal(data, &versionInfo); e != nil {
		return nil, fmt.Errorf("failed to detect config version: %w", e)
	}
	if len(data) <= 10 {
		logger.Warn(fmt.Sprintf("content is [%s]", string(data)))
		return DefaultConfig().WithSecurity(&SecurityConfig{}), nil
	}

	// Load config based on detected version
	var cfg *Config
	switch versionInfo.Version {
	case 0:
		logger.InfoF("config migrate start", map[string]any{"from": versionInfo.Version, "to": CurrentVersion})
		// Legacy config (no version field)
		v, e := loadConfigV0(data)
		if e != nil {
			return nil, e
		}
		cfg, e = v.Migrate()
		if e != nil {
			logger.ErrorF("config migrate fail", map[string]any{"from": versionInfo.Version, "to": CurrentVersion})
			return nil, e
		}
		logger.InfoF("config migrate success", map[string]any{"from": versionInfo.Version, "to": CurrentVersion})
		err = makeBackup(path)
		if err != nil {
			return nil, err
		}
		// Load existing security config and merge with migrated one to prevent data loss
		existingSec, secErr := loadSecurityConfig(securityPath(path))
		if secErr != nil {
			logger.WarnF("failed to load existing security config during migration", map[string]any{"error": secErr})
		}
		if existingSec != nil && cfg.security != nil {
			cfg.security = mergeSecurityConfig(existingSec, cfg.security)
			// Re-apply the merged security config to update all channels and models
			if err = applySecurityConfig(cfg, cfg.security); err != nil {
				logger.WarnF("failed to re-apply merged security config during migration", map[string]any{"error": err})
			}
		}
		defer func(cfg *Config) {
			_ = SaveConfig(path, cfg)
		}(cfg)
	case CurrentVersion:
		// Current version
		cfg, err = loadConfig(data)
		if err != nil {
			return nil, err
		}

		// Legacy config (no version field)
		tmpCfg, e := loadConfigV0(data)
		if e != nil {
			return nil, e
		}

		tmpCfgMigrated, e := tmpCfg.Migrate()
		if e != nil {
			logger.ErrorF("config migrate fail", map[string]any{"from": versionInfo.Version, "to": CurrentVersion})
			return nil, e
		}

		// Load security configuration from .security.yml
		secPath := securityPath(path)
		sec, err := loadSecurityConfig(secPath)
		if err != nil {
			return nil, fmt.Errorf("failed to load security config: %w", err)
		}

		// Merge security configs: config.json takes precedence over .security.yml
		if err := applySecurityConfigWithPrecedence(cfg, tmpCfgMigrated, sec); err != nil {
			return nil, fmt.Errorf("failed to merge security config: %w", err)
		}
	default:
		return nil, fmt.Errorf("unsupported config version: %d", versionInfo.Version)
	}

	if passphrase := credential.PassphraseProvider(); passphrase != "" {
		for _, m := range cfg.ModelList {
			for _, k := range m.apiKeys {
				if k != "" && !strings.HasPrefix(k, "enc://") && !strings.HasPrefix(k, "file://") {
					fmt.Fprintf(os.Stderr,
						"picoclaw: warning: model %q has a plaintext api_key; call SaveConfig to encrypt it\n",
						m.ModelName)
					break // Only warn once per model
				}
			}
		}
	}

	if err := env.Parse(cfg); err != nil {
		return nil, err
	}

	if err := resolveAPIKeys(cfg.ModelList, filepath.Dir(path)); err != nil {
		return nil, err
	}

	// Resolve security fields like authToken that may contain file:// references
	if err := resolveSecurityFields(cfg, filepath.Dir(path)); err != nil {
		return nil, err
	}

	// Expand multi-key configs into separate entries for key-level failover
	cfg.ModelList = expandMultiKeyModels(cfg.ModelList)

	// Migrate legacy channel config fields to new unified structures
	cfg.migrateChannelConfigs()

	// Validate model_list for uniqueness and required fields
	if err := cfg.ValidateModelList(); err != nil {
		return nil, err
	}

	// Ensure Workspace has a default if not set
	if cfg.Agents.Defaults.Workspace == "" {
		homePath, _ := os.UserHomeDir()
		if picoclawHome := os.Getenv(EnvHome); picoclawHome != "" {
			homePath = picoclawHome
		} else if homePath != "" {
			homePath = filepath.Join(homePath, pkg.DefaultPicoClawHome)
		}
		cfg.Agents.Defaults.Workspace = filepath.Join(homePath, pkg.WorkspaceName)
	}

	return cfg, nil
}

func makeBackup(path string) error {
	if _, err := os.Stat(path); os.IsNotExist(err) {
		return nil
	}
	// Create backup of the config file before migration
	bakPath := path + ".bak"
	if err := fileutil.CopyFile(path, bakPath, 0o600); err != nil {
		logger.ErrorF("failed to create config backup", map[string]any{"error": err})
		return fmt.Errorf("failed to create config backup: %w", err)
	}
	return nil
}

func copyArray[T any](dst, src *[]T) {
	*dst = make([]T, len(*src))
	copy(*dst, *src)
}

// applySecurityConfig resolves all security references in config
// It checks each field for "ref:" prefixed values and resolves them from .security.yml
func applySecurityConfig(cfg *Config, sec *SecurityConfig) error {
	if sec == nil {
		return nil
	}

	if sec.Web != nil {
		if sec.Web.Brave != nil && len(sec.Web.Brave.APIKeys) > 0 {
			copyArray(&cfg.Tools.Web.Brave.apiKeys, &sec.Web.Brave.APIKeys)
		}

		if sec.Web.Tavily != nil && len(sec.Web.Tavily.APIKeys) > 0 {
			copyArray(&cfg.Tools.Web.Tavily.apiKeys, &sec.Web.Tavily.APIKeys)
		}

		if sec.Web.Perplexity != nil && len(sec.Web.Perplexity.APIKeys) > 0 {
			copyArray(&cfg.Tools.Web.Perplexity.apiKeys, &sec.Web.Perplexity.APIKeys)
		}

		if sec.Web.GLMSearch != nil && sec.Web.GLMSearch.APIKey != "" {
			cfg.Tools.Web.GLMSearch.apiKey = sec.Web.GLMSearch.APIKey
		}

		if sec.Web.BaiduSearch != nil && sec.Web.BaiduSearch.APIKey != "" {
			cfg.Tools.Web.BaiduSearch.apiKey = sec.Web.BaiduSearch.APIKey
		}
	}

	if sec.Skills != nil {
		if sec.Skills.Github != nil && sec.Skills.Github.Token != "" {
			cfg.Tools.Skills.Github.token = sec.Skills.Github.Token
		}

		if sec.Skills.ClawHub != nil && sec.Skills.ClawHub.AuthToken != "" {
			cfg.Tools.Skills.Registries.ClawHub.authToken = sec.Skills.ClawHub.AuthToken
		}
	}

	names := toNameIndex(cfg.ModelList)
	for i, model := range cfg.ModelList {
		// Try exact match first (e.g., "abc:0" -> "abc:0")
		if entry, exists := sec.ModelList[names[i]]; exists {
			copyArray(&model.apiKeys, &entry.APIKeys)
			model.secModelName = names[i]
			continue
		}

		// Try match without index suffix (e.g., "abc" -> "abc")
		// This allows .security.yml to use simpler keys like "test-model" instead of "test-model:0"
		baseName := model.ModelName
		if entry, exists := sec.ModelList[baseName]; exists {
			copyArray(&model.apiKeys, &entry.APIKeys)
			model.secModelName = baseName
			continue
		}
	}

	if sec.Channels != nil {
		// Handle Telegram token
		if sec.Channels.Telegram != nil && sec.Channels.Telegram.Token != "" {
			cfg.Channels.Telegram.token = sec.Channels.Telegram.Token
		}

		// Handle Feishu credentials
		if sec.Channels.Feishu != nil {
			if sec.Channels.Feishu.AppSecret != "" {
				cfg.Channels.Feishu.appSecret = sec.Channels.Feishu.AppSecret
			}
			if sec.Channels.Feishu.EncryptKey != "" {
				cfg.Channels.Feishu.encryptKey = sec.Channels.Feishu.EncryptKey
			}
			if sec.Channels.Feishu.VerificationToken != "" {
				cfg.Channels.Feishu.verificationToken = sec.Channels.Feishu.VerificationToken
			}
		}

		// Handle Discord token
		if sec.Channels.Discord != nil && sec.Channels.Discord.Token != "" {
			cfg.Channels.Discord.token = sec.Channels.Discord.Token
		}

		// Handle Weixin token
		if sec.Channels.Weixin != nil && sec.Channels.Weixin.Token != "" {
			cfg.Channels.Weixin.token = sec.Channels.Weixin.Token
		}

		// Handle DingTalk client secret
		if sec.Channels.DingTalk != nil && sec.Channels.DingTalk.ClientSecret != "" {
			cfg.Channels.DingTalk.clientSecret = sec.Channels.DingTalk.ClientSecret
		}

		// Handle Slack tokens
		if sec.Channels.Slack != nil {
			if sec.Channels.Slack.BotToken != "" {
				cfg.Channels.Slack.botToken = sec.Channels.Slack.BotToken
			}
			if sec.Channels.Slack.AppToken != "" {
				cfg.Channels.Slack.appToken = sec.Channels.Slack.AppToken
			}
		}

		// Handle Matrix access token
		if sec.Channels.Matrix != nil && sec.Channels.Matrix.AccessToken != "" {
			cfg.Channels.Matrix.accessToken = sec.Channels.Matrix.AccessToken
		}

		// Handle LINE credentials
		if sec.Channels.LINE != nil {
			if sec.Channels.LINE.ChannelSecret != "" {
				cfg.Channels.LINE.channelSecret = sec.Channels.LINE.ChannelSecret
			}
			if sec.Channels.LINE.ChannelAccessToken != "" {
				cfg.Channels.LINE.channelAccessToken = sec.Channels.LINE.ChannelAccessToken
			}
		}

		// Handle OneBot access token
		if sec.Channels.OneBot != nil && sec.Channels.OneBot.AccessToken != "" {
			cfg.Channels.OneBot.accessToken = sec.Channels.OneBot.AccessToken
		}

		// Handle WeCom bot secret
		if sec.Channels.WeCom != nil {
			if sec.Channels.WeCom.Secret != "" {
				cfg.Channels.WeCom.secret = sec.Channels.WeCom.Secret
			}
		}

		// Handle Pico channel token
		if sec.Channels.Pico != nil && sec.Channels.Pico.Token != "" {
			cfg.Channels.Pico.token = sec.Channels.Pico.Token
		}

		// Handle IRC passwords
		if sec.Channels.IRC != nil {
			if sec.Channels.IRC.Password != "" {
				cfg.Channels.IRC.password = sec.Channels.IRC.Password
			}
			if sec.Channels.IRC.NickServPassword != "" {
				cfg.Channels.IRC.nickServPassword = sec.Channels.IRC.NickServPassword
			}
			if sec.Channels.IRC.SASLPassword != "" {
				cfg.Channels.IRC.saslPassword = sec.Channels.IRC.SASLPassword
			}
		}

		// Handle QQ app secret
		if sec.Channels.QQ != nil && sec.Channels.QQ.AppSecret != "" {
			cfg.Channels.QQ.appSecret = sec.Channels.QQ.AppSecret
		}
	}

	cfg.security = sec

	return nil
}

// applySecurityConfigWithPrecedence merges security config from tmpCfg (migrated from configV0) and sec (SecurityConfig),
// with tmpCfg taking precedence. It then applies the merged security config to cfg.
func applySecurityConfigWithPrecedence(cfg *Config, tmpCfg *Config, sec *SecurityConfig) error {
	// Get security config from tmpCfg (already extracted during migration)
	var tmpSec *SecurityConfig
	if tmpCfg != nil {
		tmpSec = tmpCfg.security
	}

	// If tmpCfg has no security config, just apply sec directly
	if tmpSec == nil {
		return applySecurityConfig(cfg, sec)
	}

	// Merge sec and tmpSec, with tmpSec (from config.json) taking precedence
	// mergeSecurityConfig(existing, newer) - newer takes precedence
	mergedSec := mergeSecurityConfig(sec, tmpSec)

	// Apply the merged security config to cfg
	return applySecurityConfig(cfg, mergedSec)
}

func toNameIndex(list []*ModelConfig) []string {
	nameList := make([]string, 0, len(list))
	countMap := make(map[string]int)
	for _, model := range list {
		name := model.ModelName
		index := countMap[name]
		nameList = append(nameList, fmt.Sprintf("%s:%d", name, index))
		countMap[name]++
	}
	return nameList
}

// encryptPlaintextAPIKeys returns a copy of models with plaintext api_key values
// encrypted. Returns (nil, nil) when nothing changed (all keys already sealed or
// empty). Returns (nil, error) if any key fails to encrypt — callers must treat
// this as a hard failure to prevent a mixed plaintext/ciphertext state on disk.
// Symmetric counterpart of resolveAPIKeys: both operate purely on []ModelConfig
// and leave JSON marshaling to the caller.
func encryptPlaintextAPIKeys(
	models map[string]ModelSecurityEntry,
	passphrase string,
) (map[string]ModelSecurityEntry, error) {
	sealed := make(map[string]ModelSecurityEntry, len(models))
	changed := false
	for k, m := range models {
		sealedEntry := ModelSecurityEntry{APIKeys: make([]string, len(m.APIKeys))}

		// Encrypt each key in APIKeys
		for i, key := range m.APIKeys {
			if key == "" || strings.HasPrefix(key, "enc://") || strings.HasPrefix(key, "file://") {
				sealedEntry.APIKeys[i] = key
				continue
			}
			encrypted, err := credential.Encrypt(passphrase, "", key)
			if err != nil {
				return nil, fmt.Errorf("cannot seal api_key for model %q: %w", k, err)
			}
			sealedEntry.APIKeys[i] = encrypted
			changed = true
		}

		sealed[k] = sealedEntry
	}
	if !changed {
		return nil, nil
	}
	return sealed, nil
}

// resolveAPIKeys decrypts or dereferences each api_key in models in-place.
// Supports plaintext (no-op), file:// (read from configDir), and enc:// (AES-GCM decrypt).
func resolveAPIKeys(models []*ModelConfig, configDir string) error {
	cr := credential.NewResolver(configDir)
	for i := range models {
		// Resolve APIKeys array
		for j, key := range models[i].apiKeys {
			resolved, err := cr.Resolve(key)
			if err != nil {
				return fmt.Errorf(
					"model_list[%d] (%s): api_keys[%d]: %w",
					i,
					models[i].ModelName,
					j,
					err,
				)
			}
			models[i].apiKeys[j] = resolved
		}
	}
	return nil
}

func (c *Config) migrateChannelConfigs() {
	// Discord: mention_only -> group_trigger.mention_only
	if c.Channels.Discord.MentionOnly && !c.Channels.Discord.GroupTrigger.MentionOnly {
		c.Channels.Discord.GroupTrigger.MentionOnly = true
	}

	// OneBot: group_trigger_prefix -> group_trigger.prefixes
	if len(c.Channels.OneBot.GroupTriggerPrefix) > 0 &&
		len(c.Channels.OneBot.GroupTrigger.Prefixes) == 0 {
		c.Channels.OneBot.GroupTrigger.Prefixes = c.Channels.OneBot.GroupTriggerPrefix
	}
}

func SaveConfig(path string, cfg *Config) error {
	if cfg.security == nil {
		logger.Errorf("config %#v", *cfg)
		if len(cfg.ModelList) > 0 {
			logger.Errorf("model[0] %#v", cfg.ModelList[0])
		}
		logger.ErrorC("config", "security is nil")
		return fmt.Errorf("security is nil")
	}
	cfg.security = normalizeSecurityConfig(cfg.security)
	// Ensure version is always set when saving
	if cfg.Version == 0 {
		cfg.Version = CurrentVersion
	}
	names := toNameIndex(cfg.ModelList)
	for i, m := range cfg.ModelList {
		if m.secDirty {
			if m.secModelName == "" {
				m.secModelName = names[i]
			}
			cfg.security.ModelList[m.secModelName] = ModelSecurityEntry{
				APIKeys: m.apiKeys,
			}
			m.secDirty = false
		}
	}
	if cfg.Channels.Pico.secDirty {
		cfg.security.Channels.Pico = &PicoSecurity{
			Token: cfg.Channels.Pico.Token(),
		}
		cfg.Channels.Pico.secDirty = false
	}
	if cfg.Channels.IRC.secDirty {
		cfg.security.Channels.IRC = &IRCSecurity{
			Password:         cfg.Channels.IRC.password,
			NickServPassword: cfg.Channels.IRC.nickServPassword,
			SASLPassword:     cfg.Channels.IRC.saslPassword,
		}
		cfg.Channels.IRC.secDirty = false
	}
	if cfg.Channels.Telegram.secDirty {
		cfg.security.Channels.Telegram = &TelegramSecurity{
			Token: cfg.Channels.Telegram.Token(),
		}
		cfg.Channels.Telegram.secDirty = false
	}
	if cfg.Channels.Feishu.secDirty {
		cfg.security.Channels.Feishu = &FeishuSecurity{
			AppSecret:         cfg.Channels.Feishu.AppSecret(),
			EncryptKey:        cfg.Channels.Feishu.EncryptKey(),
			VerificationToken: cfg.Channels.Feishu.VerificationToken(),
		}
		cfg.Channels.Feishu.secDirty = false
	}
	if cfg.Channels.Discord.secDirty {
		cfg.security.Channels.Discord = &DiscordSecurity{
			Token: cfg.Channels.Discord.Token(),
		}
		cfg.Channels.Discord.secDirty = false
	}
	if cfg.Channels.Weixin.secDirty {
		cfg.security.Channels.Weixin = &WeixinSecurity{
			Token: cfg.Channels.Weixin.Token(),
		}
		cfg.Channels.Discord.secDirty = false
	}
	if cfg.Channels.QQ.secDirty {
		cfg.security.Channels.QQ = &QQSecurity{
			AppSecret: cfg.Channels.QQ.AppSecret(),
		}
		cfg.Channels.QQ.secDirty = false
	}
	if cfg.Channels.DingTalk.secDirty {
		cfg.security.Channels.DingTalk = &DingTalkSecurity{
			ClientSecret: cfg.Channels.DingTalk.ClientSecret(),
		}
		cfg.Channels.DingTalk.secDirty = false
	}
	if cfg.Channels.Slack.secDirty {
		cfg.security.Channels.Slack = &SlackSecurity{
			BotToken: cfg.Channels.Slack.BotToken(),
			AppToken: cfg.Channels.Slack.AppToken(),
		}
		cfg.Channels.Slack.secDirty = false
	}
	if cfg.Channels.Matrix.secDirty {
		cfg.security.Channels.Matrix = &MatrixSecurity{
			AccessToken: cfg.Channels.Matrix.AccessToken(),
		}
		cfg.Channels.Matrix.secDirty = false
	}
	if cfg.Channels.LINE.secDirty {
		cfg.security.Channels.LINE = &LINESecurity{
			ChannelSecret:      cfg.Channels.LINE.ChannelSecret(),
			ChannelAccessToken: cfg.Channels.LINE.ChannelAccessToken(),
		}
		cfg.Channels.LINE.secDirty = false
	}
	if cfg.Channels.OneBot.secDirty {
		cfg.security.Channels.OneBot = &OneBotSecurity{
			AccessToken: cfg.Channels.OneBot.AccessToken(),
		}
		cfg.Channels.OneBot.secDirty = false
	}
	if cfg.Channels.WeCom.secDirty {
		cfg.security.Channels.WeCom = &WeComSecurity{
			Secret: cfg.Channels.WeCom.Secret(),
		}
		cfg.Channels.WeCom.secDirty = false
	}
	if cfg.Tools.Web.Brave.secDirty {
		cfg.security.Web.Brave = &BraveSecurity{
			APIKeys: cfg.Tools.Web.Brave.APIKeys(),
		}
		cfg.Tools.Web.Brave.secDirty = false
	}
	if cfg.Tools.Web.Tavily.secDirty {
		cfg.security.Web.Tavily = &TavilySecurity{
			APIKeys: cfg.Tools.Web.Tavily.APIKeys(),
		}
		cfg.Tools.Web.Tavily.secDirty = false
	}
	if cfg.Tools.Web.Perplexity.secDirty {
		cfg.security.Web.Perplexity = &PerplexitySecurity{
			APIKeys: cfg.Tools.Web.Perplexity.APIKeys(),
		}
		cfg.Tools.Web.Perplexity.secDirty = false
	}
	if cfg.Tools.Web.GLMSearch.secDirty {
		cfg.security.Web.GLMSearch = &GLMSearchSecurity{
			APIKey: cfg.Tools.Web.GLMSearch.APIKey(),
		}
		cfg.Tools.Web.GLMSearch.secDirty = false
	}
	if cfg.Tools.Web.BaiduSearch.secDirty {
		cfg.security.Web.BaiduSearch = &BaiduSearchSecurity{
			APIKey: cfg.Tools.Web.BaiduSearch.APIKey(),
		}
		cfg.Tools.Web.BaiduSearch.secDirty = false
	}
	if cfg.Tools.Skills.Github.secDirty {
		cfg.security.Skills.Github = &GithubSecurity{
			Token: cfg.Tools.Skills.Github.Token(),
		}
		cfg.Tools.Skills.Github.secDirty = false
	}
	if cfg.Tools.Skills.Registries.ClawHub.secDirty {
		cfg.security.Skills.ClawHub = &ClawHubSecurity{
			AuthToken: cfg.Tools.Skills.Registries.ClawHub.AuthToken(),
		}
		cfg.Tools.Skills.Registries.ClawHub.secDirty = false
	}

	if passphrase := credential.PassphraseProvider(); passphrase != "" {
		sealed, err := encryptPlaintextAPIKeys(cfg.security.ModelList, passphrase)
		if err != nil {
			return err
		}
		if sealed != nil {
			cfg.security.ModelList = sealed
		}
	}
	if err := saveSecurityConfig(securityPath(path), cfg.security); err != nil {
		logger.ErrorCF("config", "cannot save .security.yml", map[string]any{"error": err})
		return err
	}

	// Filter out virtual models before serializing to config file
	nonVirtualModels := make([]*ModelConfig, 0, len(cfg.ModelList))
	for _, m := range cfg.ModelList {
		if !m.isVirtual {
			nonVirtualModels = append(nonVirtualModels, m)
		}
	}
	// Temporarily replace ModelList with filtered version for serialization
	originalModelList := cfg.ModelList
	cfg.ModelList = nonVirtualModels

	data, err := json.MarshalIndent(cfg, "", "  ")
	// Restore original ModelList after serialization
	cfg.ModelList = originalModelList
	if err != nil {
		return err
	}
	logger.Infof("saving config to %s", path)
	return fileutil.WriteFileAtomic(path, data, 0o600)
}

func (c *Config) WorkspacePath() string {
	return expandHome(c.Agents.Defaults.Workspace)
}

func expandHome(path string) string {
	if path == "" {
		return path
	}
	if path[0] == '~' {
		home, _ := os.UserHomeDir()
		if len(path) > 1 && path[1] == '/' {
			return home + path[1:]
		}
		return home
	}
	return path
}

// GetModelConfig returns the ModelConfig for the given model name.
// If multiple configs exist with the same model_name, it uses round-robin
// selection for load balancing. Returns an error if the model is not found.
func (c *Config) GetModelConfig(modelName string) (*ModelConfig, error) {
	matches := c.findMatches(modelName)
	if len(matches) == 0 {
		return nil, fmt.Errorf("model %q not found in model_list or providers", modelName)
	}
	if len(matches) == 1 {
		return matches[0], nil
	}

	// Multiple configs - use round-robin for load balancing
	idx := (rrCounter.Add(1) - 1) % uint64(len(matches))
	return matches[idx], nil
}

// findMatches finds all ModelConfig entries with the given model_name.
func (c *Config) findMatches(modelName string) []*ModelConfig {
	var matches []*ModelConfig
	for i := range c.ModelList {
		if c.ModelList[i].ModelName == modelName {
			matches = append(matches, c.ModelList[i])
		}
	}
	return matches
}

// ValidateModelList validates all ModelConfig entries in the model_list.
// It checks that each model config is valid.
// Note: Multiple entries with the same model_name are allowed for load balancing.
func (c *Config) ValidateModelList() error {
	for i := range c.ModelList {
		if err := c.ModelList[i].Validate(); err != nil {
			return fmt.Errorf("model_list[%d]: %w", i, err)
		}
	}
	return nil
}

func (c *Config) SecurityCopyFrom(cfg *Config) {
	c.security = cfg.security
	if c.security != nil {
		if err := applySecurityConfig(c, c.security); err != nil {
			logger.Errorf("failed to apply security config in SecurityCopyFrom: %v", err)
		}
	}
}

// ApplySecurity re-applies the stored security config to populate private fields (tokens, API keys, etc.).
// Call this after SecurityCopyFrom when you need private fields to be accessible for validation or use.
func (c *Config) ApplySecurity() error {
	return applySecurityConfig(c, c.security)
}

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
}

// resolveSecurityFields resolves file:// and enc:// references in security-sensitive fields
// like authToken and token that are not part of ModelConfig's apiKeys
func resolveSecurityFields(cfg *Config, configDir string) error {
	cr := credential.NewResolver(configDir)

	// Resolve Web tool API keys - set apiKey field to first resolved apiKeys entry
	if len(cfg.Tools.Web.Brave.apiKeys) > 0 {
		keys := cfg.Tools.Web.Brave.apiKeys
		for i, key := range keys {
			resolved, err := cr.Resolve(key)
			if err != nil {
				return fmt.Errorf("brave api_keys[%d]: %w", i, err)
			}
			keys[i] = resolved
		}
	}

	if len(cfg.Tools.Web.Tavily.apiKeys) > 0 {
		keys := cfg.Tools.Web.Tavily.apiKeys
		for i, key := range keys {
			resolved, err := cr.Resolve(key)
			if err != nil {
				return fmt.Errorf("tavily api_keys[%d]: %w", i, err)
			}
			keys[i] = resolved
		}
	}

	if len(cfg.Tools.Web.Perplexity.apiKeys) > 0 {
		keys := cfg.Tools.Web.Perplexity.apiKeys
		for i, key := range keys {
			resolved, err := cr.Resolve(key)
			if err != nil {
				return fmt.Errorf("perplexity api_keys[%d]: %w", i, err)
			}
			keys[i] = resolved
		}
	}

	// GLMSearch has a private apiKey field
	if cfg.Tools.Web.GLMSearch.apiKey != "" {
		resolved, err := cr.Resolve(cfg.Tools.Web.GLMSearch.apiKey)
		if err != nil {
			return fmt.Errorf("glm api_key: %w", err)
		}
		cfg.Tools.Web.GLMSearch.apiKey = resolved
	}

	// Resolve Skills tokens
	if cfg.Tools.Skills.Github.token != "" {
		resolved, err := cr.Resolve(cfg.Tools.Skills.Github.token)
		if err != nil {
			return fmt.Errorf("github token: %w", err)
		}
		cfg.Tools.Skills.Github.token = resolved
	}

	if cfg.Tools.Skills.Registries.ClawHub.authToken != "" {
		resolved, err := cr.Resolve(cfg.Tools.Skills.Registries.ClawHub.authToken)
		if err != nil {
			return fmt.Errorf("clawhub auth_token: %w", err)
		}
		cfg.Tools.Skills.Registries.ClawHub.authToken = resolved
	}

	return nil
}

// expandMultiKeyModels expands ModelConfig entries with multiple API keys into
// separate entries for key-level failover. Each key gets its own ModelConfig entry,
// and the original entry's fallbacks are set up to chain through the expanded entries.
//
// Example: {"model_name": "gpt-4", "api_keys": ["k1", "k2", "k3"]}
// Becomes:
//   - {"model_name": "gpt-4", "api_keys": ["k1"], "fallbacks": ["gpt-4__key_1", "gpt-4__key_2"]}
//   - {"model_name": "gpt-4__key_1", "api_keys": {"k2"}}
//   - {"model_name": "gpt-4__key_2", "api_keys": {"k3"}}
func expandMultiKeyModels(models []*ModelConfig) []*ModelConfig {
	var expanded []*ModelConfig

	for _, m := range models {
		keys := MergeAPIKeys("", m.apiKeys)

		// Single key or no keys: keep as-is
		if len(keys) <= 1 {
			m.apiKeys = keys
			expanded = append(expanded, m)
			continue
		}

		// Multiple keys: expand
		originalName := m.ModelName

		// Create entries for additional keys (key_1, key_2, ...)
		var fallbackNames []string
		for i := 1; i < len(keys); i++ {
			suffix := fmt.Sprintf("__key_%d", i)
			expandedName := originalName + suffix

			// Create a copy for the additional key
			additionalEntry := &ModelConfig{
				ModelName:      expandedName,
				Model:          m.Model,
				APIBase:        m.APIBase,
				apiKeys:        []string{keys[i]},
				Proxy:          m.Proxy,
				AuthMethod:     m.AuthMethod,
				ConnectMode:    m.ConnectMode,
				Workspace:      m.Workspace,
				RPM:            m.RPM,
				MaxTokensField: m.MaxTokensField,
				RequestTimeout: m.RequestTimeout,
				ThinkingLevel:  m.ThinkingLevel,
				ExtraBody:      m.ExtraBody,
				isVirtual:      true,
			}
			expanded = append(expanded, additionalEntry)
			fallbackNames = append(fallbackNames, expandedName)
		}

		// Create the primary entry with first key and fallbacks
		primaryEntry := &ModelConfig{
			ModelName:      originalName,
			Model:          m.Model,
			APIBase:        m.APIBase,
			Proxy:          m.Proxy,
			AuthMethod:     m.AuthMethod,
			ConnectMode:    m.ConnectMode,
			Workspace:      m.Workspace,
			RPM:            m.RPM,
			MaxTokensField: m.MaxTokensField,
			RequestTimeout: m.RequestTimeout,
			ThinkingLevel:  m.ThinkingLevel,
			ExtraBody:      m.ExtraBody,
			apiKeys:        []string{keys[0]},
		}

		// Prepend new fallbacks to existing ones
		if len(fallbackNames) > 0 {
			primaryEntry.Fallbacks = append(fallbackNames, m.Fallbacks...)
		} else if len(m.Fallbacks) > 0 {
			primaryEntry.Fallbacks = m.Fallbacks
		}

		expanded = append(expanded, primaryEntry)
	}

	return expanded
}

func (t *ToolsConfig) IsToolEnabled(name string) bool {
	switch name {
	case "web":
		return t.Web.Enabled
	case "cron":
		return t.Cron.Enabled
	case "exec":
		return t.Exec.Enabled
	case "skills":
		return t.Skills.Enabled
	case "media_cleanup":
		return t.MediaCleanup.Enabled
	case "append_file":
		return t.AppendFile.Enabled
	case "edit_file":
		return t.EditFile.Enabled
	case "find_skills":
		return t.FindSkills.Enabled
	case "i2c":
		return t.I2C.Enabled
	case "install_skill":
		return t.InstallSkill.Enabled
	case "list_dir":
		return t.ListDir.Enabled
	case "message":
		return t.Message.Enabled
	case "read_file":
		return t.ReadFile.Enabled
	case "spawn":
		return t.Spawn.Enabled
	case "spawn_status":
		return t.SpawnStatus.Enabled
	case "spi":
		return t.SPI.Enabled
	case "subagent":
		return t.Subagent.Enabled
	case "web_fetch":
		return t.WebFetch.Enabled
	case "send_file":
		return t.SendFile.Enabled
	case "write_file":
		return t.WriteFile.Enabled
	case "mcp":
		return t.MCP.Enabled
	default:
		return true
	}
}
