use std::env;
use std::path::{Path, PathBuf};
use std::process::Command;

use crate::error::{ConfigError, Result};

/// Verifica si el CLI de Nickel está disponible
pub fn is_nickel_available() -> bool {
    Command::new("nickel")
        .arg("--version")
        .output()
        .map(|output| output.status.success())
        .unwrap_or(false)
}

/// Scans ~/.cache/provisioning/extensions/ for cached extension schemas
fn scan_extension_schemas(home: &str) -> Vec<PathBuf> {
    let extensions_cache = PathBuf::from(format!("{}/.cache/provisioning/extensions", home));

    if !extensions_cache.exists() {
        return Vec::new();
    }

    let mut extension_paths = Vec::new();

    match std::fs::read_dir(&extensions_cache) {
        Ok(entries) => {
            for entry in entries.flatten() {
                let path = entry.path();
                if path.is_dir() {
                    // Look for schemas subdirectory in each extension dir
                    // Expect structure: extensions/{name}@{version}/schemas
                    let schemas_path = path.join("schemas");
                    if schemas_path.exists() {
                        tracing::debug!("Found extension schemas cache at: {:?}", schemas_path);
                        extension_paths.push(schemas_path);
                    }
                }
            }
        }
        Err(e) => {
            tracing::warn!("Failed to scan extension schemas cache: {}", e);
        }
    }

    extension_paths
}

/// Returns the platform user data directory: ~/Library/Application Support/provisioning (macOS)
/// or ~/.local/share/provisioning (Linux).
fn platform_user_data_dir(home: &str) -> PathBuf {
    #[cfg(target_os = "macos")]
    return PathBuf::from(home).join("Library/Application Support/provisioning");
    #[cfg(not(target_os = "macos"))]
    return PathBuf::from(home).join(".local/share/provisioning");
}

/// Reads the stored provisioning project root from the user data dir.
///
/// Written by `prvng` (via `write-provisioning-root` in platform/target.nu) when
/// `$PROVISIONING` is available. Acts as a fallback for Rust binaries launched
/// outside the CLI wrapper where `PROVISIONING` is not set.
pub fn read_stored_provisioning_root(home: &str) -> Option<PathBuf> {
    let root_file = platform_user_data_dir(home).join("project-root");
    std::fs::read_to_string(&root_file)
        .ok()
        .map(|s| PathBuf::from(s.trim()))
        .filter(|p| p.exists())
}

/// Persists the provisioning project root to the user data dir.
///
/// Called from the CLI wrapper or platform init so that Rust binaries
/// can resolve schema imports without requiring `PROVISIONING` in env.
pub fn write_provisioning_root(home: &str, root: &Path) -> std::io::Result<()> {
    let data_dir = platform_user_data_dir(home);
    std::fs::create_dir_all(&data_dir)?;
    std::fs::write(data_dir.join("project-root"), root.to_string_lossy().as_bytes())
}

/// Construye el NICKEL_IMPORT_PATH correcto usando:
/// 1. Variable de entorno NICKEL_IMPORT_PATH si existe (additive, prepended)
/// 2. Extension schemas (scan ~/.cache/provisioning/extensions/*/schemas)
/// 3. Git cache directories (schemas, configs)
/// 4. $PROVISIONING env var (repo provisioning/ root — set by CLI wrapper)
/// 5. Stored project-root file (fallback when PROVISIONING not in env)
/// 6. Path relativo al directorio actual (provisioning/schemas parent)
/// 7. PROVISIONING_USER_PLATFORM
fn build_nickel_import_path() -> Result<String> {
    let mut paths = Vec::new();

    // Priority 1: Explicit NICKEL_IMPORT_PATH env var (additive - prepend to final
    // path)
    let explicit_path = env::var("NICKEL_IMPORT_PATH").ok();

    // Priority 2: Extension schemas (scan
    // ~/.cache/provisioning/extensions/*/schemas)
    let home = env::var("HOME").ok();

    if let Some(ref home) = home {
        let mut ext_paths = scan_extension_schemas(home);
        paths.append(&mut ext_paths);

        // Priority 3: Git cache directories (if they exist)
        let schemas_cache = PathBuf::from(format!("{}/.cache/provisioning/schemas", home));
        if schemas_cache.exists() {
            tracing::debug!("Found Git schemas cache at: {:?}", schemas_cache);
            paths.push(schemas_cache);
        }

        let configs_cache = PathBuf::from(format!("{}/.cache/provisioning/configs", home));
        if configs_cache.exists() {
            tracing::debug!("Found Git configs cache at: {:?}", configs_cache);
            paths.push(configs_cache);
        }
    }

    // Priority 4: $PROVISIONING — repo provisioning/ root (set by the CLI wrapper).
    // schemas/ lives directly under this directory, so Nickel can resolve
    // `import "schemas/platform/..."` when this path is in NICKEL_IMPORT_PATH.
    if let Ok(provisioning_root) = env::var("PROVISIONING") {
        let root = PathBuf::from(&provisioning_root);
        if root.exists() {
            tracing::debug!("Found PROVISIONING root at: {:?}", root);
            paths.push(root);
        }
    } else if let Some(ref home) = home {
        // Priority 4b: stored project-root file — written by the CLI wrapper,
        // used as fallback when the binary is launched outside `prvng`.
        if let Some(stored_root) = read_stored_provisioning_root(home) {
            tracing::debug!("Found stored provisioning root at: {:?}", stored_root);
            paths.push(stored_root);
        }
    }

    // Priority 5: Find provisioning/schemas relative to current directory
    if let Ok(cwd) = env::current_dir() {
        let schemas_path = cwd.join("provisioning").join("schemas");
        if schemas_path.exists() {
            tracing::debug!("Found provisioning/schemas at: {:?}", schemas_path);
            paths.push(schemas_path);
        }
    }

    // Priority 6: Use PROVISIONING_USER_PLATFORM from main provisioning script
    if let Ok(user_platform) = env::var("PROVISIONING_USER_PLATFORM") {
        let user_platform_path = PathBuf::from(&user_platform);
        if user_platform_path.exists() {
            tracing::debug!("Found PROVISIONING_USER_PLATFORM at: {}", user_platform);
            paths.push(user_platform_path);
        }
    }

    // Build final path, optionally prepend explicit env var
    if paths.is_empty() {
        tracing::warn!("No NICKEL_IMPORT_PATH sources found, using default 'provisioning/schemas'");
        paths.push(PathBuf::from("provisioning/schemas"));
    }

    let mut import_path = paths
        .iter()
        .map(|p| p.to_string_lossy().to_string())
        .collect::<Vec<_>>()
        .join(":");

    // If explicit NICKEL_IMPORT_PATH was set, prepend it to allow overrides
    if let Some(explicit) = explicit_path {
        tracing::debug!("Prepending explicit NICKEL_IMPORT_PATH from environment");
        import_path = format!("{}:{}", explicit, import_path);
    }

    Ok(import_path)
}

/// Exporta un archivo NCL a JSON usando el CLI de Nickel
///
/// # Arguments
/// - `ncl_path`: Ruta al archivo .ncl a exportar
///
/// # Returns
/// String con el JSON resultante de la exportación
///
/// # Errors
/// - `NickelNotInstalled` si el CLI no está disponible
/// - `NickelExportFailed` si la exportación falla
pub fn export_to_json<P: AsRef<Path>>(ncl_path: P) -> Result<String> {
    export_with_overrides(ncl_path, &serde_json::Value::Null)
}

/// Converts a `serde_json::Value` into a Nickel record literal.
///
/// Nickel uses `{ key = value }` syntax; JSON's `{"key": value}` is invalid
/// Nickel because `:` is a contract annotation, not assignment.
/// Generates a Nickel record literal from a JSON value.
///
/// Record containers are emitted without merge priority so Nickel deep-merges
/// them with the base config. Leaf values (strings, numbers, bools, arrays)
/// are emitted with `| priority 1` so they win over user config assignments
/// at the default (priority 0) level, without dropping unrelated fields.
fn json_to_nickel_record(value: &serde_json::Value) -> String {
    json_to_nickel_object(value, 0)
}

fn json_to_nickel_object(value: &serde_json::Value, indent: usize) -> String {
    match value {
        serde_json::Value::Object(map) => {
            if map.is_empty() {
                return "{}".to_string();
            }
            let pad = "  ".repeat(indent);
            let inner_pad = "  ".repeat(indent + 1);
            let fields: Vec<String> = map
                .iter()
                .map(|(k, v)| match v {
                    serde_json::Value::Object(_) => {
                        format!("{}{} = {}", inner_pad, k, json_to_nickel_object(v, indent + 1))
                    }
                    _ => format!(
                        "{}{} | priority 1 = {}",
                        inner_pad,
                        k,
                        json_to_nickel_leaf(v)
                    ),
                })
                .collect();
            format!("{{\n{}\n{}}}", fields.join(",\n"), pad)
        }
        _ => json_to_nickel_leaf(value),
    }
}

fn json_to_nickel_leaf(value: &serde_json::Value) -> String {
    match value {
        serde_json::Value::Null => "null".to_string(),
        serde_json::Value::Bool(b) => b.to_string(),
        serde_json::Value::Number(n) => n.to_string(),
        serde_json::Value::String(s) => {
            format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
        }
        serde_json::Value::Array(arr) => {
            let items: Vec<String> = arr.iter().map(json_to_nickel_leaf).collect();
            format!("[{}]", items.join(", "))
        }
        serde_json::Value::Object(_) => json_to_nickel_object(value, 0),
    }
}

/// Exporta un NCL a JSON, mergeando overrides dinámicos (env vars, CLI args)
/// como un record Nickel temporal. Los contracts del schema base se aplican
/// al resultado mergeado, validando los overrides contra las constraints.
///
/// Si `overrides` es null o un objeto vacío, se comporta como `export_to_json`.
///
/// # Errors
/// - `NickelNotInstalled` si el CLI no está disponible
/// - `NickelExportFailed` si la exportación o validación de contracts falla
///   (e.g., un override viola un constraint como port_high)
pub fn export_with_overrides<P: AsRef<Path>>(
    ncl_path: P,
    overrides: &serde_json::Value,
) -> Result<String> {
    let ncl_path = ncl_path.as_ref();

    if !is_nickel_available() {
        tracing::warn!("Nickel CLI not installed - cannot load {:?}", ncl_path);
        return Err(ConfigError::NickelNotInstalled);
    }

    tracing::debug!("Exporting NCL config to JSON: {:?}", ncl_path);

    let import_path = build_nickel_import_path()?;
    tracing::debug!("Using NICKEL_IMPORT_PATH: {}", import_path);

    let has_overrides = matches!(overrides, serde_json::Value::Object(m) if !m.is_empty());

    // Build override temp file if needed.
    // JSON uses `{"k": v}` but Nickel interprets `:` as a contract annotation.
    // We must emit valid Nickel record syntax: `{ k = v }`.
    let override_file = if has_overrides {
        let override_content = json_to_nickel_record(overrides);

        let tmp_dir = std::env::temp_dir();
        let override_path = tmp_dir.join(format!("prov-override-{}.ncl", std::process::id()));

        std::fs::write(&override_path, &override_content).map_err(|e| {
            ConfigError::io_error(format!("Failed to write override file: {}", e))
        })?;

        tracing::debug!(
            "Created override file at {:?} with content: {}",
            override_path,
            override_content
        );

        Some(override_path)
    } else {
        None
    };

    let mut cmd = Command::new("nickel");
    cmd.arg("export")
        .arg("--format")
        .arg("json")
        .arg(ncl_path)
        .env("NICKEL_IMPORT_PATH", &import_path);

    // Nickel merges multiple input files: base config + overrides
    if let Some(ref path) = override_file {
        cmd.arg(path);
    }

    let output = cmd
        .output()
        .map_err(|e| ConfigError::io_error(format!("Failed to execute nickel CLI: {}", e)))?;

    // Cleanup temp file regardless of outcome
    if let Some(ref path) = override_file {
        if let Err(e) = std::fs::remove_file(path) {
            tracing::warn!("Failed to cleanup override file {:?}: {}", path, e);
        }
    }

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        tracing::error!("Nickel export failed for {:?}: {}", ncl_path, stderr);
        return Err(ConfigError::nickel_failed(stderr.to_string()));
    }

    let json_string = String::from_utf8(output.stdout).map_err(|e| {
        ConfigError::deserialization_failed(format!("Invalid UTF-8 in nickel output: {}", e))
    })?;

    tracing::debug!(
        "Successfully exported NCL config ({} bytes)",
        json_string.len()
    );

    Ok(json_string)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_is_nickel_available() {
        // This test just verifies the function doesn't panic
        let _ = is_nickel_available();
    }
}
