// vapora-shared: Nickel CLI bridge - Execute Nickel commands for schema
// operations Provides interface to nickel query, typecheck, and export commands

use std::path::{Path, PathBuf};
use std::process::Stdio;

use tokio::process::Command;
use tracing::debug;

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

/// Nickel CLI bridge for schema operations
#[derive(Debug, Clone)]
pub struct NickelCli {
    /// Path to nickel executable
    nickel_path: PathBuf,

    /// Timeout for nickel commands (milliseconds)
    timeout_ms: u64,
}

impl NickelCli {
    /// Create new Nickel CLI bridge with default path
    pub fn new() -> Self {
        Self {
            nickel_path: PathBuf::from("nickel"),
            timeout_ms: 30_000, // 30 seconds
        }
    }

    /// Create Nickel CLI bridge with custom path
    pub fn with_path(nickel_path: PathBuf) -> Self {
        Self {
            nickel_path,
            timeout_ms: 30_000,
        }
    }

    /// Set timeout for Nickel commands
    pub fn with_timeout(mut self, timeout_ms: u64) -> Self {
        self.timeout_ms = timeout_ms;
        self
    }

    /// Execute `nickel query` to extract schema metadata
    ///
    /// Returns JSON representation of Nickel value
    pub async fn query(&self, path: &Path, field: Option<&str>) -> Result<serde_json::Value> {
        let mut args = vec!["query", "--format", "json"];

        if let Some(f) = field {
            args.push("--field");
            args.push(f);
        }

        let path_str = path.to_str().ok_or_else(|| {
            VaporaError::ValidationError(format!("Invalid path: {}", path.display()))
        })?;
        args.push(path_str);

        debug!("Executing: nickel {}", args.join(" "));

        let output = self.execute_with_timeout(&args).await?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            return Err(VaporaError::ValidationError(format!(
                "Nickel query failed: {}",
                stderr
            )));
        }

        let stdout = String::from_utf8_lossy(&output.stdout);

        serde_json::from_str(&stdout).map_err(|e| {
            VaporaError::ValidationError(format!("Failed to parse Nickel output: {}", e))
        })
    }

    /// Execute `nickel typecheck` to validate schema
    ///
    /// Returns Ok(()) if typecheck passes, Err with details if it fails
    pub async fn typecheck(&self, path: &Path) -> Result<()> {
        let path_str = path.to_str().ok_or_else(|| {
            VaporaError::ValidationError(format!("Invalid path: {}", path.display()))
        })?;

        let args = vec!["typecheck", path_str];

        debug!("Executing: nickel {}", args.join(" "));

        let output = self.execute_with_timeout(&args).await?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            return Err(VaporaError::ValidationError(format!(
                "Nickel typecheck failed: {}",
                stderr
            )));
        }

        Ok(())
    }

    /// Execute `nickel export` to generate JSON output
    ///
    /// Returns exported JSON value
    pub async fn export(&self, path: &Path) -> Result<serde_json::Value> {
        let path_str = path.to_str().ok_or_else(|| {
            VaporaError::ValidationError(format!("Invalid path: {}", path.display()))
        })?;

        let args = vec!["export", "--format", "json", path_str];

        debug!("Executing: nickel {}", args.join(" "));

        let output = self.execute_with_timeout(&args).await?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            return Err(VaporaError::ValidationError(format!(
                "Nickel export failed: {}",
                stderr
            )));
        }

        let stdout = String::from_utf8_lossy(&output.stdout);

        serde_json::from_str(&stdout).map_err(|e| {
            VaporaError::ValidationError(format!("Failed to parse Nickel export: {}", e))
        })
    }

    /// Check if Nickel CLI is available
    pub async fn is_available(&self) -> bool {
        let result = Command::new(&self.nickel_path)
            .arg("--version")
            .stdout(Stdio::null())
            .stderr(Stdio::null())
            .status()
            .await;

        match result {
            Ok(status) => status.success(),
            Err(_) => false,
        }
    }

    /// Get Nickel CLI version
    pub async fn version(&self) -> Result<String> {
        let output = Command::new(&self.nickel_path)
            .arg("--version")
            .output()
            .await
            .map_err(|e| {
                VaporaError::ValidationError(format!("Failed to get Nickel version: {}", e))
            })?;

        if !output.status.success() {
            return Err(VaporaError::ValidationError(
                "Failed to get Nickel version".to_string(),
            ));
        }

        let version = String::from_utf8_lossy(&output.stdout);
        Ok(version.trim().to_string())
    }

    /// Execute Nickel command with timeout
    async fn execute_with_timeout(&self, args: &[&str]) -> Result<std::process::Output> {
        let child = Command::new(&self.nickel_path)
            .args(args)
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .spawn()
            .map_err(|e| {
                VaporaError::ValidationError(format!("Failed to execute Nickel: {}", e))
            })?;

        // Wait with timeout
        let timeout = tokio::time::Duration::from_millis(self.timeout_ms);
        let result = tokio::time::timeout(timeout, child.wait_with_output()).await;

        match result {
            Ok(Ok(output)) => Ok(output),
            Ok(Err(e)) => Err(VaporaError::ValidationError(format!(
                "Nickel command failed: {}",
                e
            ))),
            Err(_) => {
                // Timeout occurred - process is still running but we give up
                Err(VaporaError::ValidationError(format!(
                    "Nickel command timed out after {}ms",
                    self.timeout_ms
                )))
            }
        }
    }
}

impl Default for NickelCli {
    fn default() -> Self {
        Self::new()
    }
}

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

    #[tokio::test]
    async fn test_nickel_cli_creation() {
        let cli = NickelCli::new();
        assert_eq!(cli.nickel_path, PathBuf::from("nickel"));
        assert_eq!(cli.timeout_ms, 30_000);
    }

    #[tokio::test]
    async fn test_nickel_cli_with_path() {
        let custom_path = PathBuf::from("/usr/local/bin/nickel");
        let cli = NickelCli::with_path(custom_path.clone());
        assert_eq!(cli.nickel_path, custom_path);
    }

    #[tokio::test]
    async fn test_nickel_cli_with_timeout() {
        let cli = NickelCli::new().with_timeout(5_000);
        assert_eq!(cli.timeout_ms, 5_000);
    }

    #[tokio::test]
    #[ignore] // Requires Nickel CLI installed
    async fn test_nickel_is_available() {
        let cli = NickelCli::new();
        let available = cli.is_available().await;
        // This will pass if nickel is in PATH
        if available {
            println!("Nickel CLI is available");
        } else {
            println!("Nickel CLI is not available (install it to run this test)");
        }
    }

    #[tokio::test]
    #[ignore] // Requires Nickel CLI installed
    async fn test_nickel_version() {
        let cli = NickelCli::new();
        if cli.is_available().await {
            let version = cli.version().await.unwrap();
            println!("Nickel version: {}", version);
            assert!(!version.is_empty());
        }
    }
}
