//! Nickel CLI bridge for loading .ncl configuration files
//!
//! This module provides utilities to load Nickel configuration files by:
//! 1. Invoking `nickel export --format json <file>.ncl` via CLI
//! 2. Parsing the JSON output
//! 3. Deserializing into Rust config structs
//!
//! Pattern adapted from `platform-config` in provisioning project.

use std::path::Path;
use std::process::Command;

use serde::de::DeserializeOwned;
use tracing::{debug, info, warn};

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

/// Run `ncl-import-resolver` on the manifest adjacent to `ncl_file` if present.
///
/// Looks for `resolver-manifest.json` in the same directory as `ncl_file`.
/// When the manifest is absent the function is a no-op, so callers in
/// environments without the tool are unaffected.
///
/// # Errors
///
/// Returns `KbError::NickelExport` if:
/// - `ncl-import-resolver` is not in PATH when the manifest exists
/// - The resolver process exits non-zero
fn resolve_nickel_imports(ncl_file: &Path) -> Result<()> {
    let manifest = ncl_file
        .parent()
        .unwrap_or(Path::new("."))
        .join("resolver-manifest.json");

    if !manifest.exists() {
        return Ok(());
    }

    let output = Command::new("ncl-import-resolver")
        .arg(&manifest)
        .output()
        .map_err(|e| {
            KbError::NickelExport(format!("ncl-import-resolver unavailable: {e}"))
        })?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        return Err(KbError::NickelExport(format!(
            "ncl-import-resolver failed for {}: {stderr}",
            manifest.display()
        )));
    }

    Ok(())
}

/// Export a Nickel file to JSON using the Nickel CLI
///
/// # Arguments
///
/// * `nickel_file` - Path to the .ncl file
///
/// # Returns
///
/// JSON string output from `nickel export --format json`
///
/// # Errors
///
/// Returns `KbError::NickelExport` if:
/// - Nickel CLI is not installed
/// - The file doesn't exist
/// - Nickel export command fails
/// - Output is not valid UTF-8
pub fn export_nickel_to_json<P: AsRef<Path>>(nickel_file: P) -> Result<String> {
    let path = nickel_file.as_ref();

    if !path.exists() {
        return Err(KbError::NickelExport(format!(
            "Nickel file not found: {}",
            path.display()
        )));
    }

    resolve_nickel_imports(path)?;

    info!("Exporting Nickel file to JSON: {}", path.display());

    let output = Command::new("nickel")
        .arg("export")
        .arg("--format")
        .arg("json")
        .arg(path)
        .output()
        .map_err(|e| {
            KbError::NickelExport(format!(
                "Failed to execute nickel command (is it installed?): {e}"
            ))
        })?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        return Err(KbError::NickelExport(format!(
            "Nickel export failed for {}: {}",
            path.display(),
            stderr
        )));
    }

    let json = String::from_utf8(output.stdout)
        .map_err(|e| KbError::NickelExport(format!("Nickel output is not valid UTF-8: {e}")))?;

    debug!("Nickel export successful: {} bytes", json.len());
    Ok(json)
}

/// Load and deserialize a Nickel configuration file
///
/// # Arguments
///
/// * `nickel_file` - Path to the .ncl file
///
/// # Returns
///
/// Deserialized configuration struct
///
/// # Errors
///
/// Returns error if:
/// - Nickel export fails
/// - JSON deserialization fails
///
/// # Example
///
/// ```no_run
/// use kogral_core::config::{nickel, schema::KbConfig};
/// use std::path::Path;
///
/// let config: KbConfig = nickel::load_nickel_config(Path::new("kb.ncl"))?;
/// # Ok::<(), kogral_core::error::KbError>(())
/// ```
pub fn load_nickel_config<T, P>(nickel_file: P) -> Result<T>
where
    T: DeserializeOwned,
    P: AsRef<Path>,
{
    let path = nickel_file.as_ref();
    info!("Loading Nickel config from: {}", path.display());

    let json = export_nickel_to_json(path)?;

    let config: T = serde_json::from_str(&json).map_err(|e| {
        KbError::Serialization(format!(
            "Failed to deserialize config from {}: {}",
            path.display(),
            e
        ))
    })?;

    debug!("Config loaded successfully from Nickel");
    Ok(config)
}

/// Check if Nickel CLI is available
///
/// # Returns
///
/// `true` if `nickel` command is in PATH, `false` otherwise
#[must_use]
pub fn is_nickel_available() -> bool {
    Command::new("nickel")
        .arg("--version")
        .output()
        .is_ok_and(|output| output.status.success())
}

/// Get Nickel CLI version
///
/// # Returns
///
/// Version string if available, or an error
///
/// # Errors
///
/// Returns an error if nickel binary cannot be executed or version cannot be
/// parsed
pub fn nickel_version() -> Result<String> {
    let output = Command::new("nickel")
        .arg("--version")
        .output()
        .map_err(|e| KbError::NickelExport(format!("Failed to check nickel version: {e}")))?;

    if !output.status.success() {
        return Err(KbError::NickelExport(
            "Nickel CLI not available".to_string(),
        ));
    }

    let version = String::from_utf8(output.stdout)
        .map_err(|e| KbError::NickelExport(format!("Invalid version output: {e}")))?;

    Ok(version.trim().to_string())
}

/// Warn if Nickel is not available and return fallback behavior
///
/// This is useful for optional Nickel support where we can fall back to
/// TOML/JSON
pub fn warn_if_nickel_unavailable() {
    if !is_nickel_available() {
        warn!("Nickel CLI not found in PATH. Install from: https://nickel-lang.org/");
        warn!("Falling back to TOML/JSON configuration support only");
    }
}

#[cfg(test)]
mod tests {
    use std::fs;

    use tempfile::tempdir;

    use super::*;

    #[test]
    fn test_is_nickel_available() {
        // This test will pass/fail depending on local environment
        let available = is_nickel_available();
        println!("Nickel available: {}", available);
    }

    #[test]
    fn test_nickel_version() {
        if is_nickel_available() {
            let version = nickel_version();
            assert!(version.is_ok());
            println!("Nickel version: {}", version.unwrap());
        }
    }

    #[test]
    fn test_export_nonexistent_file() {
        let result = export_nickel_to_json("/nonexistent/file.ncl");
        assert!(result.is_err());
        assert!(matches!(result, Err(KbError::NickelExport(_))));
    }

    #[test]
    #[ignore] // Requires Nickel CLI to be installed
    fn test_export_valid_nickel() {
        if !is_nickel_available() {
            eprintln!("Skipping test: Nickel CLI not available");
            return;
        }

        let dir = tempdir().unwrap();
        let file_path = dir.path().join("test.ncl");

        // Write a simple Nickel file
        fs::write(
            &file_path,
            r#"
            {
                name = "test",
                value = 42,
            }
            "#,
        )
        .unwrap();

        let result = export_nickel_to_json(&file_path);
        assert!(result.is_ok());

        let json = result.unwrap();
        assert!(json.contains("name"));
        assert!(json.contains("test"));
        assert!(json.contains("42"));
    }

    #[test]
    #[ignore] // Requires Nickel CLI to be installed
    fn test_load_nickel_config() {
        use serde::{Deserialize, Serialize};

        if !is_nickel_available() {
            eprintln!("Skipping test: Nickel CLI not available");
            return;
        }

        #[derive(Debug, Serialize, Deserialize, PartialEq)]
        struct TestConfig {
            name: String,
            value: i32,
        }

        let dir = tempdir().unwrap();
        let file_path = dir.path().join("test.ncl");

        fs::write(
            &file_path,
            r#"
            {
                name = "test-config",
                value = 123,
            }
            "#,
        )
        .unwrap();

        let config: Result<TestConfig> = load_nickel_config(&file_path);
        assert!(config.is_ok());

        let config = config.unwrap();
        assert_eq!(config.name, "test-config");
        assert_eq!(config.value, 123);
    }

    #[test]
    fn test_warn_if_nickel_unavailable() {
        // Just ensure it doesn't panic
        warn_if_nickel_unavailable();
    }
}
