use log::*;
use std::io::{self, Write};
use std::process::{Child, Command, Stdio};
use which::which;

use crate::meta_plugin::{BaseMetaPlugin, MetaPlugin, MetaPluginResponse, MetaPluginType};

/// External program execution meta plugin.
///
/// This plugin executes a specified external command during item save operations,
/// capturing its output as metadata. It supports piping input data to the command's stdin
/// and processing stdout. Useful for dynamic metadata generation via shell commands.
///
/// # Examples
///
/// Configured via options like `command: "date"`, the plugin runs `date` and captures output as metadata.
pub struct MetaPluginExec {
    pub program: String,
    pub args: Vec<String>,
    pub supported: bool,
    pub split_whitespace: bool,
    process: Option<Child>,
    writer: Option<Box<dyn Write + Send>>,
    result: Option<String>,
    base: BaseMetaPlugin,
}

// Manual Debug implementation because Box<dyn Write> doesn't implement Debug
/// Custom Debug implementation for MetaPluginExec.
///
/// Obfuscates the writer field since Box<dyn Write> does not implement Debug.
impl std::fmt::Debug for MetaPluginExec {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("MetaPluginExec")
            .field("program", &self.program)
            .field("args", &self.args)
            .field("supported", &self.supported)
            .field("split_whitespace", &self.split_whitespace)
            .field("process", &self.process)
            .field("writer", &self.writer.as_ref().map(|_| "Box<dyn Write>"))
            .field("result", &self.result)
            .field("base", &self.base)
            .finish()
    }
}

impl MetaPluginExec {
    /// Creates a new MetaPluginExec instance.
    ///
    /// Validates the program availability using `which` and initializes outputs and options.
    /// The meta_name determines the default output key for captured command output.
    ///
    /// # Arguments
    ///
    /// * `program` - The executable name or path to run.
    /// * `args` - Slice of arguments to pass to the program.
    /// * `meta_name` - Name for the metadata output key.
    /// * `split_whitespace` - If true, takes the first whitespace-separated word from output; otherwise, trims full output.
    /// * `_options` - Optional configuration options (currently unused beyond passing through).
    /// * `outputs` - Optional output mappings to override defaults.
    ///
    /// # Returns
    ///
    /// * `MetaPluginExec` - New plugin instance, with `supported` set based on program availability.
    ///
    /// # Examples
    ///
    /// ```
    /// # use keep::meta_plugin::MetaPluginExec;
    /// let plugin = MetaPluginExec::new("date", &[], "date_output".to_string(), false, None, None);
    /// ```
    pub fn new(
        program: &str,
        args: &[String],
        meta_name: String,
        split_whitespace: bool,
        _options: Option<std::collections::HashMap<String, serde_yaml::Value>>,
        outputs: Option<std::collections::HashMap<String, serde_yaml::Value>>,
    ) -> MetaPluginExec {
        let supported = which(program).is_ok();

        let mut base = BaseMetaPlugin::new();

        // Set default output
        let default_outputs = &[meta_name.as_str()];
        base.initialize_plugin(default_outputs, &_options, &outputs);

        MetaPluginExec {
            program: program.to_string(),
            args: args.to_vec(),
            supported,
            split_whitespace,
            process: None,
            writer: None,
            result: None,
            base,
        }
    }

    /// Starts the external process if not already running.
    ///
    /// Spawns the command with piped stdin/stdout and stores the child process and writer.
    ///
    /// # Returns
    ///
    /// * `MetaPluginResponse` - Empty response, initializes the process.
    fn start_process(&mut self) -> MetaPluginResponse {
        if self.process.is_some() {
            return MetaPluginResponse {
                metadata: Vec::new(),
                is_finalized: false,
            };
        }

        if !self.supported {
            debug!(
                "META: Exec plugin: program '{}' not supported",
                self.program
            );
            return MetaPluginResponse {
                metadata: Vec::new(),
                is_finalized: true,
            };
        }

        let mut cmd = Command::new(&self.program);
        cmd.args(&self.args)
            .stdin(Stdio::piped())
            .stdout(Stdio::piped())
            .stderr(Stdio::piped());

        match cmd.spawn() {
            Ok(mut child) => {
                let stdin = child.stdin.take().unwrap();
                self.writer = Some(Box::new(stdin));
                self.process = Some(child);
                debug!("META: Exec plugin: started process for '{}'", self.program);
                MetaPluginResponse {
                    metadata: Vec::new(),
                    is_finalized: false,
                }
            }
            Err(e) => {
                error!(
                    "META: Exec plugin: failed to start '{}': {}",
                    self.program, e
                );
                MetaPluginResponse {
                    metadata: Vec::new(),
                    is_finalized: true,
                }
            }
        }
    }
}

impl MetaPlugin for MetaPluginExec {
    fn meta_type(&self) -> MetaPluginType {
        MetaPluginType::Exec
    }

    fn is_supported(&self) -> bool {
        self.supported
    }

    fn is_internal(&self) -> bool {
        false
    }

    fn set_save_meta(&mut self, save_meta: crate::meta_plugin::SaveMetaFn) {
        self.base.set_save_meta(save_meta);
    }

    fn save_meta(&self, name: &str, value: &str) {
        self.base.save_meta(name, value);
    }

    fn initialize(&mut self) -> MetaPluginResponse {
        self.start_process()
    }

    fn update(&mut self, data: &[u8]) -> MetaPluginResponse {
        if let Some(writer) = self.writer.as_mut()
            && let Err(e) = writer.write_all(data)
        {
            error!("META: Exec plugin: failed to write to stdin: {e}");
        }
        MetaPluginResponse {
            metadata: Vec::new(),
            is_finalized: false,
        }
    }

    fn finalize(&mut self) -> MetaPluginResponse {
        let mut metadata = Vec::new();

        // Close stdin if writer exists
        drop(self.writer.take());

        // Wait for process to complete and capture output
        if let Some(child) = self.process.take() {
            match child.wait_with_output() {
                Ok(output) => {
                    if output.status.success() {
                        let stdout = String::from_utf8_lossy(&output.stdout);
                        let result = if self.split_whitespace {
                            stdout
                                .split_whitespace()
                                .next()
                                .unwrap_or(&stdout)
                                .to_string()
                        } else {
                            stdout.trim().to_string()
                        };

                        self.result = Some(result.clone());

                        if let Some(meta_data) = crate::meta_plugin::process_metadata_outputs(
                            self.base
                                .outputs()
                                .keys()
                                .next()
                                .unwrap_or(&"exec".to_string()),
                            serde_yaml::Value::String(result),
                            self.base.outputs(),
                        ) {
                            metadata.push(meta_data);
                        }
                    } else {
                        let stderr = String::from_utf8_lossy(&output.stderr);
                        error!("META: Exec plugin: command failed: {stderr}");
                    }
                }
                Err(e) => {
                    error!("META: Exec plugin: failed to wait on process: {e}");
                }
            }
        }

        MetaPluginResponse {
            metadata,
            is_finalized: true,
        }
    }

    fn program_info(&self) -> Option<(&str, Vec<&str>)> {
        let args_str: Vec<&str> = self.args.iter().map(|s| s.as_str()).collect();
        Some((&self.program, args_str))
    }

    fn outputs(&self) -> &std::collections::HashMap<String, serde_yaml::Value> {
        &self.base.outputs
    }

    fn outputs_mut(
        &mut self,
    ) -> anyhow::Result<&mut std::collections::HashMap<String, serde_yaml::Value>> {
        Ok(&mut self.base.outputs)
    }

    fn options(&self) -> &std::collections::HashMap<String, serde_yaml::Value> {
        &self.base.options
    }

    fn options_mut(
        &mut self,
    ) -> anyhow::Result<&mut std::collections::HashMap<String, serde_yaml::Value>> {
        Ok(&mut self.base.options)
    }

    fn default_outputs(&self) -> Vec<String> {
        vec!["exec".to_string()]
    }

    fn parallel_safe(&self) -> bool {
        true
    }
}

use crate::meta_plugin::register_meta_plugin;

// Register the plugin at module initialization time
#[ctor::ctor]
fn register_exec_plugin() {
    register_meta_plugin(MetaPluginType::Exec, |options, outputs| {
        // Parse command from options for registration
        let mut program_name = String::new();
        let mut args = Vec::new();
        let mut meta_name = "exec".to_string();
        let mut split_whitespace = false;

        if let Some(opts) = &options {
            if let Some(command_value) = opts.get("command")
                && let Some(command_str) = command_value.as_str()
            {
                let parts: Vec<&str> = command_str.split_whitespace().collect();
                if !parts.is_empty() {
                    program_name = parts[0].to_string();
                    args = parts[1..].iter().map(|s| s.to_string()).collect();
                }
            }
            if let Some(split_value) = opts.get("split_whitespace")
                && let Some(split_bool) = split_value.as_bool()
            {
                split_whitespace = split_bool;
            }
            if let Some(name_value) = opts.get("name")
                && let Some(name_str) = name_value.as_str()
            {
                meta_name = name_str.to_string();
            }
        }

        Box::new(MetaPluginExec::new(
            &program_name,
            &args,
            meta_name,
            split_whitespace,
            options,
            outputs,
        ))
    })
    .expect("Failed to register ExecMetaPlugin");
}
