//! Configuration Management
//!
//! This module handles saving and updating runtime configuration.
//! It provides atomic file writes and supports JSON/TOML formats.

use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::sync::RwLock;
use tracing::{debug, error, info, warn};

use crate::config::{AcpServerConfig, CoreConfig};
use crate::CoreError;

/// Recursively strip JSON null values from a serde_json::Value.
///
/// TOML has no null type, so `serde_json::Value::Null` serializes as `()`
/// which the toml crate rejects with "unsupported unit type". This function
/// removes null entries from objects and arrays before TOML serialization.
pub fn strip_json_nulls(value: &mut serde_json::Value) {
    match value {
        serde_json::Value::Object(map) => {
            map.retain(|_, v| !v.is_null());
            for v in map.values_mut() {
                strip_json_nulls(v);
            }
        }
        serde_json::Value::Array(arr) => {
            arr.retain(|v| !v.is_null());
            for v in arr.iter_mut() {
                strip_json_nulls(v);
            }
        }
        _ => {}
    }
}

/// Check whether a config path is project-local (relative or under CWD).
fn is_project_local(path: &Path) -> bool {
    path.is_relative() || path.starts_with(std::env::current_dir().unwrap_or_default())
}

/// Promote a project-local config path to the user config directory,
/// preserving the file extension. Returns the original path if the
/// user config directory cannot be resolved.
fn promote_to_user_dir(source: &Path) -> PathBuf {
    if let Ok(paths) = dirigent_config::DirigentPaths::resolve() {
        let user_dir = paths.config_dir().to_path_buf();
        let _ = std::fs::create_dir_all(&user_dir);
        let ext = source
            .extension()
            .and_then(|e| e.to_str())
            .unwrap_or("toml");
        user_dir.join(format!("dirigent.{}", ext))
    } else {
        source.to_path_buf()
    }
}

/// Return the default user config path for new saves.
fn default_user_config_path() -> PathBuf {
    if let Ok(paths) = dirigent_config::DirigentPaths::resolve() {
        let user_dir = paths.config_dir().to_path_buf();
        let _ = std::fs::create_dir_all(&user_dir);
        user_dir.join("dirigent.toml")
    } else {
        PathBuf::from("dirigent.json")
    }
}

/// Update the ACP server configuration
///
/// This updates the in-memory configuration. Call `save_config` to persist.
pub async fn update_acp_server_config(
    config: &Arc<RwLock<CoreConfig>>,
    acp_config: AcpServerConfig,
) -> Result<(), CoreError> {
    let mut cfg = config.write().await;
    cfg.acp_server = Some(acp_config);
    info!("Updated ACP Server configuration");
    Ok(())
}

/// Save the current configuration to disk
///
/// This method serializes the current runtime configuration to JSON or TOML format
/// and writes it to the filesystem atomically (write to temp file, then rename).
///
/// The save path is determined as follows:
/// 1. The file the config was originally loaded from (`config_source_path`),
///    promoted to the user config directory if it was project-local
/// 2. User config directory default (`~/.config/dirigent/dirigent.toml`)
///
/// Project-local config files (relative paths or under CWD) are treated as
/// read-only: on save the config is promoted to the user config directory
/// so that the original project-local file is never modified.
///
/// The file format is determined by the extension of the save path.
pub async fn save_config(config: &Arc<RwLock<CoreConfig>>) -> Result<(), CoreError> {
    use std::io::Write;

    // Lock config for read and create a sanitized copy for saving.
    // Zed-sourced connectors (source == "zed") are transient runtime entries.
    // They should not be persisted — stripping them prevents ghost duplicates.
    let config_guard = config.read().await;
    let mut save_config = config_guard.clone();
    let zed_count = save_config
        .connectors
        .iter()
        .filter(|c| c.source.as_deref() == Some("zed"))
        .count();
    if zed_count > 0 {
        debug!(
            count = zed_count,
            "Stripping transient Zed-sourced connectors from saved config"
        );
        save_config
            .connectors
            .retain(|c| c.source.as_deref() != Some("zed"));
    }
    // Capture the source path before releasing the lock
    let source_path = config_guard.config_source_path.clone();
    drop(config_guard); // Release lock early — we work from the cloned copy

    // Determine save path with promotion logic:
    // - Project-local sources are promoted to the user config directory
    // - Explicit user-dir or env-var paths are used as-is
    let save_path = if let Some(ref source) = source_path {
        if is_project_local(source) {
            let promoted = promote_to_user_dir(source);
            info!(
                from = %source.display(),
                to = %promoted.display(),
                "Promoting project-local config to user config directory"
            );
            promoted
        } else {
            source.clone()
        }
    } else {
        default_user_config_path()
    };

    // Update config_source_path so subsequent saves go to the same location
    if source_path.as_ref() != Some(&save_path) {
        config.write().await.config_source_path = Some(save_path.clone());
    }

    info!(path = ?save_path, "Saving configuration");

    // Determine format by extension
    let contents = match save_path.extension().and_then(|s| s.to_str()) {
        Some("toml") => {
            debug!("Serializing config as TOML");
            // TOML has no null type — strip JSON nulls from connector params
            for connector in &mut save_config.connectors {
                strip_json_nulls(&mut connector.params);
            }
            toml::to_string_pretty(&save_config).map_err(|e| {
                error!(error = %e, "Failed to serialize config as TOML");
                CoreError::Internal(format!("Failed to serialize config as TOML: {}", e))
            })?
        }
        Some("json") | None => {
            debug!("Serializing config as JSON");
            serde_json::to_string_pretty(&save_config).map_err(|e| {
                error!(error = %e, "Failed to serialize config as JSON");
                CoreError::Internal(format!("Failed to serialize config as JSON: {}", e))
            })?
        }
        Some(ext) => {
            warn!(
                extension = ext,
                "Unsupported config file extension, defaulting to JSON"
            );
            serde_json::to_string_pretty(&save_config).map_err(|e| {
                error!(error = %e, "Failed to serialize config as JSON");
                CoreError::Internal(format!("Failed to serialize config as JSON: {}", e))
            })?
        }
    };

    // Ensure the parent directory exists (critical for first save to user config dir)
    if let Some(parent) = save_path.parent() {
        if !parent.exists() {
            std::fs::create_dir_all(parent).map_err(|e| {
                error!(error = %e, path = ?parent, "Failed to create config directory");
                CoreError::Internal(format!(
                    "Failed to create config directory {}: {}",
                    parent.display(),
                    e
                ))
            })?;
            info!(path = ?parent, "Created config directory");
        }
    }

    // Write atomically: write to temp file, then rename
    let temp_path = save_path.with_extension("tmp");
    debug!(temp_path = ?temp_path, "Writing to temporary file");

    // Write to temp file
    {
        let mut file = std::fs::File::create(&temp_path).map_err(|e| {
            error!(error = %e, path = ?temp_path, "Failed to create temp config file");
            CoreError::Internal(format!(
                "Failed to create temp config file {}: {}",
                temp_path.display(),
                e
            ))
        })?;

        file.write_all(contents.as_bytes()).map_err(|e| {
            error!(error = %e, path = ?temp_path, "Failed to write to temp config file");
            CoreError::Internal(format!(
                "Failed to write to temp config file {}: {}",
                temp_path.display(),
                e
            ))
        })?;

        file.sync_all().map_err(|e| {
            error!(error = %e, path = ?temp_path, "Failed to sync temp config file");
            CoreError::Internal(format!(
                "Failed to sync temp config file {}: {}",
                temp_path.display(),
                e
            ))
        })?;
    }

    // Rename temp file to final path
    debug!(from = ?temp_path, to = ?save_path, "Renaming temp file to final path");
    std::fs::rename(&temp_path, &save_path).map_err(|e| {
        error!(error = %e, from = ?temp_path, to = ?save_path, "Failed to rename temp file");
        CoreError::Internal(format!(
            "Failed to rename temp file {} to {}: {}",
            temp_path.display(),
            save_path.display(),
            e
        ))
    })?;

    info!(path = ?save_path, "Configuration saved successfully");

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::config::CoreConfig;
    use std::sync::Arc;
    use tokio::sync::RwLock;

    #[tokio::test]
    async fn test_update_acp_server_config() {
        let config = Arc::new(RwLock::new(CoreConfig::default()));

        let acp_config = AcpServerConfig {
            enabled: true,
            port: Some(3001),
            allowed_origins: None,
            max_connections: 100,
            default_connector_id: None,
        };

        let result = update_acp_server_config(&config, acp_config).await;
        assert!(result.is_ok());

        let cfg = config.read().await;
        assert!(cfg.acp_server.is_some());
        assert_eq!(cfg.acp_server.as_ref().unwrap().port, Some(3001));
    }
}
