//! Configuration types for the Dirigent core runtime
//!
//! This module defines configuration structures used throughout the dirigent_core
//! system for runtime setup and connector management.

use crate::types::{ConnectorKind, UserId};
use crate::CoreError;
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use tracing::{debug, info, warn};

/// Core runtime configuration
///
/// Defines the configuration for the CoreRuntime, including connector
/// configurations, archive settings, and project management.
///
/// # Example
///
/// ```
/// use dirigent_core::CoreConfig;
/// use std::path::PathBuf;
///
/// let config = CoreConfig {
///     runtime_working_dir: PathBuf::from("/my/project"),
///     connectors: vec![],
///     ..CoreConfig::default()
/// };
/// ```
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CoreConfig {
    /// Web server listen port. Read by the server at startup and set as the
    /// `PORT` env var before Dioxus resolves the listen address.
    /// Precedence: `PORT` env var > this field > Dioxus default (8080).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub port: Option<u16>,

    /// Default working directory for connector processes (e.g., spawning Claude).
    /// Used when no project or connector-specific working directory is configured.
    /// Relative paths are resolved against the config file's directory.
    #[serde(alias = "project_dir")]
    pub runtime_working_dir: PathBuf,

    /// Connector configurations to be loaded on startup
    ///
    /// These connectors will be automatically created and started when the
    /// runtime initializes. This enables persistent connector setups across
    /// server restarts.
    pub connectors: Vec<ConnectorConfig>,

    /// Archive root directory (deprecated in config file — use DIRIGENT_DATA_DIR instead)
    /// Kept deserializable for backward compatibility but no longer written.
    #[serde(default, skip_serializing)]
    pub archive_root: Option<PathBuf>,

    /// Archive backend declarations. Phase 3+ replaces `archive_root` with
    /// this `[[archives]]`-array config. When both are set, `archives` wins.
    ///
    /// Stored as typed `ArchiveConfig` on server builds and as raw
    /// `serde_json::Value` on WASM/non-server builds (archivist types pull in
    /// the full coordinator which is server-only).
    #[cfg(feature = "server")]
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub archives: Vec<dirigent_archivist::registry::ArchiveConfig>,

    /// Archive backend declarations (opaque on non-server builds).
    #[cfg(not(feature = "server"))]
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub archives: Vec<serde_json::Value>,

    /// Root directory for project storage (deprecated in config file — use DIRIGENT_DATA_DIR instead)
    /// Kept deserializable for backward compatibility but no longer written.
    #[serde(default, skip_serializing)]
    pub projects_root: Option<PathBuf>,

    /// ACP Server configuration
    ///
    /// If specified, configures the ACP Server for accepting incoming
    /// Agent-Client Protocol connections. When enabled, external ACP agents
    /// can connect to this Dirigent instance.
    ///
    /// If None, no ACP Server will be started (disabled).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub acp_server: Option<AcpServerConfig>,

    /// List of Zed agent connector titles that the user has explicitly dismissed
    ///
    /// When a user removes a Zed-detected connector, its title is added
    /// to this list so it won't be re-added on next startup by auto-detection.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub dismissed_zed_agents: Vec<String>,

    /// Task definitions for background process management
    ///
    /// These tasks can be configured via the UI or TOML file.
    /// Tasks with `run_at_startup = true` will be automatically started.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub tasks: Vec<TaskConfig>,

    /// Named accounts parsed from [accounts.*] sections.
    ///
    /// Each key is the account name used elsewhere in the config.
    /// Identity, credentials, and connection properties live here.
    ///
    /// Example in dirigent.toml:
    /// ```toml
    /// [accounts.matrix-bot]
    /// type = "matrix"
    /// username = "dirigent_bot"
    /// display_name = "Dirigent Bot"
    /// homeserver = "https://matrix.example.com"
    ///
    /// [accounts.matrix-bot.credentials.password]
    /// type = "env"
    /// value = "DIRIGENT_MATRIX_PASSWORD"
    /// ```
    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
    pub accounts: HashMap<String, dirigent_auth::Account>,

    /// Matrix sharing behavior configuration.
    ///
    /// References an account by name for identity/credentials.
    /// If None, Matrix session sharing is disabled.
    ///
    /// Stored as typed `MatrixBehaviorConfig` on server builds and as
    /// raw `serde_json::Value` on WASM/non-server builds to avoid pulling
    /// in the heavy `matrix-sdk` dependency.
    ///
    /// Example in dirigent.toml:
    /// ```toml
    /// [matrix]
    /// account = "matrix-bot"
    /// default_invite = ["@user:example.com"]
    /// store_path = "matrix/bot/store"
    /// ```
    #[cfg(feature = "server")]
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub matrix: Option<dirigent_matrix::MatrixBehaviorConfig>,

    /// Matrix sharing behavior configuration (opaque on non-server builds).
    #[cfg(not(feature = "server"))]
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub matrix: Option<serde_json::Value>,

    /// `[[streams]]` config blocks — declarative stream attachments that the
    /// runtime wires up at boot via `CoreRuntime::attach_stream`.
    ///
    /// `StreamsConfig` uses `#[serde(rename = "streams")]` on its `entries`
    /// field, so `#[serde(flatten)]` lifts it into the top-level TOML as
    /// `[[streams]]` tables. The inner `entries` field has its own
    /// `#[serde(default)]`, so omitting `[[streams]]` deserialises fine.
    #[serde(flatten)]
    pub streams: crate::sharing::StreamsConfig,

    /// Path the config was loaded from (not serialized)
    ///
    /// Tracked so that `save_config` writes back to the same file that was
    /// originally loaded, preserving the user's chosen format and location.
    #[serde(skip)]
    pub config_source_path: Option<PathBuf>,

    /// Resolved runtime working directory (computed at load time, never serialized).
    ///
    /// Populated by `resolve_paths()`. Use `effective_working_dir()` to read —
    /// it returns this value when set, falling back to `runtime_working_dir`.
    #[serde(skip)]
    pub resolved_working_dir: Option<PathBuf>,
}

/// Configuration for the ACP Server
///
/// Defines settings for the ACP Server which accepts incoming ACP connections
/// from external agents.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AcpServerConfig {
    /// Whether the ACP Server is enabled
    pub enabled: bool,

    /// Port to listen on
    /// - None: Integrated at /acp on main server (default)
    /// - Some(port): Separate server on specified port
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub port: Option<u16>,

    /// Allowed origins for CORS (None = all origins allowed)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub allowed_origins: Option<Vec<String>>,

    /// Maximum concurrent connections (default: 100)
    pub max_connections: usize,

    /// Default connector ID for routing incoming sessions (None = use Gateway)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub default_connector_id: Option<String>,
}

/// Configuration for a background task (serialized to TOML)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskConfig {
    /// Human-readable title for display in the UI
    pub title: String,
    /// Unique machine-friendly identifier
    pub name: String,
    /// The command to execute (e.g., "cargo", "npm")
    pub command: String,
    /// Arguments passed to the command
    #[serde(default)]
    pub args: Vec<String>,
    /// Working directory for the process (None = inherit runtime working dir)
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub working_directory: Option<PathBuf>,
    /// Whether to start this task automatically when the runtime starts
    #[serde(default)]
    pub run_at_startup: bool,
    /// Maximum number of output lines to keep in memory
    #[serde(default = "default_task_buffer_size")]
    pub buffer_size: usize,
    /// Whether to persist output to disk
    #[serde(default = "default_task_persist")]
    pub persist_to_disk: bool,
    /// Whether to rotate (rename) previous output files on restart
    #[serde(default)]
    pub rotate_previous: bool,
    /// Additional environment variables for the process
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub env: Vec<(String, String)>,
}

fn default_task_buffer_size() -> usize {
    10000
}

fn default_task_persist() -> bool {
    true
}

impl Default for AcpServerConfig {
    fn default() -> Self {
        Self {
            enabled: false,
            port: None, // Default to integrated mode
            allowed_origins: None,
            max_connections: 100,
            default_connector_id: None,
        }
    }
}

impl Default for CoreConfig {
    fn default() -> Self {
        Self {
            port: None,
            runtime_working_dir: PathBuf::from("."),
            connectors: vec![],
            archive_root: None,
            archives: Vec::new(),
            projects_root: None,
            acp_server: None,
            dismissed_zed_agents: vec![],
            tasks: vec![],
            accounts: HashMap::new(),
            matrix: None,
            streams: crate::sharing::StreamsConfig::default(),
            config_source_path: None,
            resolved_working_dir: None,
        }
    }
}

impl CoreConfig {
    /// Returns the effective working directory for runtime use.
    ///
    /// Prefers `resolved_working_dir` (populated by `resolve_paths()` at load time).
    /// Falls back to the raw `runtime_working_dir` if resolution hasn't been called.
    pub fn effective_working_dir(&self) -> &std::path::Path {
        self.resolved_working_dir
            .as_deref()
            .unwrap_or(&self.runtime_working_dir)
    }

    /// Resolve relative paths against the config file's parent directory.
    ///
    /// Populates `resolved_working_dir` based on `runtime_working_dir`:
    /// - `"."` or equivalent → `noproject_home_dir()` (no explicit project)
    /// - Relative path → joined with `config_dir_parent`
    /// - Absolute path → used as-is
    ///
    /// Also resolves deprecated `archive_root` and `projects_root` in-place
    /// (these are skip_serializing so mutation is safe).
    pub fn resolve_paths(&mut self, config_dir_parent: Option<&std::path::Path>) {
        let is_dot = self.runtime_working_dir == PathBuf::from(".");

        let resolved = if is_dot {
            dirigent_config::DirigentPaths::resolve()
                .map(|p| p.noproject_home_dir())
                .unwrap_or_else(|_| {
                    config_dir_parent
                        .map(|p| p.to_path_buf())
                        .unwrap_or_else(|| std::env::current_dir().unwrap_or_default())
                })
        } else if self.runtime_working_dir.is_relative() {
            if let Some(parent) = config_dir_parent {
                let joined = parent.join(&self.runtime_working_dir);
                debug!(
                    original = ?self.runtime_working_dir,
                    resolved = ?joined,
                    "Resolved relative runtime_working_dir against config directory"
                );
                joined
            } else {
                std::env::current_dir()
                    .unwrap_or_default()
                    .join(&self.runtime_working_dir)
            }
        } else {
            self.runtime_working_dir.clone()
        };

        // If resolved path equals the config dir, fall back to noproject_home
        let final_resolved = if let Some(parent) = config_dir_parent {
            if resolved == parent {
                dirigent_config::DirigentPaths::resolve()
                    .map(|p| p.noproject_home_dir())
                    .unwrap_or(resolved)
            } else {
                resolved
            }
        } else {
            resolved
        };

        self.resolved_working_dir = Some(final_resolved);

        // Resolve deprecated fields in-place (skip_serializing, safe from corruption)
        if let Some(parent) = config_dir_parent {
            if let Some(ref archive) = self.archive_root {
                if archive.is_relative() {
                    let resolved = parent.join(archive);
                    debug!(original = ?archive, resolved = ?resolved, "Resolved relative archive_root against config directory");
                    self.archive_root = Some(resolved);
                }
            }
            if let Some(ref projects) = self.projects_root {
                if projects.is_relative() {
                    let resolved = parent.join(projects);
                    debug!(original = ?projects, resolved = ?resolved, "Resolved relative projects_root against config directory");
                    self.projects_root = Some(resolved);
                }
            }
        }
    }

    /// Populate runtime fields on accounts after deserialization.
    ///
    /// Sets `config_name` from the map key and derives a stable `user_id`
    /// (UUID v5 from the account name) for any account that doesn't have one.
    /// Call this immediately after deserializing or loading a `CoreConfig`.
    pub fn populate_account_names(&mut self) {
        for (name, account) in &mut self.accounts {
            account.config_name = name.clone();
            if account.user_id.is_none() {
                account.user_id = Some(uuid::Uuid::new_v5(
                    &uuid::Uuid::NAMESPACE_URL,
                    format!("dirigent:account:{}", name).as_bytes(),
                ));
            }
        }
    }

    /// Find the first local account, if any.
    pub fn default_local_account(&self) -> Option<&dirigent_auth::Account> {
        self.accounts
            .values()
            .find(|a| a.kind == dirigent_auth::AccountKind::Local)
    }

    /// Get the UserId for the default local account, or Uuid::nil() if none.
    pub fn default_owner(&self) -> dirigent_auth::UserId {
        self.default_local_account()
            .and_then(|a| a.user_id)
            .unwrap_or(uuid::Uuid::nil())
    }

    /// Load configuration from a file
    ///
    /// This function attempts to load configuration from various sources in priority order:
    /// 1. The provided `path` parameter (if Some)
    /// 2. Config directory (platform-native, or overridden via `DIRIGENT_CONFIG_DIR` env var):
    ///    `%APPDATA%\dirigent\` on Windows, `~/.config/dirigent/` on Linux/macOS
    /// 3. Default configuration (if no file found)
    ///
    /// The file format (TOML or JSON) is automatically detected based on the file extension.
    ///
    /// # Arguments
    ///
    /// * `path` - Optional explicit path to configuration file
    ///
    /// # Returns
    ///
    /// A `Result` containing the loaded `CoreConfig` or a `CoreError`
    ///
    /// # Errors
    ///
    /// Returns `CoreError::InvalidConfig` if:
    /// - The file exists but cannot be read
    /// - The file exists but contains invalid TOML/JSON
    /// - The file has an unsupported extension
    ///
    /// # Example
    ///
    /// ```no_run
    /// use dirigent_core::CoreConfig;
    /// use std::path::PathBuf;
    ///
    /// // Load from explicit path
    /// let config = CoreConfig::load_config(Some(PathBuf::from("my-config.toml"))).unwrap();
    ///
    /// // Load from config directory or defaults
    /// let config = CoreConfig::load_config(None).unwrap();
    /// ```
    pub fn load_config(path: Option<PathBuf>) -> Result<CoreConfig, CoreError> {
        // Determine which file to load
        let config_path = if let Some(p) = path {
            debug!(path = ?p, "Loading config from provided path");
            Some(p)
        } else {
            // Check config directory (platform-native, or DIRIGENT_CONFIG_DIR override)
            if let Ok(paths) = dirigent_config::DirigentPaths::resolve() {
                let user_toml = paths.config_dir().join("dirigent.toml");
                let user_json = paths.config_dir().join("dirigent.json");

                if user_toml.exists() {
                    debug!(path = ?user_toml, "Found dirigent.toml in config directory");
                    Some(user_toml)
                } else if user_json.exists() {
                    debug!(path = ?user_json, "Found dirigent.json in config directory");
                    Some(user_json)
                } else {
                    debug!("No config file found in config directory, using defaults");
                    None
                }
            } else {
                debug!("Could not resolve config directory, using defaults");
                None
            }
        };

        // If we have a path, try to load it
        if let Some(path) = config_path {
            // Check if file exists
            if !path.exists() {
                warn!(path = ?path, "Config file not found, using default configuration");
                info!("Using default configuration");
                return Ok(CoreConfig::default());
            }

            // Read file contents
            let contents = std::fs::read_to_string(&path).map_err(|e| {
                CoreError::Internal(format!(
                    "Failed to read config file {}: {}",
                    path.display(),
                    e
                ))
            })?;

            // Determine format by extension
            let mut config = match path.extension().and_then(|s| s.to_str()) {
                Some("toml") => {
                    debug!("Parsing config as TOML");
                    toml::from_str::<CoreConfig>(&contents).map_err(|e| {
                        CoreError::Internal(format!(
                            "Failed to parse TOML config file {}: {}",
                            path.display(),
                            e
                        ))
                    })?
                }
                Some("json") => {
                    debug!("Parsing config as JSON");
                    serde_json::from_str::<CoreConfig>(&contents).map_err(|e| {
                        CoreError::Internal(format!(
                            "Failed to parse JSON config file {}: {}",
                            path.display(),
                            e
                        ))
                    })?
                }
                _ => {
                    let msg = format!(
                        "Unsupported config file extension for {}, expected .toml or .json",
                        path.display()
                    );
                    warn!("{}", msg);
                    return Err(CoreError::Internal(msg));
                }
            };

            info!(path = ?path, "Successfully loaded configuration");

            if config.archive_root.is_some() {
                warn!("archive_root in config file is deprecated. Set DIRIGENT_DATA_DIR instead. Archives default to <data_dir>/archives/");
            }
            if config.projects_root.is_some() {
                warn!("projects_root in config file is deprecated. Set DIRIGENT_DATA_DIR instead. Projects default to <data_dir>/projects/");
            }

            // Deduplicate connectors by ID (in case of corrupted config file)
            let original_count = config.connectors.len();
            let mut seen_ids = std::collections::HashSet::new();
            config.connectors.retain(|c| {
                if let Some(ref id) = c.id {
                    if seen_ids.contains(id) {
                        warn!(connector_id = %id, "Removing duplicate connector from config");
                        false
                    } else {
                        seen_ids.insert(id.clone());
                        true
                    }
                } else {
                    // Keep connectors without IDs (shouldn't happen, but be safe)
                    warn!("Found connector without ID in config, keeping it");
                    true
                }
            });

            let deduped_count = config.connectors.len();
            if deduped_count < original_count {
                info!(
                    original = original_count,
                    deduped = deduped_count,
                    removed = original_count - deduped_count,
                    "Deduplicated connectors in config"
                );
            }

            // Track which file we loaded from so save_config writes back to it
            config.config_source_path = Some(path.clone());

            // Resolve relative paths against config file location.
            // This populates resolved_working_dir (never mutates runtime_working_dir)
            // and resolves deprecated archive_root/projects_root in-place.
            config.resolve_paths(path.parent());

            // Populate runtime fields on accounts (config_name, user_id).
            config.populate_account_names();

            Ok(config)
        } else {
            // No config file found, use default
            info!("Using default configuration");
            let mut config = CoreConfig::default();
            config.resolve_paths(None);
            // No accounts to populate in default config, but call for consistency.
            config.populate_account_names();
            Ok(config)
        }
    }
}

/// Lenient deserializer for the `owner` field.
///
/// Accepts a valid UUID string, or silently returns `None` for non-UUID
/// values (e.g. legacy `"DIRIGENT-USER"` strings) and missing fields.
fn deserialize_owner_lenient<'de, D>(deserializer: D) -> Result<Option<UserId>, D::Error>
where
    D: serde::Deserializer<'de>,
{
    // Accept either null/missing or a string
    let opt: Option<String> = Option::deserialize(deserializer)?;
    match opt {
        None => Ok(None),
        Some(s) => match uuid::Uuid::parse_str(&s) {
            Ok(id) => Ok(Some(id)),
            Err(_) => {
                tracing::debug!(
                    value = %s,
                    "Ignoring non-UUID owner value in connector config, treating as None"
                );
                Ok(None)
            }
        },
    }
}

/// Configuration for creating a connector
///
/// This struct contains all the information needed to create and configure
/// a connector instance. The `params` field is a JSON value that will be
/// deserialized into the appropriate connector-specific config type.
///
/// ConnectorConfig can be serialized and persisted to disk, enabling
/// connectors to be automatically recreated on server restart.
///
/// # Example
///
/// ```no_run
/// use dirigent_core::{ConnectorConfig, ConnectorKind};
/// use serde_json::json;
///
/// let config = ConnectorConfig {
///     id: Some("my-connector".to_string()),
///     kind: ConnectorKind::OpenCode,
///     owner: Some(uuid::Uuid::now_v7()),
///     title: Some("My OpenCode".to_string()),
///     working_directory: None,
///     params: json!({
///         "base_url": "http://localhost:12225",
///         "title": "My OpenCode",
///         "initial_session": null
///     }),
///     ..Default::default()
/// };
/// ```
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct ConnectorConfig {
    /// Optional connector ID (will be generated if not provided)
    ///
    /// When persisting connectors, an explicit ID ensures the same connector
    /// can be referenced consistently across restarts.
    pub id: Option<String>,

    /// Type of connector to create
    ///
    /// Determines which connector implementation will be instantiated.
    /// Each variant corresponds to a different agent system integration.
    pub kind: ConnectorKind,

    /// Optional owner user ID (may be set by the runtime)
    ///
    /// Identifies which user owns this connector. Used for authorization
    /// and filtering. If not provided during creation, the runtime will
    /// set this based on the creating user.
    ///
    /// For backwards compatibility, non-UUID strings (e.g. `"DIRIGENT-USER"`)
    /// are silently treated as `None` during deserialization.
    #[serde(default, deserialize_with = "deserialize_owner_lenient")]
    pub owner: Option<UserId>,

    /// Optional title (may be provided in params instead)
    ///
    /// Human-readable title for the connector. If not provided here,
    /// the connector-specific params should include a title field.
    pub title: Option<String>,

    /// Optional working directory for the connector
    ///
    /// If specified, this directory will be used as the working directory for
    /// agent operations. If not specified, the working directory is resolved
    /// using the following hierarchy:
    /// 1. Explicit working_directory (if set)
    /// 2. Global project_dir from CoreConfig
    /// 3. Current working directory of the Dirigent process
    ///
    /// The resolved path is normalized and canonicalized where possible.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub working_directory: Option<PathBuf>,

    /// Connector-specific parameters as JSON
    ///
    /// These will be deserialized into the appropriate config type
    /// based on the `kind` field. For example:
    /// - OpenCode: deserialized to `OpenCodeConfig`
    /// - Acp: deserialized to `AcpConfig` (future)
    ///
    /// Using serde_json::Value allows flexible persistence without
    /// requiring the config module to know about all connector types.
    pub params: serde_json::Value,

    /// T034: Optional custom icon path for this connector
    ///
    /// If specified, this path points to an image file that will be used
    /// as the connector's icon in the UI instead of the default emoji.
    /// The path can be absolute or relative to the project directory.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub icon_path: Option<String>,

    /// T035: Show connector type emoji as overlay on custom icon
    ///
    /// When true and a custom icon_path is provided, the connector type
    /// emoji (e.g., for OpenCode, ACP) will appear as a small overlay
    /// in the lower-right corner of the custom icon, similar to Windows
    /// shortcut overlay icons.
    #[serde(default)]
    pub show_type_overlay: bool,

    /// T001: List of features this connector supports
    ///
    /// Orchestration-level field that declares which optional features
    /// this connector implementation supports. Examples:
    /// - "session_resume" - Can resume previous sessions
    /// - "session_list" - Can list available sessions
    /// - "message_history" - Can retrieve message history
    /// - "cancellation" - Can cancel in-progress operations
    /// - "model_selection" - Supports model selection
    /// - "mode_selection" - Supports mode/persona selection
    ///
    /// This field should NOT appear in connector-specific params.
    #[serde(default)]
    pub supported_features: Vec<String>,

    /// Tool configuration for this connector.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub tool_configuration: Option<crate::tools::ToolConfiguration>,

    /// Plugin assignments for this connector.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub plugin_assignments: Vec<crate::plugins::PluginAssignment>,

    /// Whether this connector is used in newly created projects by default.
    #[serde(default = "default_true")]
    pub use_in_new_projects: bool,

    /// Source of this connector (e.g., "user", "zed").
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub source: Option<String>,

    /// Zed agent name this connector was created from.
    ///
    /// When set, this connector's binary path will be automatically refreshed
    /// at startup to track Zed agent upgrades. The value is the Zed agent name
    /// (e.g. "claude-agent-acp", "codex", "gemini") used to look up the
    /// current binary in Zed's `external_agents/` directory.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub zed_agent_name: Option<String>,
}

fn default_true() -> bool {
    true
}

impl Default for ConnectorConfig {
    fn default() -> Self {
        Self {
            id: None,
            kind: ConnectorKind::Mock,
            owner: None,
            title: None,
            working_directory: None,
            params: serde_json::json!({}),
            icon_path: None,
            show_type_overlay: false,
            supported_features: vec![],
            tool_configuration: None,
            plugin_assignments: vec![],
            use_in_new_projects: true,
            source: None,
            zed_agent_name: None,
        }
    }
}

/// Connector configuration templates
///
/// Provides pre-defined connector configurations with sensible defaults.
/// Templates can be referenced by (ConnectorKind, label) and customized
/// via JSON patches when creating connectors.
///
/// # Available Templates
///
/// ## OpenCode Templates
/// - `opencode/default` - Standard OpenCode connector with localhost URL
///
/// ## ACP Templates
/// - `acp/claude-default` - Claude API connector with default model (stub)
///
/// # Usage
///
/// Templates are accessed via the `apply_template` function, which merges
/// a template with a user-provided patch to create a final ConnectorConfig.
static TEMPLATES: Lazy<HashMap<(ConnectorKind, &'static str), serde_json::Value>> =
    Lazy::new(|| {
        let mut m = HashMap::new();

        // OpenCode default template
        m.insert(
            (ConnectorKind::OpenCode, "default"),
            serde_json::json!({
                "base_url": "http://localhost:12225",
                "initial_session": null
            }),
        );

        // ACP Claude stdio template
        // Note: Claude Code has limited ACP support:
        // - NOT session_resume: generates ephemeral ACP session IDs
        // - NOT cancellation: session/cancel not implemented
        m.insert(
            (ConnectorKind::Acp, "claude-stdio"),
            serde_json::json!({
                "transport": {
                    "type": "stdio",
                    "command": "claude",
                    "args": ["--acp"]
                },
                "protocol_version": 1,
                "cwd": ".",
                "retry": {
                    "max_retries": 5,
                    "retry_delays_ms": [1000, 3000, 5000, 5000, 5000]
                }
            }),
        );

        // ACP Mocker stdio template (for testing)
        // Note: Mocker supports all features for testing purposes
        m.insert(
            (ConnectorKind::Acp, "mocker-stdio"),
            serde_json::json!({
                "transport": {
                    "type": "stdio",
                    "command": "dirigate",
                    "args": ["serve", "--fixtures", "basic.yaml", "--stdio"]
                },
                "protocol_version": 1,
                "cwd": ".",
                "retry": {
                    "max_retries": 5,
                    "retry_delays_ms": [1000, 3000, 5000, 5000, 5000]
                }
            }),
        );

        // ACP HTTP template
        // Note: HTTP agent capabilities vary - user should configure supported_features
        m.insert(
            (ConnectorKind::Acp, "http"),
            serde_json::json!({
                "transport": {
                    "type": "http",
                    "base_url": "http://localhost:3000",
                    "timeout_ms": 30000
                },
                "protocol_version": 1,
                "cwd": ".",
                "retry": {
                    "max_retries": 5,
                    "retry_delays_ms": [1000, 3000, 5000, 5000, 5000]
                }
            }),
        );

        m
    });

/// Apply a connector template with optional patches
///
/// This function retrieves a template by kind and label, then merges it with
/// the provided patch object to create a final ConnectorConfig. The patch
/// values override template values, allowing customization of defaults.
///
/// # Arguments
///
/// * `kind` - The type of connector (OpenCode, Acp, etc.)
/// * `label` - The template identifier (e.g., "default", "claude-default")
/// * `patch` - JSON object with values to override in the template
///
/// # Returns
///
/// A `Result` containing the merged `ConnectorConfig` or a `CoreError`
///
/// # Errors
///
/// Returns `CoreError::NotFound` if no template exists for the given (kind, label) pair.
///
/// # Example
///
/// ```no_run
/// use dirigent_core::{apply_template, ConnectorKind};
/// use serde_json::json;
///
/// // Use template with default values
/// let config = apply_template(
///     ConnectorKind::OpenCode,
///     "default",
///     json!({})
/// ).unwrap();
///
/// // Override the base_url and title
/// let config = apply_template(
///     ConnectorKind::OpenCode,
///     "default",
///     json!({
///         "base_url": "http://localhost:8080",
///         "title": "My Custom OpenCode"
///     })
/// ).unwrap();
/// ```
pub fn apply_template(
    kind: ConnectorKind,
    label: &str,
    patch: serde_json::Value,
) -> Result<ConnectorConfig, CoreError> {
    // Lookup template
    let template = TEMPLATES
        .get(&(kind.clone(), label))
        .ok_or(CoreError::NotFound)?;

    // Merge template with patch (patch overrides template)
    let merged = merge_json(template.clone(), patch);

    // Extract orchestration-level fields from merged JSON
    let title = merged
        .get("title")
        .and_then(|v| v.as_str())
        .map(|s| s.to_string());

    let supported_features = merged
        .get("supported_features")
        .and_then(|v| v.as_array())
        .map(|arr| {
            arr.iter()
                .filter_map(|v| v.as_str().map(|s| s.to_string()))
                .collect()
        })
        .unwrap_or_default();

    // Remove orchestration fields from params
    let mut params = merged;
    if let Some(obj) = params.as_object_mut() {
        obj.remove("title");
        obj.remove("supported_features");
        obj.remove("icon_path");
        obj.remove("show_type_overlay");
    }

    // Build ConnectorConfig
    Ok(ConnectorConfig {
        id: None, // ID will be generated by runtime
        kind,
        owner: None, // Owner will be set by runtime
        title,
        working_directory: None, // Working directory will be resolved from global config
        params,
        icon_path: None,          // Custom icons are not set via templates
        show_type_overlay: false, // Default to no overlay
        supported_features,
        tool_configuration: None,
        plugin_assignments: vec![],
        use_in_new_projects: true,
        source: None,
        zed_agent_name: None,
    })
}

/// Merge two JSON values with right-hand side taking precedence
///
/// This is a simple shallow merge that combines two JSON objects.
/// If both values are objects, their keys are merged with `patch`
/// values overriding `base` values. Otherwise, `patch` replaces `base`.
///
/// # Arguments
///
/// * `base` - The base JSON value (typically from a template)
/// * `patch` - The patch JSON value (user overrides)
///
/// # Returns
///
/// The merged JSON value
fn merge_json(base: serde_json::Value, patch: serde_json::Value) -> serde_json::Value {
    use serde_json::Value;

    match (base, patch) {
        (Value::Object(mut base_map), Value::Object(patch_map)) => {
            // Merge objects: patch values override base values
            for (key, value) in patch_map {
                base_map.insert(key, value);
            }
            Value::Object(base_map)
        }
        (_, patch) => {
            // For non-objects, patch completely replaces base
            patch
        }
    }
}

/// Resolve the working directory for a connector
///
/// This function implements the resolution hierarchy for determining the
/// working directory to use for a connector's operations:
///
/// 1. Explicit `working_directory` in connector config (if set)
/// 2. `runtime_working_dir` from global config
///
/// The resolved path is normalized and, if it exists, canonicalized.
/// If the directory does not exist, a warning is logged but the path
/// is still returned (allowing for creation later).
///
/// # Arguments
///
/// * `connector_config` - The connector configuration
/// * `global_config` - The global runtime configuration
///
/// # Returns
///
/// The resolved working directory path. This path is normalized and
/// canonicalized where possible.
///
/// # Example
///
/// ```no_run
/// use dirigent_core::{resolve_default_runtime_working_directory, ConnectorConfig, CoreConfig};
///
/// let connector_config = ConnectorConfig {
///     id: None,
///     kind: dirigent_core::ConnectorKind::OpenCode,
///     owner: None,
///     title: None,
///     working_directory: Some("/path/to/workdir".into()),
///     params: serde_json::json!({}),
/// };
/// let global_config = CoreConfig::default();
///
/// let workdir = resolve_default_runtime_working_directory(&connector_config, &global_config);
/// println!("Working directory: {}", workdir.display());
/// ```
pub fn resolve_default_runtime_working_directory(
    connector_config: &ConnectorConfig,
    global_config: &CoreConfig,
) -> PathBuf {
    // Step 1: Check if explicit working_directory is set in connector config
    if let Some(ref wd) = connector_config.working_directory {
        debug!(
            working_directory = ?wd,
            "Using explicit working_directory from connector config"
        );

        // Normalize and canonicalize if path exists
        return normalize_path(wd);
    }

    // Step 2: Use runtime_working_dir from global config
    let project_dir = &global_config.runtime_working_dir;
    debug!(
        working_directory = ?project_dir,
        "Using runtime_working_dir from global config"
    );

    normalize_path(project_dir)
}

/// Normalize and canonicalize a path
///
/// This helper function normalizes a path and attempts to canonicalize it
/// if the path exists. If canonicalization fails (e.g., directory doesn't
/// exist), the normalized path is returned with a warning.
///
/// # Arguments
///
/// * `path` - The path to normalize
///
/// # Returns
///
/// The normalized (and canonicalized if possible) path
fn normalize_path(path: &PathBuf) -> PathBuf {
    // Try to canonicalize (resolves symlinks and relative paths)
    match path.canonicalize() {
        Ok(canonical) => {
            debug!(
                original = ?path,
                canonical = ?canonical,
                "Successfully canonicalized path"
            );
            canonical
        }
        Err(e) => {
            // Path doesn't exist or can't be canonicalized
            warn!(
                path = ?path,
                error = %e,
                "Failed to canonicalize path (directory may not exist), using normalized path"
            );

            // Return the normalized path as-is
            path.clone()
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::types::ConnectorKind;
    use serde_json::json;

    #[test]
    fn test_core_config_default() {
        let config = CoreConfig::default();
        assert_eq!(config.runtime_working_dir, PathBuf::from("."));
        assert_eq!(config.connectors.len(), 0);
    }

    #[test]
    fn test_core_config_custom() {
        let config = CoreConfig {
            runtime_working_dir: PathBuf::from("/my/project"),
            connectors: vec![],
            archive_root: None,
            archives: Vec::new(),
            projects_root: None,
            acp_server: None,
            dismissed_zed_agents: vec![],
            tasks: vec![],
            accounts: HashMap::new(),
            matrix: None,
            streams: crate::sharing::StreamsConfig::default(),
            config_source_path: None,
            resolved_working_dir: None,
        };

        assert_eq!(config.runtime_working_dir, PathBuf::from("/my/project"));
    }

    #[test]
    fn test_core_config_serialization() {
        let config = CoreConfig {
            runtime_working_dir: PathBuf::from("/my/project"),
            connectors: vec![],
            archive_root: None,
            archives: Vec::new(),
            projects_root: None,
            acp_server: None,
            dismissed_zed_agents: vec![],
            tasks: vec![],
            accounts: HashMap::new(),
            matrix: None,
            streams: crate::sharing::StreamsConfig::default(),
            config_source_path: None,
            resolved_working_dir: None,
        };

        let json = serde_json::to_string(&config).expect("Failed to serialize");
        let deserialized: CoreConfig = serde_json::from_str(&json).expect("Failed to deserialize");

        assert_eq!(deserialized.runtime_working_dir, config.runtime_working_dir);
    }

    #[test]
    fn test_connector_config_basic() {
        let config = ConnectorConfig {
            id: Some("conn-1".to_string()),
            kind: ConnectorKind::OpenCode,
            owner: Some(uuid::Uuid::nil()),
            title: Some("My Connector".to_string()),
            working_directory: None,
            params: json!({
                "base_url": "http://localhost:12225",
                "title": "My Connector",
            }),
            icon_path: None,
            show_type_overlay: false,
            supported_features: vec![],
            tool_configuration: None,
            plugin_assignments: vec![],
            use_in_new_projects: true,
            source: None,
            zed_agent_name: None,
        };

        assert_eq!(config.id, Some("conn-1".to_string()));
        assert_eq!(config.kind, ConnectorKind::OpenCode);
        assert_eq!(config.owner, Some(uuid::Uuid::nil()));
        assert_eq!(config.title, Some("My Connector".to_string()));
    }

    #[test]
    fn test_connector_config_serialization() {
        let config = ConnectorConfig {
            id: Some("conn-1".to_string()),
            kind: ConnectorKind::OpenCode,
            owner: Some(uuid::Uuid::nil()),
            title: Some("Test".to_string()),
            working_directory: None,
            params: json!({
                "base_url": "http://localhost:12225",
            }),
            icon_path: None,
            show_type_overlay: false,
            supported_features: vec![],
            tool_configuration: None,
            plugin_assignments: vec![],
            use_in_new_projects: true,
            source: None,
            zed_agent_name: None,
        };

        let json = serde_json::to_string(&config).expect("Failed to serialize");
        let deserialized: ConnectorConfig =
            serde_json::from_str(&json).expect("Failed to deserialize");

        assert_eq!(deserialized.id, config.id);
        assert_eq!(deserialized.kind, config.kind);
        assert_eq!(deserialized.owner, config.owner);
        assert_eq!(deserialized.title, config.title);
    }

    #[test]
    fn test_connector_config_minimal() {
        // Test with minimal fields (id, owner, title all optional)
        let config = ConnectorConfig {
            id: None,
            kind: ConnectorKind::Mock,
            owner: None,
            title: None,
            working_directory: None,
            params: json!({}),
            icon_path: None,
            show_type_overlay: false,
            supported_features: vec![],
            tool_configuration: None,
            plugin_assignments: vec![],
            use_in_new_projects: true,
            source: None,
            zed_agent_name: None,
        };

        assert!(config.id.is_none());
        assert_eq!(config.kind, ConnectorKind::Mock);
        assert!(config.owner.is_none());
        assert!(config.title.is_none());
    }

    #[test]
    fn test_core_config_with_connectors() {
        let connector1 = ConnectorConfig {
            id: Some("conn-1".to_string()),
            kind: ConnectorKind::OpenCode,
            owner: Some(uuid::Uuid::nil()),
            title: Some("Connector 1".to_string()),
            working_directory: None,
            params: json!({"base_url": "http://localhost:12225"}),
            icon_path: None,
            show_type_overlay: false,
            supported_features: vec![],
            tool_configuration: None,
            plugin_assignments: vec![],
            use_in_new_projects: true,
            source: None,
            zed_agent_name: None,
        };

        let connector2 = ConnectorConfig {
            id: Some("conn-2".to_string()),
            kind: ConnectorKind::Acp,
            owner: Some(uuid::Uuid::nil()),
            title: Some("Connector 2".to_string()),
            working_directory: None,
            params: json!({"endpoint": "http://localhost:8080"}),
            icon_path: None,
            show_type_overlay: false,
            supported_features: vec![],
            tool_configuration: None,
            plugin_assignments: vec![],
            use_in_new_projects: true,
            source: None,
            zed_agent_name: None,
        };

        let config = CoreConfig {
            connectors: vec![connector1, connector2],
            ..CoreConfig::default()
        };

        assert_eq!(config.connectors.len(), 2);
        assert_eq!(config.connectors[0].id, Some("conn-1".to_string()));
        assert_eq!(config.connectors[1].id, Some("conn-2".to_string()));
    }

    #[test]
    fn test_load_config_no_file_returns_default() {
        // When no file exists and no env var is set, should return default
        let config = CoreConfig::load_config(None).unwrap();
        // project_dir may be resolved against config directory if a config file exists
        assert!(!config.runtime_working_dir.as_os_str().is_empty());
    }

    #[test]
    fn test_apply_template_opencode_default() {
        // Test applying OpenCode default template with no patch
        let config = apply_template(ConnectorKind::OpenCode, "default", json!({})).unwrap();

        assert_eq!(config.kind, ConnectorKind::OpenCode);
        assert!(config.id.is_none());
        assert!(config.owner.is_none());
        assert_eq!(config.title, None); // Title removed from templates (T001)
        assert_eq!(config.params["base_url"], "http://localhost:12225");
        assert!(config.params["initial_session"].is_null());
    }

    #[test]
    fn test_apply_template_opencode_with_patch() {
        // Test applying OpenCode template with custom values
        let config = apply_template(
            ConnectorKind::OpenCode,
            "default",
            json!({
                "base_url": "http://localhost:8080",
                "title": "My Custom OpenCode",
                "initial_session": "session-123"
            }),
        )
        .unwrap();

        assert_eq!(config.kind, ConnectorKind::OpenCode);
        assert_eq!(config.title, Some("My Custom OpenCode".to_string()));
        assert_eq!(config.params["base_url"], "http://localhost:8080");
        assert_eq!(config.params["initial_session"], "session-123");
    }

    #[test]
    fn test_apply_template_acp_claude_stdio() {
        // Test applying ACP Claude stdio template
        let config = apply_template(ConnectorKind::Acp, "claude-stdio", json!({})).unwrap();

        assert_eq!(config.kind, ConnectorKind::Acp);
        assert!(config.id.is_none());
        assert!(config.owner.is_none());
        assert_eq!(config.title, None); // Title removed from templates (T001)
        assert_eq!(config.params["transport"]["type"], "stdio");
        assert_eq!(config.params["transport"]["command"], "claude");
        assert_eq!(config.params["protocol_version"], 1);

        // supported_features removed from templates (T001)
        assert!(config.params.get("supported_features").is_none());
        // supported_features now in ConnectorConfig, not params
        assert_eq!(config.supported_features, Vec::<String>::new());
    }

    #[test]
    fn test_apply_template_acp_mocker_stdio() {
        // Test applying ACP Mocker stdio template
        let config = apply_template(ConnectorKind::Acp, "mocker-stdio", json!({})).unwrap();

        assert_eq!(config.kind, ConnectorKind::Acp);
        assert_eq!(config.title, None); // Title removed from templates (T001)
        assert_eq!(config.params["transport"]["type"], "stdio");
        assert_eq!(config.params["transport"]["command"], "dirigate");

        // supported_features removed from templates (T001)
        assert!(config.params.get("supported_features").is_none());
        // supported_features now in ConnectorConfig, not params
        assert_eq!(config.supported_features, Vec::<String>::new());
    }

    #[test]
    fn test_apply_template_acp_http() {
        // Test applying ACP HTTP template
        let config = apply_template(ConnectorKind::Acp, "http", json!({})).unwrap();

        assert_eq!(config.kind, ConnectorKind::Acp);
        assert_eq!(config.title, None); // Title removed from templates (T001)
        assert_eq!(config.params["transport"]["type"], "http");
        assert_eq!(
            config.params["transport"]["base_url"],
            "http://localhost:3000"
        );
        assert_eq!(config.params["transport"]["timeout_ms"], 30000);

        // supported_features removed from templates (T001)
        assert!(config.params.get("supported_features").is_none());
        // supported_features now in ConnectorConfig, not params
        assert_eq!(config.supported_features, Vec::<String>::new());
    }

    #[test]
    fn test_apply_template_acp_with_patch() {
        // Test applying ACP template with custom command
        let config = apply_template(
            ConnectorKind::Acp,
            "claude-stdio",
            json!({
                "title": "My Custom Claude",
                "transport": {
                    "type": "stdio",
                    "command": "/usr/local/bin/claude",
                    "args": ["--acp", "--verbose"]
                }
            }),
        )
        .unwrap();

        assert_eq!(config.kind, ConnectorKind::Acp);
        assert_eq!(config.title, Some("My Custom Claude".to_string()));
        assert_eq!(
            config.params["transport"]["command"],
            "/usr/local/bin/claude"
        );
    }

    #[test]
    fn test_apply_template_not_found() {
        // Test that unknown template returns NotFound error
        let result = apply_template(ConnectorKind::OpenCode, "nonexistent", json!({}));

        assert_eq!(result, Err(CoreError::NotFound));
    }

    #[test]
    fn test_apply_template_wrong_kind() {
        // Test that wrong kind returns NotFound error
        let result = apply_template(ConnectorKind::Mock, "default", json!({}));

        assert_eq!(result, Err(CoreError::NotFound));
    }

    #[test]
    fn test_merge_json_objects() {
        // Test merging two JSON objects
        let base = json!({
            "a": 1,
            "b": 2,
            "c": 3
        });
        let patch = json!({
            "b": 20,
            "d": 4
        });

        let merged = merge_json(base, patch);

        assert_eq!(merged["a"], 1);
        assert_eq!(merged["b"], 20); // Overridden
        assert_eq!(merged["c"], 3);
        assert_eq!(merged["d"], 4); // Added
    }

    #[test]
    fn test_merge_json_empty_patch() {
        // Test that empty patch returns base unchanged
        let base = json!({
            "a": 1,
            "b": 2
        });
        let patch = json!({});

        let merged = merge_json(base.clone(), patch);

        assert_eq!(merged, base);
    }

    #[test]
    fn test_merge_json_non_objects() {
        // Test that non-object patch replaces base
        let base = json!({
            "a": 1,
            "b": 2
        });
        let patch = json!("replacement");

        let merged = merge_json(base, patch.clone());

        assert_eq!(merged, patch);
    }

    #[test]
    fn test_merge_json_nested_not_recursive() {
        // Test that merge is shallow, not recursive
        let base = json!({
            "nested": {
                "a": 1,
                "b": 2
            }
        });
        let patch = json!({
            "nested": {
                "c": 3
            }
        });

        let merged = merge_json(base, patch);

        // Should replace entire nested object, not merge it
        assert!(merged["nested"]["c"] == 3);
        assert!(merged["nested"].get("a").is_none());
        assert!(merged["nested"].get("b").is_none());
    }

    #[test]
    fn test_template_params_serialization() {
        // Test that template-generated configs can be serialized
        let config =
            apply_template(ConnectorKind::OpenCode, "default", json!({"title": "Test"})).unwrap();

        let json = serde_json::to_string(&config).expect("Failed to serialize");
        let deserialized: ConnectorConfig =
            serde_json::from_str(&json).expect("Failed to deserialize");

        assert_eq!(deserialized.kind, config.kind);
        assert_eq!(deserialized.title, config.title);
        assert_eq!(deserialized.params, config.params);
    }

    // INTEG-008: Test ACP config serialization round-trip
    #[test]
    fn test_acp_config_serialization_toml_stdio() {
        // Test ACP stdio config serialization to/from TOML
        let config = ConnectorConfig {
            id: Some("acp-stdio-test".to_string()),
            kind: ConnectorKind::Acp,
            owner: Some(uuid::Uuid::nil()),
            title: Some("Test ACP Stdio".to_string()),
            working_directory: None,
            params: json!({
                "transport": {
                    "type": "stdio",
                    "command": "dirigate",
                    "args": ["serve", "--stdio"]
                },
                "title": "Test ACP Stdio",
                "protocol_version": 1,
                "cwd": ".",
                "retry": {
                    "max_retries": 5,
                    "retry_delays_ms": [1000, 3000, 5000, 5000, 5000]
                }
            }),
            icon_path: None,
            show_type_overlay: false,
            supported_features: vec![],
            tool_configuration: None,
            plugin_assignments: vec![],
            use_in_new_projects: true,
            source: None,
            zed_agent_name: None,
        };

        // Serialize to TOML
        let toml_str = toml::to_string(&config).expect("Failed to serialize to TOML");

        // Deserialize from TOML
        let deserialized: ConnectorConfig =
            toml::from_str(&toml_str).expect("Failed to deserialize from TOML");

        // Verify round-trip
        assert_eq!(deserialized.id, config.id);
        assert_eq!(deserialized.kind, config.kind);
        assert_eq!(deserialized.owner, config.owner);
        assert_eq!(deserialized.title, config.title);
        assert_eq!(deserialized.params["transport"]["command"], "dirigate");
        assert_eq!(deserialized.params["protocol_version"], 1);
    }

    #[test]
    fn test_acp_config_serialization_json_http() {
        // Test ACP HTTP config serialization to/from JSON
        let config = ConnectorConfig {
            id: Some("acp-http-test".to_string()),
            kind: ConnectorKind::Acp,
            owner: Some(uuid::Uuid::nil()),
            title: Some("Test ACP HTTP".to_string()),
            working_directory: None,
            params: json!({
                "transport": {
                    "type": "http",
                    "base_url": "http://localhost:3000",
                    "timeout_ms": 30000
                },
                "title": "Test ACP HTTP",
                "protocol_version": 1,
                "cwd": ".",
                "retry": {
                    "max_retries": 5,
                    "retry_delays_ms": [1000, 3000, 5000, 5000, 5000]
                }
            }),
            icon_path: None,
            show_type_overlay: false,
            supported_features: vec![],
            tool_configuration: None,
            plugin_assignments: vec![],
            use_in_new_projects: true,
            source: None,
            zed_agent_name: None,
        };

        // Serialize to JSON
        let json_str = serde_json::to_string(&config).expect("Failed to serialize to JSON");

        // Deserialize from JSON
        let deserialized: ConnectorConfig =
            serde_json::from_str(&json_str).expect("Failed to deserialize from JSON");

        // Verify round-trip
        assert_eq!(deserialized.id, config.id);
        assert_eq!(deserialized.kind, config.kind);
        assert_eq!(deserialized.owner, config.owner);
        assert_eq!(deserialized.title, config.title);
        assert_eq!(
            deserialized.params["transport"]["base_url"],
            "http://localhost:3000"
        );
        assert_eq!(deserialized.params["transport"]["timeout_ms"], 30000);
    }

    #[test]
    fn test_core_config_with_acp_connectors_toml() {
        // Test full CoreConfig with ACP connectors serialized to TOML
        let acp_stdio_config = ConnectorConfig {
            id: Some("acp-1".to_string()),
            kind: ConnectorKind::Acp,
            owner: Some(uuid::Uuid::nil()),
            title: Some("ACP Stdio".to_string()),
            working_directory: None,
            params: json!({
                "transport": {
                    "type": "stdio",
                    "command": "claude",
                    "args": ["--acp"]
                },
                "title": "ACP Stdio",
                "protocol_version": 1,
                "cwd": ".",
                "retry": {
                    "max_retries": 5,
                    "retry_delays_ms": [1000, 3000, 5000, 5000, 5000]
                }
            }),
            icon_path: None,
            show_type_overlay: false,
            supported_features: vec![],
            tool_configuration: None,
            plugin_assignments: vec![],
            use_in_new_projects: true,
            source: None,
            zed_agent_name: None,
        };

        let acp_http_config = ConnectorConfig {
            id: Some("acp-2".to_string()),
            kind: ConnectorKind::Acp,
            owner: Some(uuid::Uuid::nil()),
            title: Some("ACP HTTP".to_string()),
            working_directory: None,
            params: json!({
                "transport": {
                    "type": "http",
                    "base_url": "http://localhost:3000",
                    "timeout_ms": 30000
                },
                "title": "ACP HTTP",
                "protocol_version": 1,
                "cwd": ".",
                "retry": {
                    "max_retries": 5,
                    "retry_delays_ms": [1000, 3000, 5000, 5000, 5000]
                }
            }),
            icon_path: None,
            show_type_overlay: false,
            supported_features: vec![],
            tool_configuration: None,
            plugin_assignments: vec![],
            use_in_new_projects: true,
            source: None,
            zed_agent_name: None,
        };

        let config = CoreConfig {
            connectors: vec![acp_stdio_config, acp_http_config],
            ..CoreConfig::default()
        };

        // Serialize to TOML
        let toml_str = toml::to_string_pretty(&config).expect("Failed to serialize to TOML");

        // Deserialize from TOML
        let deserialized: CoreConfig =
            toml::from_str(&toml_str).expect("Failed to deserialize from TOML");

        // Verify round-trip
        assert_eq!(deserialized.connectors.len(), 2);
        assert_eq!(deserialized.connectors[0].kind, ConnectorKind::Acp);
        assert_eq!(deserialized.connectors[1].kind, ConnectorKind::Acp);
        assert_eq!(
            deserialized.connectors[0].params["transport"]["type"],
            "stdio"
        );
        assert_eq!(
            deserialized.connectors[1].params["transport"]["type"],
            "http"
        );
    }

    #[test]
    fn test_core_config_with_acp_connectors_json() {
        // Test full CoreConfig with ACP connectors serialized to JSON
        let acp_config = ConnectorConfig {
            id: Some("acp-test".to_string()),
            kind: ConnectorKind::Acp,
            owner: Some(uuid::Uuid::nil()),
            title: Some("Test ACP".to_string()),
            working_directory: None,
            params: json!({
                "transport": {
                    "type": "stdio",
                    "command": "test-command",
                    "args": []
                },
                "title": "Test ACP",
                "protocol_version": 1,
                "cwd": ".",
                "retry": {
                    "max_retries": 5,
                    "retry_delays_ms": [1000, 3000, 5000, 5000, 5000]
                }
            }),
            icon_path: None,
            show_type_overlay: false,
            supported_features: vec![],
            tool_configuration: None,
            plugin_assignments: vec![],
            use_in_new_projects: true,
            source: None,
            zed_agent_name: None,
        };

        let config = CoreConfig {
            connectors: vec![acp_config],
            ..CoreConfig::default()
        };

        // Serialize to JSON
        let json_str = serde_json::to_string_pretty(&config).expect("Failed to serialize to JSON");

        // Deserialize from JSON
        let deserialized: CoreConfig =
            serde_json::from_str(&json_str).expect("Failed to deserialize from JSON");

        // Verify round-trip
        assert_eq!(deserialized.connectors.len(), 1);
        assert_eq!(deserialized.connectors[0].kind, ConnectorKind::Acp);
        assert_eq!(
            deserialized.connectors[0].params["transport"]["command"],
            "test-command"
        );
    }

    /// Regression test: CoreConfig::default() must roundtrip through TOML.
    /// This is the minimal config that gets saved on first launch.
    #[test]
    fn test_default_core_config_toml_roundtrip() {
        let config = CoreConfig::default();
        let toml_str = toml::to_string_pretty(&config)
            .expect("Default CoreConfig must serialize to TOML");
        let deserialized: CoreConfig =
            toml::from_str(&toml_str).expect("Default CoreConfig must deserialize from TOML");
        assert_eq!(deserialized.runtime_working_dir, config.runtime_working_dir);
    }

    /// Regression test: CoreConfig with tool_configuration must roundtrip through TOML.
    /// ToolHandler unit variants (Agent, Deny, etc.) previously failed with
    /// "unsupported unit type" when using adjacently-tagged serde representation.
    #[test]
    fn test_core_config_with_tool_configuration_toml() {
        use crate::tools::{ToolConfiguration, ToolDirective, ToolHandler};

        let mut tool_config = ToolConfiguration::new();
        tool_config.set(ToolDirective::passthrough("read_file"));
        tool_config.set(ToolDirective::checked("shell_exec", ToolHandler::Deny));
        tool_config.set(ToolDirective::checked("editor_write", ToolHandler::Editor));
        tool_config.set(ToolDirective::checked(
            "plugin_tool",
            ToolHandler::Plugin { name: "my_plugin".to_string() },
        ));

        let connector = ConnectorConfig {
            id: Some("test-acp".to_string()),
            kind: ConnectorKind::Acp,
            owner: None,
            title: Some("Test ACP".to_string()),
            working_directory: None,
            params: json!({
                "transport": { "type": "stdio", "command": "echo", "args": [] },
                "title": "Test ACP"
            }),
            icon_path: None,
            show_type_overlay: false,
            supported_features: vec![],
            tool_configuration: Some(tool_config),
            plugin_assignments: vec![],
            use_in_new_projects: true,
            source: None,
            zed_agent_name: None,
        };

        let config = CoreConfig {
            connectors: vec![connector],
            ..CoreConfig::default()
        };

        let toml_str = toml::to_string_pretty(&config)
            .expect("CoreConfig with tool_configuration must serialize to TOML");
        let deserialized: CoreConfig =
            toml::from_str(&toml_str).expect("CoreConfig with tool_configuration must deserialize from TOML");
        assert_eq!(deserialized.connectors.len(), 1);
        assert!(deserialized.connectors[0].tool_configuration.is_some());
    }

    /// Regression test: params containing JSON null values cause TOML serialization failure.
    /// TOML has no null type — serde_json::Value::Null serializes as unit `()`
    /// which toml crate rejects with "unsupported unit type".
    /// The fix: config_manager::save_config strips null values before TOML serialization.
    #[test]
    fn test_core_config_with_null_params_toml_fails_without_sanitization() {
        let connector = ConnectorConfig {
            id: Some("test".to_string()),
            kind: ConnectorKind::Acp,
            owner: None,
            title: Some("Test".to_string()),
            working_directory: None,
            params: json!({
                "transport": { "type": "stdio", "command": "echo", "args": [] },
                "initial_session": null,
                "cwd": null
            }),
            icon_path: None,
            show_type_overlay: false,
            supported_features: vec![],
            tool_configuration: None,
            plugin_assignments: vec![],
            use_in_new_projects: true,
            source: None,
            zed_agent_name: None,
        };

        let config = CoreConfig {
            connectors: vec![connector],
            ..CoreConfig::default()
        };

        // Raw TOML serialization fails with null values — this documents the known limitation
        let err = toml::to_string_pretty(&config).unwrap_err();
        assert!(err.to_string().contains("unsupported"), "Expected 'unsupported unit type' error, got: {}", err);
    }

    /// Regression test: after stripping null values, TOML serialization succeeds.
    #[test]
    fn test_core_config_with_null_params_toml_succeeds_after_sanitization() {
        use crate::runtime::config_manager::strip_json_nulls;

        let connector = ConnectorConfig {
            id: Some("test".to_string()),
            kind: ConnectorKind::Acp,
            owner: None,
            title: Some("Test".to_string()),
            working_directory: None,
            params: json!({
                "transport": { "type": "stdio", "command": "echo", "args": [] },
                "initial_session": null,
                "cwd": null,
                "nested": { "a": 1, "b": null }
            }),
            icon_path: None,
            show_type_overlay: false,
            supported_features: vec![],
            tool_configuration: None,
            plugin_assignments: vec![],
            use_in_new_projects: true,
            source: None,
            zed_agent_name: None,
        };

        let mut config = CoreConfig {
            connectors: vec![connector],
            ..CoreConfig::default()
        };

        // Strip nulls (same as config_manager::save_config does)
        for c in &mut config.connectors {
            strip_json_nulls(&mut c.params);
        }

        let toml_str = toml::to_string_pretty(&config)
            .expect("CoreConfig with sanitized params must serialize to TOML");
        let deserialized: CoreConfig =
            toml::from_str(&toml_str).expect("Must deserialize from TOML");
        assert_eq!(deserialized.connectors.len(), 1);
        // Verify nulls were stripped
        assert!(deserialized.connectors[0].params.get("initial_session").is_none());
        assert!(deserialized.connectors[0].params.get("cwd").is_none());
        assert_eq!(deserialized.connectors[0].params["nested"]["a"], 1);
        assert!(deserialized.connectors[0].params["nested"].get("b").is_none());
    }

    /// Regression test: CoreConfig with ACP server config must serialize to TOML.
    /// archive_root and projects_root are deprecated — they are deserializable for
    /// backward compatibility but must NOT appear in serialized output.
    #[test]
    fn test_core_config_with_acp_server_toml() {
        let config = CoreConfig {
            runtime_working_dir: PathBuf::from("."),
            connectors: vec![],
            archive_root: Some(PathBuf::from("/tmp/archive")),
            archives: Vec::new(),
            projects_root: Some(PathBuf::from("/tmp/projects")),
            acp_server: Some(AcpServerConfig {
                enabled: true,
                port: Some(3001),
                allowed_origins: None,
                max_connections: 50,
                default_connector_id: Some("default".to_string()),
            }),
            dismissed_zed_agents: vec!["old-agent".to_string()],
            tasks: vec![],
            accounts: HashMap::new(),
            matrix: None,
            streams: crate::sharing::StreamsConfig::default(),
            config_source_path: None,
            resolved_working_dir: None,
        };

        let toml_str = toml::to_string_pretty(&config)
            .expect("CoreConfig with ACP server must serialize to TOML");

        // Deprecated fields must NOT appear in serialized output
        assert!(
            !toml_str.contains("archive_root"),
            "archive_root should be skipped during serialization (deprecated)"
        );
        assert!(
            !toml_str.contains("projects_root"),
            "projects_root should be skipped during serialization (deprecated)"
        );

        let deserialized: CoreConfig =
            toml::from_str(&toml_str).expect("CoreConfig with ACP server must deserialize from TOML");
        assert_eq!(deserialized.acp_server.as_ref().unwrap().port, Some(3001));
        // Deprecated fields are absent from serialized TOML, so they deserialize as None
        assert_eq!(deserialized.archive_root, None);
        assert_eq!(deserialized.projects_root, None);
        assert_eq!(deserialized.dismissed_zed_agents, vec!["old-agent".to_string()]);

        // Verify backward compat: old TOML with archive_root still deserializes without error
        let legacy_toml = r#"
runtime_working_dir = "."
connectors = []
archive_root = "/tmp/archive"
projects_root = "/tmp/projects"
"#;
        let legacy: CoreConfig =
            toml::from_str(legacy_toml).expect("Legacy TOML with deprecated fields must deserialize");
        assert_eq!(legacy.archive_root, Some(PathBuf::from("/tmp/archive")));
        assert_eq!(legacy.projects_root, Some(PathBuf::from("/tmp/projects")));
    }

    /// Phase 4 Task 23: `[[streams]]` entries flatten onto CoreConfig.streams.
    ///
    /// The `streams` field uses `#[serde(flatten)]` and the inner
    /// `StreamsConfig::entries` is renamed to `"streams"`, so `[[streams]]`
    /// tables at the top level of dirigent.toml populate
    /// `cfg.streams.entries`.
    #[test]
    fn test_core_config_parses_streams_block() {
        let toml_str = r#"
runtime_working_dir = "."
connectors = []

[[streams]]
name = "lf"
type = "langfuse"

[streams.scope]
kind = "archive_wide"
acknowledged = false

[streams.params]
host = "http://example.com"
public_key = "pk"
secret_key = "sk"
"#;
        let cfg: CoreConfig =
            toml::from_str(toml_str).expect("CoreConfig with [[streams]] must parse");
        assert_eq!(cfg.streams.entries.len(), 1);
        assert_eq!(cfg.streams.entries[0].name, "lf");
        assert_eq!(cfg.streams.entries[0].kind, "langfuse");
        assert!(cfg.streams.entries[0].enabled);
    }

    /// Omitting `[[streams]]` entirely must still parse (empty vec default).
    #[test]
    fn test_core_config_no_streams_block_parses() {
        let cfg: CoreConfig = toml::from_str(
            r#"
runtime_working_dir = "."
connectors = []
"#,
        )
        .expect("CoreConfig without [[streams]] must parse");
        assert!(cfg.streams.entries.is_empty());
    }

    // Tests for working directory resolution

    #[test]
    fn test_resolve_default_runtime_working_directory_explicit() {
        // When working_directory is explicitly set, it should be used
        let connector_config = ConnectorConfig {
            id: None,
            kind: ConnectorKind::Mock,
            owner: None,
            title: None,
            working_directory: Some(PathBuf::from(".")),
            params: json!({}),
            icon_path: None,
            show_type_overlay: false,
            supported_features: vec![],
            tool_configuration: None,
            plugin_assignments: vec![],
            use_in_new_projects: true,
            source: None,
            zed_agent_name: None,
        };

        let global_config = CoreConfig {
            runtime_working_dir: PathBuf::from("/some/other/path"),
            ..CoreConfig::default()
        };

        let resolved = resolve_default_runtime_working_directory(&connector_config, &global_config);

        // Should resolve to current directory (canonicalized)
        assert!(resolved.exists());
        assert!(resolved.is_absolute());
    }

    #[test]
    fn test_resolve_default_runtime_working_directory_from_project_dir() {
        // When working_directory is not set, should use project_dir from global config
        let connector_config = ConnectorConfig {
            id: None,
            kind: ConnectorKind::Mock,
            owner: None,
            title: None,
            working_directory: None,
            params: json!({}),
            icon_path: None,
            show_type_overlay: false,
            supported_features: vec![],
            tool_configuration: None,
            plugin_assignments: vec![],
            use_in_new_projects: true,
            source: None,
            zed_agent_name: None,
        };

        let global_config = CoreConfig::default();

        let resolved = resolve_default_runtime_working_directory(&connector_config, &global_config);

        // Should resolve to project_dir (canonicalized)
        assert!(resolved.exists());
        assert!(resolved.is_absolute());
    }

    #[test]
    fn test_resolve_default_runtime_working_directory_nonexistent_path() {
        // When path doesn't exist, should still return the path (with warning)
        let connector_config = ConnectorConfig {
            id: None,
            kind: ConnectorKind::Mock,
            owner: None,
            title: None,
            working_directory: Some(PathBuf::from("/this/path/does/not/exist")),
            params: json!({}),
            icon_path: None,
            show_type_overlay: false,
            supported_features: vec![],
            tool_configuration: None,
            plugin_assignments: vec![],
            use_in_new_projects: true,
            source: None,
            zed_agent_name: None,
        };

        let global_config = CoreConfig::default();

        let resolved = resolve_default_runtime_working_directory(&connector_config, &global_config);

        // Should return the path even if it doesn't exist
        assert_eq!(resolved, PathBuf::from("/this/path/does/not/exist"));
    }

    #[test]
    fn test_connector_config_serialization_with_workdir() {
        // Test that working_directory serializes/deserializes correctly
        let config = ConnectorConfig {
            id: Some("test-conn".to_string()),
            kind: ConnectorKind::OpenCode,
            owner: Some(uuid::Uuid::nil()),
            title: Some("Test".to_string()),
            working_directory: Some(PathBuf::from("/path/to/workdir")),
            params: json!({"base_url": "http://localhost:12225"}),
            icon_path: None,
            show_type_overlay: false,
            supported_features: vec![],
            tool_configuration: None,
            plugin_assignments: vec![],
            use_in_new_projects: true,
            source: None,
            zed_agent_name: None,
        };

        let json = serde_json::to_string(&config).unwrap();
        let deserialized: ConnectorConfig = serde_json::from_str(&json).unwrap();

        assert_eq!(deserialized.working_directory, config.working_directory);
    }

    #[test]
    fn test_connector_config_serialization_without_workdir() {
        // Test that working_directory=None is skipped during serialization
        let config = ConnectorConfig {
            id: Some("test-conn".to_string()),
            kind: ConnectorKind::OpenCode,
            owner: Some(uuid::Uuid::nil()),
            title: Some("Test".to_string()),
            working_directory: None,
            params: json!({"base_url": "http://localhost:12225"}),
            icon_path: None,
            show_type_overlay: false,
            supported_features: vec![],
            tool_configuration: None,
            plugin_assignments: vec![],
            use_in_new_projects: true,
            source: None,
            zed_agent_name: None,
        };

        let json = serde_json::to_string(&config).unwrap();

        // Should not contain "working_directory" field
        assert!(!json.contains("working_directory"));

        let deserialized: ConnectorConfig = serde_json::from_str(&json).unwrap();
        assert_eq!(deserialized.working_directory, None);
    }

    #[test]
    fn test_load_config_no_env_returns_ok() {
        let config = CoreConfig::load_config(None);
        assert!(config.is_ok());
    }

    #[test]
    fn test_effective_working_dir_returns_resolved_when_set() {
        let mut config = CoreConfig::default();
        config.resolved_working_dir = Some(PathBuf::from("/resolved/path"));
        assert_eq!(config.effective_working_dir(), std::path::Path::new("/resolved/path"));
    }

    #[test]
    fn test_effective_working_dir_falls_back_to_raw_when_unresolved() {
        let config = CoreConfig::default();
        assert_eq!(config.effective_working_dir(), std::path::Path::new("."));
    }

    #[test]
    fn test_resolve_paths_relative_joins_config_dir() {
        let mut config = CoreConfig {
            runtime_working_dir: PathBuf::from("my-project"),
            ..CoreConfig::default()
        };
        config.resolve_paths(Some(std::path::Path::new("/some/config/dir")));
        let resolved = config.resolved_working_dir.as_ref().expect("should be resolved");
        assert_eq!(resolved, &PathBuf::from("/some/config/dir/my-project"));
    }

    #[test]
    fn test_resolve_paths_absolute_unchanged() {
        let mut config = CoreConfig {
            runtime_working_dir: PathBuf::from("/absolute/project"),
            ..CoreConfig::default()
        };
        config.resolve_paths(Some(std::path::Path::new("/some/config/dir")));
        let resolved = config.resolved_working_dir.as_ref().expect("should be resolved");
        assert_eq!(resolved, &PathBuf::from("/absolute/project"));
    }

    #[test]
    fn test_resolve_paths_dot_becomes_noproject_home() {
        let mut config = CoreConfig::default(); // runtime_working_dir = "."
        config.resolve_paths(Some(std::path::Path::new("/some/config/dir")));
        let resolved = config.resolved_working_dir.as_ref().expect("should be resolved");
        // "." should resolve to noproject_home, not to the config dir
        assert!(
            resolved.to_string_lossy().contains("noproject_home"),
            "Expected noproject_home, got: {:?}",
            resolved
        );
    }

    #[test]
    fn test_resolve_paths_no_config_dir_dot_still_noproject() {
        let mut config = CoreConfig::default(); // runtime_working_dir = "."
        config.resolve_paths(None);
        let resolved = config.resolved_working_dir.as_ref().expect("should be resolved");
        assert!(
            resolved.to_string_lossy().contains("noproject_home"),
            "Expected noproject_home, got: {:?}",
            resolved
        );
    }

    #[test]
    fn test_load_config_does_not_mutate_runtime_working_dir() {
        let dir = std::env::temp_dir().join("dirigent_test_no_mutate");
        let _ = std::fs::create_dir_all(&dir);
        let config_path = dir.join("dirigent.toml");
        std::fs::write(&config_path, "runtime_working_dir = \".\"\nconnectors = []\n").unwrap();

        let config = CoreConfig::load_config(Some(config_path.clone())).unwrap();

        // The raw field must remain "." — never mutated
        assert_eq!(
            config.runtime_working_dir,
            PathBuf::from("."),
            "runtime_working_dir should stay as stored value, got: {:?}",
            config.runtime_working_dir
        );

        // But the resolved field should be populated
        assert!(
            config.resolved_working_dir.is_some(),
            "resolved_working_dir should be populated by load_config"
        );

        let _ = std::fs::remove_dir_all(&dir);
    }

    #[test]
    fn test_save_config_preserves_stored_value_not_resolved() {
        // Simulate: load a config with ".", resolve it, serialize back
        let mut config = CoreConfig {
            runtime_working_dir: PathBuf::from("."),
            ..CoreConfig::default()
        };
        config.resolve_paths(Some(std::path::Path::new("/some/config/dir")));

        // resolved_working_dir should be set (noproject_home or similar)
        assert!(config.resolved_working_dir.is_some());

        // Serialize to TOML
        let toml_str = toml::to_string_pretty(&config)
            .expect("CoreConfig must serialize to TOML");

        // The TOML must contain the STORED value ".", not the resolved path
        assert!(
            toml_str.contains(r#"runtime_working_dir = ".""#),
            "TOML should contain stored value '.', got:\n{}",
            toml_str
        );
        assert!(
            !toml_str.contains("noproject_home"),
            "TOML must NOT contain resolved noproject_home path:\n{}",
            toml_str
        );

        // Round-trip: deserialize should give back "."
        let reloaded: CoreConfig = toml::from_str(&toml_str).unwrap();
        assert_eq!(reloaded.runtime_working_dir, PathBuf::from("."));
        // resolved_working_dir is skip-serialized, so it's None after deserialize
        assert!(reloaded.resolved_working_dir.is_none());
    }
}
