use kcl_lang::{API, ExecProgramArgs, ExternalPkg};
use nu_plugin::{EngineInterface, EvaluatedCall, SimplePluginCommand};
use nu_plugin::{MsgPackSerializer, Plugin, PluginCommand, serve_plugin};
use nu_protocol::IntoPipelineData;
use nu_protocol::{
    Category, Example, LabeledError, PipelineData, Signature, SyntaxShape, Type, Value,
};
use serde::Deserialize;
use std::collections::HashMap;
use std::path::{Path, PathBuf};

/// Nushell plugin for executing KCL files and inline KCL code.
///
/// This plugin integrates [KCL](https://kcl-lang.io/) (Kusion Configuration Language) with [Nushell](https://www.nushell.sh/), allowing you to execute KCL files or inline KCL code directly from your Nushell pipeline.
///
/// # Provided Commands
///
/// - `kcl-exec`: Execute one or more KCL files and return the result as YAML. Accepts an array of file paths and an optional `--work_dir` flag to specify the working directory.
/// - `kcl-run`: Execute inline KCL code from the pipeline and return the result as YAML.
///
/// # Usage
///
/// ## kcl-exec
///
/// Run multiple KCL files in the current directory:
///
/// ```shell
/// kcl-exec [settings.k defs/aws_defaults.k]
/// ```
///
/// Run multiple KCL files from any directory, specifying the project root:
///
/// ```shell
/// kcl-exec [settings.k defs/aws_defaults.k] --work_dir /path/to/project
/// ```
///
/// Run a single KCL file:
///
/// ```shell
/// kcl-exec [settings.k] --work_dir /path/to/project
/// ```
///
/// ## kcl-run
///
/// Run inline KCL code:
///
/// ```shell
/// 'a = 1' | kcl-run
/// ```
///
/// Run a KCL file from the pipeline:
///
/// ```shell
/// open settings.k | kcl-run
/// ```
///
/// # Limitations
///
/// - The `kcl-exec` command requires the specified files to exist and be accessible.
/// - The `kcl-run` command expects a string of KCL code from the pipeline input.
/// - Example-based tests that require actual files or pipeline input may not run in all test environments.
///
/// # Resources
///
/// - [KCL Language Documentation](https://kcl-lang.io/docs/)
/// - [Nushell Plugin Development](https://www.nushell.sh/book/plugins.html)
pub struct KclPlugin;

impl Plugin for KclPlugin {
    fn version(&self) -> String {
        // This automatically uses the version of your package from Cargo.toml as the plugin version
        // sent to Nushell
        env!("CARGO_PKG_VERSION").into()
    }

    fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> {
        vec![
            // Commands should be added here
            Box::new(KclExec),
            Box::new(KclValidate),
        ]
    }
}

/// Command to execute KCL files from a file path.
///
/// This command takes a path to a KCL file and executes it, returning the result as YAML.
///
/// # Examples
///
/// ```shell
/// kcl-exec ./src/myfile.k
/// ```
///
/// # Arguments
///
/// - `file`: Path to the KCL file to execute (required).
/// - `work_dir`: Optional working directory for KCL execution.
///
/// # Limitations
///
/// The file must exist and be readable by the plugin process.
/// - If `--work_dir` is not provided, the plugin will automatically search for `kcl.mod` upwards from the first file in the files array and use its directory as the working directory.
pub struct KclExec;

impl SimplePluginCommand for KclExec {
    type Plugin = KclPlugin;

    fn name(&self) -> &str {
        "kcl-exec"
    }

    fn signature(&self) -> Signature {
        Signature::build(PluginCommand::name(self))
            .input_output_type(Type::Any, Type::String)
            .required(
                "files",
                SyntaxShape::List(Box::new(SyntaxShape::Filepath)),
                "KCL files to execute (array)",
            )
            .named(
                "work_dir",
                SyntaxShape::Directory,
                "Work directory",
                Some('w'),
            )
            .category(Category::Experimental)
    }

    fn description(&self) -> &str {
        "Exec KCL files and return result in YAML"
    }

    fn examples(&self) -> Vec<Example> {
        let examples: Vec<Example> = vec![Example {
            example: "kcl-exec [settings.k defs/aws_defaults.k] --work_dir .",
            description: "Execute multiple KCL files in the current directory",
            result: None,
        }];
        examples
    }

    fn run(
        &self,
        _plugin: &KclPlugin,
        _engine: &EngineInterface,
        call: &EvaluatedCall,
        _input: &Value,
    ) -> Result<Value, LabeledError> {
        let files: Vec<String> = call.req(0)?;
        let work_dir: Option<String> = call.get_flag("work_dir")?;
        match exec_kcl_file(&files, work_dir.as_deref()) {
            Ok(yaml) => Ok(Value::string(yaml, call.head)),
            Err(e) => Err(LabeledError::new("KCL code execution error").with_label(e, call.head)),
        }
    }
}

/// Command to execute inline KCL code from the Nushell pipeline.
///
/// This command takes a string of KCL code from the pipeline and executes it, returning the result as YAML.
///
/// # Example
///
/// ```shell
/// open myfile.k | kcl-run
/// ```
///
/// # Arguments
///
/// - `filename`: Optional virtual filename for the KCL code (default: `main.k`).
///
/// # Limitations
///
/// The input must be a string containing valid KCL code.
struct KclValidate;

impl PluginCommand for KclValidate {
    type Plugin = KclPlugin;

    fn name(&self) -> &str {
        "kcl-run"
    }
    fn description(&self) -> &str {
        "Exec KCL inline code from pipeline"
    }
    fn signature(&self) -> Signature {
        Signature::build(PluginCommand::name(self))
            .input_output_type(Type::String, Type::String)
            .optional(
                "filename",
                SyntaxShape::String,
                "Nombre virtual del archivo",
            )
            .category(Category::Experimental)
    }

    fn run(
        &self,
        _plugin: &KclPlugin,
        _engine: &EngineInterface,
        call: &EvaluatedCall,
        input: PipelineData,
    ) -> Result<PipelineData, LabeledError> {
        let filename = call
            .opt::<String>(0)?
            .unwrap_or_else(|| "main.k".to_string());
        let value = input.into_value(call.head);
        let code = match value {
            Ok(Value::String { val, .. }) => val,
            Ok(_) => {
                return Err(LabeledError::new("KCL code expected as string")
                    .with_label("Use a string as KCL code", call.head));
            }
            Err(e) => {
                return Err(LabeledError::new("Failed to extract input value")
                    .with_label(e.to_string(), call.head));
            }
        };
        match exec_kcl_inline(&code, &filename) {
            Ok(yaml) => Ok(Value::string(yaml, call.head).into_pipeline_data()),
            Err(e) => Err(LabeledError::new("KCL code execution error").with_label(e, call.head)),
        }
    }

    fn examples(&self) -> Vec<Example> {
        vec![Example {
            example: "'a = 1' | kcl-run",
            description: "Run inline KCL code from the pipeline",
            result: None,
        }]
    }
}

fn main() {
    serve_plugin(&KclPlugin, MsgPackSerializer);
}

fn exec_kcl_file(file_paths: &[String], work_dir: Option<&str>) -> Result<String, String> {
    let work_dir = match work_dir {
        Some(dir) => dir.to_string(),
        None => {
            // Try to find kcl.mod upwards from the first file
            if let Some(first_file) = file_paths.get(0) {
                match find_kcl_mod_dir(first_file) {
                    Some(dir) => dir,
                    None => return Err("No kcl.mod found in parent directories of the first file, and --work_dir not provided".to_string()),
                }
            } else {
                return Err("No files provided to execute".to_string());
            }
        }
    };
    let external_pkgs = resolve_dependencies_toml(&work_dir)?;
    // Debug: mostrar dependencias resueltas
    for pkg in &external_pkgs {
        println!("Dependencies: {} -> {}", pkg.pkg_name, pkg.pkg_path);
        println!("Existe: {}", Path::new(&pkg.pkg_path).exists());
    }
    let api = API::default();
    let args = ExecProgramArgs {
        k_filename_list: file_paths.to_vec(),
        work_dir,
        ..Default::default()
    };
    match api.exec_program(&args) {
        Ok(result) => Ok(result.yaml_result),
        Err(e) => Err(e.to_string()),
    }
}

/// Search upwards from the given file for a kcl.mod file and return its directory as a String.
fn find_kcl_mod_dir<P: AsRef<Path>>(start_file: P) -> Option<String> {
    let mut dir = PathBuf::from(start_file.as_ref()).parent()?.to_path_buf();
    loop {
        let kcl_mod = dir.join("kcl.mod");
        if kcl_mod.exists() {
            return Some(dir.to_string_lossy().to_string());
        }
        if !dir.pop() {
            break;
        }
    }
    None
}

fn exec_kcl_inline(code: &str, filename: &str) -> Result<String, String> {
    let api = API::default();
    let args = ExecProgramArgs {
        k_filename_list: vec![filename.to_string()],
        k_code_list: vec![code.to_string()],
        ..Default::default()
    };
    match api.exec_program(&args) {
        Ok(result) => Ok(result.yaml_result),
        Err(e) => Err(e.to_string()),
    }
}

#[allow(dead_code)]
#[derive(Deserialize)]
struct KclMod {
    package: Package,
    dependencies: Option<HashMap<String, Dependency>>,
}

#[allow(dead_code)]
#[derive(Deserialize)]
struct Package {
    name: String,
    edition: String,
    version: String,
}

#[allow(dead_code)]
#[derive(Deserialize)]
struct Dependency {
    path: Option<String>,
    version: String,
}

fn resolve_dependencies_toml(work_dir: &str) -> Result<Vec<ExternalPkg>, String> {
    let kcl_mod_path = Path::new(work_dir).join("kcl.mod");

    if !kcl_mod_path.exists() {
        return Ok(vec![]);
    }

    let content = std::fs::read_to_string(&kcl_mod_path)
        .map_err(|e| format!("Error reading kcl.mod: {}", e))?;

    let kcl_mod: KclMod =
        toml::from_str(&content).map_err(|e| format!("Error parsing kcl.mod: {}", e))?;

    let mut external_pkgs = Vec::new();

    if let Some(dependencies) = kcl_mod.dependencies {
        for (pkg_name, dep) in dependencies {
            if let Some(relative_path) = dep.path {
                let absolute_path = Path::new(work_dir)
                    .join(relative_path)
                    .canonicalize()
                    .map_err(|e| format!("Error resolving path for {}: {}", pkg_name, e))?
                    .to_string_lossy()
                    .to_string();

                external_pkgs.push(ExternalPkg {
                    pkg_name,
                    pkg_path: absolute_path,
                });
            }
        }
    }
    dbg!(&external_pkgs);

    Ok(external_pkgs)
}

/// Test all examples for the `kcl-exec` command.
///
/// This test runs the examples provided in the `examples` method of `KclExec`.
/// Note: Examples that require actual files may not pass unless the files exist.
#[test]
fn test_examples() -> Result<(), nu_protocol::ShellError> {
    use nu_plugin_test_support::PluginTest;

    PluginTest::new("kcl", KclPlugin.into())?.test_command_examples(&KclExec)
}
/// Test all examples for the `kcl-run` command.
///
/// This test runs the examples provided in the `examples` method of `KclValidate`.
/// Note: Examples that require pipeline input may not be fully testable in all environments.
#[test]
fn test_kcl_validate_examples() -> Result<(), nu_protocol::ShellError> {
    use nu_plugin_test_support::PluginTest;

    PluginTest::new("kcl", KclPlugin.into())?.test_command_examples(&KclValidate)
}

/// Entry point for the KCL Nushell plugin.
///
/// This function starts the plugin and serves it to Nushell using message pack serialization.

#[cfg(test)]
mod unit_tests {
    use super::*;
    use std::fs;
    use std::io::Write;
    extern crate tempfile;
    use tempfile::tempdir;

    #[test]
    fn test_exec_kcl_file_nonexistent() {
        let result = exec_kcl_file(&vec!["/tmp/does_not_exist.k".to_string()], None);
        assert!(result.is_err());
    }

    #[test]
    fn test_exec_kcl_inline_invalid_code() {
        let result = exec_kcl_inline("not valid kcl", "main.k");
        assert!(result.is_err());
    }

    #[test]
    fn test_resolve_dependencies_toml_success() {
        let dir = tempdir().unwrap();
        let dep_dir = dir.path().join("dep");
        fs::create_dir_all(&dep_dir).unwrap();
        let kcl_mod_content = r#"
[package]
name = "testpkg"
edition = "v0.9.0"
version = "0.1.0"

[dependencies]
foo = { path = "dep" , version = "0.1.0" }
"#;
        let kcl_mod_path = dir.path().join("kcl.mod");
        let mut file = fs::File::create(&kcl_mod_path).unwrap();
        file.write_all(kcl_mod_content.as_bytes()).unwrap();
        let pkgs = resolve_dependencies_toml(dir.path().to_str().unwrap()).unwrap();
        assert_eq!(pkgs.len(), 1);
        assert_eq!(pkgs[0].pkg_name, "foo");
        assert!(pkgs[0].pkg_path.ends_with("dep"));
    }

    #[test]
    fn test_resolve_dependencies_toml_no_kcl_mod() {
        let dir = tempdir().unwrap();
        let pkgs = resolve_dependencies_toml(dir.path().to_str().unwrap()).unwrap();
        assert!(pkgs.is_empty());
    }
}
