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

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

/// Returns true if the Nickel CLI is in PATH.
pub fn is_nickel_available() -> bool {
    Command::new("nickel")
        .arg("--version")
        .output()
        .map(|output| output.status.success())
        .unwrap_or(false)
}

/// Resolve NICKEL_IMPORT_PATH for a given NCL file.
///
/// Priority:
/// 1. Nearest `rustelo/nickel/` or `nickel/` directory found by walking up the
///    ancestor tree (up to 5 levels). `rustelo/nickel/` (new layout) is checked
///    before `nickel/` (legacy layout) at each level. Typical paths from entry
///    point `site/config/index.ncl`:
///    - L1: site/config/ → site/config/rustelo/nickel/, site/config/nickel/
///    - L2: site/       → site/rustelo/nickel/, site/nickel/
///    - L3: <impl-root>/→ <impl-root>/rustelo/nickel/  ← new canonical layout
/// 2. `NICKEL_IMPORT_PATH` environment variable (explicit override / CI
///    fallback).
///
/// The ancestor walk takes priority over the env var to avoid stale values from
/// the caller's shell.
fn resolve_import_path(ncl_path: &Path) -> Result<String> {
    let mut dir = ncl_path.parent();
    for _ in 0..5 {
        let Some(d) = dir else { break };
        for candidate in [d.join("rustelo").join("nickel"), d.join("nickel")] {
            if candidate.exists() {
                return Ok(candidate.to_string_lossy().into_owned());
            }
        }
        dir = d.parent();
    }

    // Explicit env var as fallback (CI, Docker, or non-standard layouts).
    if let Ok(path) = std::env::var("NICKEL_IMPORT_PATH") {
        if !path.is_empty() {
            return Ok(path);
        }
    }

    Err(ConfigError::validation_failed(format!(
        "No rustelo/nickel/ or nickel/ directory found in ancestor tree of {} (searched up to 5 \
         levels) and NICKEL_IMPORT_PATH is not set.\nExpected layout: <impl-root>/rustelo/nickel/ \
         (or legacy site/nickel/).",
        ncl_path.display()
    )))
}

/// Export a `.ncl` file to JSON using the Nickel CLI.
///
/// Sets `NICKEL_IMPORT_PATH` automatically via [`resolve_import_path`].
///
/// # Errors
/// - [`ConfigError::NickelNotInstalled`] if the CLI is absent.
/// - [`ConfigError::NickelExportFailed`] if the export exits non-zero.
/// - [`ConfigError::ConfigError`] if `NICKEL_IMPORT_PATH` cannot be resolved.
pub fn export_to_json<P: AsRef<Path>>(ncl_path: P) -> Result<String> {
    let ncl_path = ncl_path.as_ref();

    if !is_nickel_available() {
        return Err(ConfigError::NickelNotInstalled);
    }

    let import_path = resolve_import_path(ncl_path)?;

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

    let output = Command::new("nickel")
        .arg("export")
        .arg("--format")
        .arg("json")
        .arg(ncl_path)
        .env("NICKEL_IMPORT_PATH", &import_path)
        .output()
        .map_err(|e| ConfigError::io_error(format!("Failed to execute nickel CLI: {}", 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 nickel_availability_does_not_panic() {
        let _ = is_nickel_available();
    }
}
