package profile

import (
	"crypto/ed25519"
	"encoding/base64"
	"fmt"
	"log/slog"
	"os"
	"path/filepath"
	"runtime"
	"strings"

	"github.com/pingcap/failpoint"
	"go.yaml.in/yaml/v3"

	"github.com/tidbcloud/AgentTeam/desktop/internal/types"
)

// Application-level constants for default configuration values.
const (
	// AppName is the application name used for config directory and env prefix.
	AppName = "agentteam"
	// DefaultHost is the default bind address for the dispatcher server.
	DefaultHost = "127.0.0.1"
	// DefaultPort is the default listen port for the dispatcher server.
	// 0 lets the OS select an available loopback port on first daemon start.
	DefaultPort = 0
	// DefaultLogLevel is the default slog level when none is configured.
	DefaultLogLevel = "info"
	// VerboseEnvKey is the environment variable name for enabling verbose output.
	VerboseEnvKey = "AGENTTEAM_VERBOSE"
	// DevelEnvKey is the environment variable name for development mode.
	// When set to host:port (e.g., "127.0.0.1:5173"), the dashboard URL
	// points to the specified address instead of the configured server.
	DevelEnvKey = "AGENTTEAM_DEV"
	// ConfigDirEnvKey is the environment variable name for overriding the
	// configuration directory. When set, all profile YAML files (agents,
	// channels, skills, apps, and related state) are read from and
	// written to this directory instead of ~/.agentteam/.
	ConfigDirEnvKey = "AGENTTEAM_CONFIG_DIR"
)

var (
	// Optional test overrides. When empty, Path and ConfigDir resolve dynamically
	// from failpoints, environment, and the current home directory.
	configPath string
	configDir  string
	verbose    bool
)

// Config is the top-level configuration structure containing server settings.
type Config struct {
	Host         string   `yaml:"host"`
	Port         int      `yaml:"port"`
	PIDFile      string   `yaml:"pid_file,omitempty"`
	Language     string   `yaml:"language,omitempty"`
	DefaultAgent string   `yaml:"default_agent,omitempty"`
	UI           UI       `yaml:"ui,omitempty"`
	Log          Log      `yaml:"log,omitempty"`
	Security     Security `yaml:"security,omitempty"`
	LLM          LLM      `yaml:"llm,omitempty"`
}

// UI holds user interface preferences persisted across restarts.
type UI struct {
	Theme                       string `yaml:"theme,omitempty"`
	ColorScheme                 string `yaml:"color_scheme,omitempty"`
	Language                    string `yaml:"language,omitempty"`
	OptimizeBackgroundResources *bool  `yaml:"optimize_background_resources,omitempty"`
}

// Log holds log file configuration.
type Log struct {
	Dir   string `yaml:"dir,omitempty"`
	File  string `yaml:"file,omitempty"`
	Level string `yaml:"level,omitempty"`
}

// Security holds Ed25519 public/private key pair for API key signing.
type Security struct {
	PublicKey  Base64Bytes `yaml:"public_key,omitempty"`
	PrivateKey Base64Bytes `yaml:"private_key,omitempty"`
}

// LLM holds legacy default provider configuration retained for older config
// files. Runtime-native settings and agent inline LLM fields are the active
// provider configuration surfaces.
type LLM struct {
	BaseURL string `yaml:"base_url,omitempty"`
	APIKey  string `yaml:"api_key,omitempty"`
	Model   string `yaml:"model,omitempty"`
}

// Base64Bytes is a []byte that marshals/unmarshals as a base64 string in YAML.
type Base64Bytes []byte

// MarshalText encodes b as standard base64 for YAML/TOML text serializers.
func (b Base64Bytes) MarshalText() ([]byte, error) {
	if len(b) == 0 {
		return nil, nil
	}
	enc := make([]byte, base64.StdEncoding.EncodedLen(len(b)))
	base64.StdEncoding.Encode(enc, b)
	return enc, nil
}

// UnmarshalText decodes a standard base64 text value into b.
func (b *Base64Bytes) UnmarshalText(text []byte) error {
	if len(text) == 0 {
		*b = nil
		return nil
	}
	dec := make([]byte, base64.StdEncoding.DecodedLen(len(text)))
	n, err := base64.StdEncoding.Decode(dec, text)
	if err != nil {
		return err
	}
	*b = dec[:n]
	return nil
}

// AgentTeamServer defines settings for one local AgentTeam server auth entry.
//
// New entries are persisted in servers.yaml under the "servers:" key. Legacy
// channels.yaml entries are migrated automatically and keep their original IDs.
type AgentTeamServer struct {
	// ID is the unique local server identifier, persisted in the config file.
	// New IDs use the srv_ prefix. Migrated legacy IDs are preserved.
	ID string `yaml:"id,omitempty"`
	// Revision is an internal durable mutation token. Every profile mutation
	// advances it so rollback paths can distinguish a current value from an ABA
	// write that happens to contain the same user-visible fields.
	Revision uint64 `yaml:"revision,omitempty" json:"-"`
	// Enabled controls whether this server auth is active.
	Enabled bool `yaml:"enabled"`
	// Name is an optional display name for the server auth.
	Name string `yaml:"name,omitempty"`
	// Description is an optional description for the server auth.
	Description string `yaml:"description,omitempty"`
	// AgentID optionally binds this server auth to a specific agent.
	AgentID string `yaml:"agent_id,omitempty"`
	// ServerURL is the AgentTeam server base URL.
	ServerURL string `yaml:"server_url,omitempty"`
	// WorkspaceID is resolved by the server during login/token exchange.
	WorkspaceID string `yaml:"workspace_id,omitempty"`
	// MachineID is resolved by the server during login/token exchange when available.
	MachineID string `yaml:"machine_id,omitempty"`
	// APIKey is the machine API key used by the daemon.
	APIKey string `yaml:"api_key,omitempty"`
	// AuthToken stores the last channel-level auth token fallback. It is kept
	// for compatibility with existing persisted config.
	AuthToken string `yaml:"auth_token,omitempty"`
	// ConnectionStatus stores local reconnect markers such as invalid_machine_api_key.
	ConnectionStatus string `yaml:"connection_status,omitempty"`

	// Config is a legacy in-memory compatibility field for code paths still
	// being migrated from channels.yaml. It is intentionally not persisted to
	// servers.yaml.
	Config types.RawMessage `yaml:"-" json:"-"`
}

// AgentRuntimeConfig defines runtime-level settings for an Agent,
// bridging the Agent model with the actual Runtime process lifecycle.
// This is used by the daemon runner to convert an Agent + Runtime into
// concrete agent backend configuration.
type AgentRuntimeConfig struct {
	Runtime     string            `yaml:"runtime,omitempty"`
	Enabled     bool              `yaml:"enabled"`
	Binary      string            `yaml:"binary,omitempty"`
	Args        []string          `yaml:"args,omitempty"`
	Port        int               `yaml:"port,omitempty"`
	Health      HealthConfig      `yaml:"health,omitempty"`
	Env         map[string]string `yaml:"env,omitempty"`
	AutoRestart bool              `yaml:"auto_restart,omitempty"`
	MaxRestarts int               `yaml:"max_restarts,omitempty"`
}

// HealthConfig defines how to perform health checks for a runtime,
// including the check endpoint path, interval, and timeout.
type HealthConfig struct {
	Path     string `yaml:"path"`
	Interval string `yaml:"interval"`
	Timeout  string `yaml:"timeout"`
}

// DefaultConfig returns a Config populated with sensible defaults.
func DefaultConfig() *Config {
	cfgDir := ConfigDir()

	return &Config{
		Host:    DefaultHost,
		Port:    DefaultPort,
		PIDFile: filepath.Join(cfgDir, "agentteam.pid"),
		Log: Log{
			Dir:   defaultLogDir(),
			File:  "agentteam.log",
			Level: DefaultLogLevel,
		},
		Language: "zh-CN",
	}
}

// OptimizeBackgroundResources returns the persisted background-resource
// optimisation preference. It defaults to true so existing users get the
// lower-resource window behaviour without editing config.
func OptimizeBackgroundResources(cfg *Config) bool {
	if cfg == nil || cfg.UI.OptimizeBackgroundResources == nil {
		return true
	}
	return *cfg.UI.OptimizeBackgroundResources
}

// NormalizeLanguage maps persisted UI language aliases to backend i18n bundle
// identifiers. The frontend may send "zh" while the Go i18n bundle is
// registered as "zh-CN".
func NormalizeLanguage(lang string) string {
	switch strings.ToLower(strings.TrimSpace(lang)) {
	case "en", "en-us", "en_us":
		return "en"
	case "ja", "ja-jp", "ja_jp":
		return "ja"
	case "zh", "zh-cn", "zh_cn", "zh-hans", "zh_hans":
		return "zh-CN"
	default:
		return ""
	}
}

// EffectiveLanguage returns the language Desktop backend UI surfaces should
// use. The newer UI language preference wins over the legacy top-level field.
func EffectiveLanguage(cfg *Config) string {
	if cfg == nil {
		return DefaultConfig().Language
	}
	if lang := NormalizeLanguage(cfg.UI.Language); lang != "" {
		return lang
	}
	if lang := NormalizeLanguage(cfg.Language); lang != "" {
		return lang
	}
	return DefaultConfig().Language
}

// defaultLogDir returns the OS-appropriate default log directory.
func defaultLogDir() string {
	homeDir, _ := os.UserHomeDir()
	switch runtime.GOOS {
	case "darwin":
		return filepath.Join(homeDir, "Library", "Logs", AppName)
	case "linux":
		return filepath.Join(os.TempDir(), AppName, "log")
	case "windows":
		localAppData := os.Getenv("LOCALAPPDATA")
		if localAppData == "" {
			localAppData = filepath.Join(homeDir, "AppData", "Local")
		}
		return filepath.Join(localAppData, AppName, "Logs")
	default:
		return filepath.Join(homeDir, "."+AppName)
	}
}

// LogFile returns the resolved log file path.
// dir defaults to the OS-specific default directory; file defaults to "agentteam.log".
func (c *Config) LogFile() string {
	dir := c.Log.Dir
	if dir == "" {
		dir = defaultLogDir()
	}
	file := c.Log.File
	if file == "" {
		file = "agentteam.log"
	}
	if filepath.IsAbs(file) {
		return file
	}
	return filepath.Join(dir, file)
}

func init() {
	v := os.Getenv(VerboseEnvKey)
	verbose = v == "1" || v == "true" || v == "yes"
}

func resolvedConfigDir() string {
	if configDir != "" {
		return configDir
	}
	if val, err := failpoint.Eval("agentteam-config-dir"); err == nil && val != nil {
		if dir, ok := val.(string); ok && strings.TrimSpace(dir) != "" {
			return dir
		}
	}
	if d := os.Getenv(ConfigDirEnvKey); d != "" {
		return d
	}
	homeDir, _ := os.UserHomeDir()
	return filepath.Join(homeDir, "."+AppName)
}

func resolvedConfigPath() string {
	if configPath != "" {
		return configPath
	}
	return filepath.Join(resolvedConfigDir(), configFileName)
}

// Path returns the resolved configuration file path.
func Path() string {
	return resolvedConfigPath()
}

// ConfigDir returns the resolved configuration directory path.
func ConfigDir() string {
	return resolvedConfigDir()
}

// AgentWorkDir returns the per-agent workspace directory under ConfigDir.
// It stores the materialized Cloud Memory identity plus the agent-local
// MEMORY.md, state/, kb/, archive/, and AgentTeam-owned .runtime/ files.
func AgentWorkDir(agentID string) string {
	return filepath.Join(ConfigDir(), "agents", agentID)
}

// ExePath returns the absolute path of the current agentteam executable.
func ExePath() (string, error) {
	return os.Executable()
}

// Load reads the configuration from the specified file path (or the default
// location if empty), merges it with defaults, and returns the resulting Config.
func Load(cfgPath string) (*Config, error) {
	cfg := DefaultConfig()

	if cfgPath == "" {
		cfgPath = Path()
	}

	data, err := os.ReadFile(cfgPath)
	if err != nil {
		if os.IsNotExist(err) {
			return cfg, nil
		}
		return nil, fmt.Errorf("failed to read config file %s: %w", cfgPath, err)
	}
	if err := os.Chmod(cfgPath, 0o600); err != nil {
		return nil, fmt.Errorf("failed to secure config file %s: %w", cfgPath, err)
	}

	if err := yaml.Unmarshal(data, cfg); err != nil {
		return nil, fmt.Errorf("failed to parse config: %w", err)
	}

	return cfg, nil
}

// LoadOrDefaults loads configuration from cfgPath, or Path() when cfgPath is empty. If the file does not exist, it returns DefaultConfig without writing to disk.
func LoadOrDefaults(cfgPath string) (*Config, error) {
	if cfgPath == "" {
		cfgPath = Path()
	}

	cfg, err := Load(cfgPath)
	if err != nil {
		if os.IsNotExist(err) {
			cfg = DefaultConfig()
			return cfg, nil
		}
		return nil, err
	}

	return cfg, nil
}

// EnsureDir creates the configuration directory with appropriate
// permissions if it does not already exist.
func EnsureDir() error {
	return os.MkdirAll(ConfigDir(), 0755)
}

// IsVerbose reports whether verbose output is enabled.
func IsVerbose() bool {
	return verbose
}

// DashboardURL returns the dashboard URL based on the server config.
// If AGENTTEAM_DEV is set (e.g., "127.0.0.1:5173"), it returns a URL
// using that address; otherwise it uses the configured server host and port.
func DashboardURL(cfg *Config) string {
	if devel := os.Getenv(DevelEnvKey); devel != "" {
		return fmt.Sprintf("http://%s/", devel)
	}
	return fmt.Sprintf("http://%s:%d/", cfg.Host, cfg.Port)
}

// Save writes the configuration to the specified file path as YAML.
// It creates parent directories as needed and serializes writes through the
// profile directory lock.
func Save(cfg *Config, cfgPath string) error {
	if cfgPath == "" {
		cfgPath = Path()
	}

	dir := filepath.Dir(cfgPath)
	if err := os.MkdirAll(dir, 0755); err != nil {
		return fmt.Errorf("failed to create config directory: %w", err)
	}

	if _, err := saveConfigWithLock(cfg, cfgPath); err != nil {
		return err
	}

	return nil
}

// UpdateConfig loads the latest config under the profile lock, applies update,
// persists the result, and returns the saved snapshot.
func UpdateConfig(cfgPath string, update func(*Config) error) (*Config, error) {
	if update == nil {
		return nil, fmt.Errorf("config update is nil")
	}
	if cfgPath == "" {
		cfgPath = Path()
	}

	dir := filepath.Dir(cfgPath)
	if err := os.MkdirAll(dir, 0755); err != nil {
		return nil, fmt.Errorf("failed to create config directory: %w", err)
	}

	var saved *Config
	err := withConfigDirLock(dir, func() error {
		var err error
		saved, err = updateConfigLocked(cfgPath, update)
		return err
	})
	if err != nil {
		return nil, err
	}
	return saved, nil
}

func updateConfigLocked(cfgPath string, update func(*Config) error) (*Config, error) {
	cfg, err := Load(cfgPath)
	if err != nil {
		return nil, err
	}
	if err := update(cfg); err != nil {
		return nil, err
	}
	if err := saveYAMLFileLockedMode(cfg, cfgPath, nil, 0o600); err != nil {
		return nil, fmt.Errorf("failed to write config file: %w", err)
	}
	return cloneConfig(cfg), nil
}

func saveConfigWithLock(cfg *Config, cfgPath string) (*Config, error) {
	var saved *Config
	err := withConfigDirLock(filepath.Dir(cfgPath), func() error {
		var err error
		saved, err = saveConfigLocked(cfg, cfgPath)
		return err
	})
	return saved, err
}

func saveConfigLocked(cfg *Config, cfgPath string) (*Config, error) {
	if cfg == nil {
		return nil, fmt.Errorf("config is nil")
	}

	var latest *Config
	if _, err := os.Stat(cfgPath); err == nil {
		loaded, err := Load(cfgPath)
		if err != nil {
			return nil, err
		}
		latest = loaded
	} else if !os.IsNotExist(err) {
		return nil, fmt.Errorf("failed to stat config file %s: %w", cfgPath, err)
	}

	merged := mergeConfigForSave(latest, cfg)
	if err := saveYAMLFileLockedMode(merged, cfgPath, nil, 0o600); err != nil {
		return nil, fmt.Errorf("failed to write config file: %w", err)
	}
	return merged, nil
}

func mergeConfigForSave(latest, incoming *Config) *Config {
	merged := cloneConfig(incoming)
	if latest == nil {
		return merged
	}

	defaults := DefaultConfig()
	if merged.Host == "" || (merged.Host == defaults.Host && latest.Host != "" && latest.Host != defaults.Host) {
		merged.Host = latest.Host
	}
	if merged.Port == defaults.Port && latest.Port > 0 && latest.Port != defaults.Port {
		merged.Port = latest.Port
	}
	if merged.PIDFile == "" || (merged.PIDFile == defaults.PIDFile && latest.PIDFile != "" && latest.PIDFile != defaults.PIDFile) {
		merged.PIDFile = latest.PIDFile
	}
	if merged.Language == "" || (merged.Language == defaults.Language && latest.Language != "" && latest.Language != defaults.Language) {
		merged.Language = latest.Language
	}
	if merged.DefaultAgent == "" && latest.DefaultAgent != "" {
		merged.DefaultAgent = latest.DefaultAgent
	}
	if isZeroSecurity(merged.Security) || shouldPreserveLatestSecurity(latest.Security, merged.Security) {
		merged.Security = cloneSecurity(latest.Security)
	}
	if isZeroLLM(merged.LLM) {
		merged.LLM = latest.LLM
	}
	mergeUIForSave(&merged.UI, latest.UI)
	mergeLogForSave(&merged.Log, latest.Log, defaults.Log)
	return merged
}

func mergeUIForSave(ui *UI, latest UI) {
	if ui.Theme == "" {
		ui.Theme = latest.Theme
	}
	if ui.ColorScheme == "" {
		ui.ColorScheme = latest.ColorScheme
	}
	if ui.Language == "" {
		ui.Language = latest.Language
	}
	if ui.OptimizeBackgroundResources == nil && latest.OptimizeBackgroundResources != nil {
		value := *latest.OptimizeBackgroundResources
		ui.OptimizeBackgroundResources = &value
	}
}

func mergeLogForSave(logCfg *Log, latest, defaults Log) {
	if logCfg.Dir == "" || (logCfg.Dir == defaults.Dir && latest.Dir != "" && latest.Dir != defaults.Dir) {
		logCfg.Dir = latest.Dir
	}
	if logCfg.File == "" || (logCfg.File == defaults.File && latest.File != "" && latest.File != defaults.File) {
		logCfg.File = latest.File
	}
	if logCfg.Level == "" || (logCfg.Level == defaults.Level && latest.Level != "" && latest.Level != defaults.Level) {
		logCfg.Level = latest.Level
	}
}

func cloneConfig(cfg *Config) *Config {
	if cfg == nil {
		return nil
	}
	clone := *cfg
	clone.Security = cloneSecurity(cfg.Security)
	if cfg.UI.OptimizeBackgroundResources != nil {
		value := *cfg.UI.OptimizeBackgroundResources
		clone.UI.OptimizeBackgroundResources = &value
	}
	return &clone
}

func cloneSecurity(security Security) Security {
	return Security{
		PublicKey:  cloneBase64Bytes(security.PublicKey),
		PrivateKey: cloneBase64Bytes(security.PrivateKey),
	}
}

func cloneBase64Bytes(in Base64Bytes) Base64Bytes {
	if len(in) == 0 {
		return nil
	}
	out := make([]byte, len(in))
	copy(out, in)
	return out
}

func isZeroSecurity(security Security) bool {
	return len(security.PublicKey) == 0 && len(security.PrivateKey) == 0
}

func isValidSecurity(security Security) bool {
	return len(security.PublicKey) == ed25519.PublicKeySize && len(security.PrivateKey) == ed25519.PrivateKeySize
}

func shouldPreserveLatestSecurity(latest, incoming Security) bool {
	if isZeroSecurity(latest) {
		return false
	}
	if isValidSecurity(latest) {
		return true
	}
	return !isValidSecurity(incoming)
}

func isZeroLLM(llm LLM) bool {
	return llm.BaseURL == "" && llm.APIKey == "" && llm.Model == ""
}

// ParseLogLevel converts a log level string to a slog.Level.
// Accepts "debug", "info", "warn", "error". Falls back to slog.LevelInfo.
func ParseLogLevel(level string) slog.Level {
	switch strings.ToLower(strings.TrimSpace(level)) {
	case "debug":
		return slog.LevelDebug
	case "warn", "warning":
		return slog.LevelWarn
	case "error":
		return slog.LevelError
	default:
		return slog.LevelInfo
	}
}
