use log::{debug, warn};
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};

pub mod cwd;
pub mod digest;
pub mod env;
pub mod exec;
pub mod hostname;
#[cfg(feature = "infer")]
pub mod infer_plugin;
pub mod keep_pid;
pub mod magic_file;
pub mod read_rate;
pub mod read_time;
pub mod shell;
pub mod shell_pid;
pub mod text;
#[cfg(feature = "tokens")]
pub mod tokens;
#[cfg(feature = "tree_magic_mini")]
pub mod tree_magic_mini;
pub mod user;

pub use digest::DigestMetaPlugin;
pub use exec::MetaPluginExec;
#[cfg(feature = "magic")]
pub use magic_file::MagicFileMetaPlugin;
// pub use text::TextMetaPlugin; // Removed duplicate
pub use cwd::CwdMetaPlugin;
pub use env::EnvMetaPlugin;
pub use hostname::HostnameMetaPlugin;
#[cfg(feature = "infer")]
pub use infer_plugin::InferMetaPlugin;
pub use keep_pid::KeepPidMetaPlugin;
pub use read_rate::ReadRateMetaPlugin;
pub use read_time::ReadTimeMetaPlugin;
pub use shell::ShellMetaPlugin;
pub use shell_pid::ShellPidMetaPlugin;
#[cfg(feature = "tree_magic_mini")]
pub use tree_magic_mini::TreeMagicMiniMetaPlugin;
pub use user::UserMetaPlugin;

#[cfg(not(feature = "magic"))]
pub use magic_file::FallbackMagicFileMetaPlugin as MagicFileMetaPlugin;

type PluginConstructor = fn(
    Option<HashMap<String, serde_yaml::Value>>,
    Option<HashMap<String, serde_yaml::Value>>,
) -> Box<dyn MetaPlugin>;

/// Represents metadata to be stored.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MetaData {
    /// The name of the metadata field.
    pub name: String,
    /// The value of the metadata field.
    pub value: String,
}

/// Response from meta plugin operations.
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct MetaPluginResponse {
    /// The generated metadata items.
    pub metadata: Vec<MetaData>,
    /// Indicates if the plugin has finished processing.
    pub is_finalized: bool,
}

/// Type alias for the save_meta callback shared by all plugins.
pub type SaveMetaFn = Arc<Mutex<dyn FnMut(&str, &str) + Send>>;

/// Creates a no-op save_meta for plugins not wired through MetaService.
pub fn noop_save_meta() -> SaveMetaFn {
    Arc::new(Mutex::new(|_: &str, _: &str| {}))
}

/// Base implementation for meta plugins to reduce boilerplate.
#[derive(Clone)]
pub struct BaseMetaPlugin {
    /// Output mappings for metadata.
    pub outputs: std::collections::HashMap<String, serde_yaml::Value>,
    /// Configuration options for the plugin.
    pub options: std::collections::HashMap<String, serde_yaml::Value>,
    /// Whether the plugin is finalized.
    pub is_finalized: bool,
    /// Callback to store metadata. Called directly by plugins.
    pub save_meta: SaveMetaFn,
}

impl std::fmt::Debug for BaseMetaPlugin {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("BaseMetaPlugin")
            .field("outputs", &self.outputs)
            .field("options", &self.options)
            .field("is_finalized", &self.is_finalized)
            .finish_non_exhaustive()
    }
}

impl Default for BaseMetaPlugin {
    fn default() -> Self {
        Self {
            outputs: HashMap::new(),
            options: HashMap::new(),
            is_finalized: false,
            save_meta: noop_save_meta(),
        }
    }
}

impl BaseMetaPlugin {
    /// Creates a new `BaseMetaPlugin`.
    ///
    /// # Returns
    ///
    /// A new instance of `BaseMetaPlugin`.
    pub fn new() -> Self {
        Self::default()
    }

    /// Returns a reference to the outputs mapping.
    pub fn outputs(&self) -> &std::collections::HashMap<String, serde_yaml::Value> {
        &self.outputs
    }

    /// Returns a mutable reference to the outputs mapping.
    pub fn outputs_mut(&mut self) -> &mut std::collections::HashMap<String, serde_yaml::Value> {
        &mut self.outputs
    }

    /// Returns a reference to the options mapping.
    pub fn options(&self) -> &std::collections::HashMap<String, serde_yaml::Value> {
        &self.options
    }

    /// Returns a mutable reference to the options mapping.
    pub fn options_mut(&mut self) -> &mut std::collections::HashMap<String, serde_yaml::Value> {
        &mut self.options
    }

    /// Sets the save_meta callback on the base plugin.
    pub fn set_save_meta(&mut self, save_meta: SaveMetaFn) {
        self.save_meta = save_meta;
    }

    /// Saves a metadata entry via the save_meta callback.
    pub fn save_meta(&self, name: &str, value: &str) {
        if let Ok(mut f) = self.save_meta.lock() {
            f(name, value);
        } else {
            warn!("META_PLUGIN: save_meta lock poisoned, dropping metadata: {name}={value}");
        }
    }

    /// Helper function to initialize plugin options and outputs.
    ///
    /// # Arguments
    ///
    /// * `default_outputs` - Slice of default output names.
    /// * `options` - Optional user-provided options.
    /// * `outputs` - Optional user-provided outputs.
    pub fn initialize_plugin(
        &mut self,
        default_outputs: &[&str],
        options: &Option<std::collections::HashMap<String, serde_yaml::Value>>,
        outputs: &Option<std::collections::HashMap<String, serde_yaml::Value>>,
    ) {
        // Set default outputs
        for output_name in default_outputs {
            self.outputs.insert(
                output_name.to_string(),
                serde_yaml::Value::String(output_name.to_string()),
            );
        }

        // Apply provided options and outputs
        if let Some(opts) = options {
            for (key, value) in opts {
                self.options.insert(key.clone(), value.clone());
            }
        }
        if let Some(outs) = outputs {
            for (key, value) in outs {
                self.outputs.insert(key.clone(), value.clone());
            }
        }
    }
}

impl MetaPlugin for BaseMetaPlugin {
    /// Returns the type of this meta plugin.
    ///
    /// # Returns
    ///
    /// `MetaPluginType::Text` (default for base).
    fn meta_type(&self) -> MetaPluginType {
        // This is a base implementation, so we need to return something
        // This might not be used, but we need to satisfy the trait
        MetaPluginType::Text
    }

    /// Returns a reference to the outputs mapping.
    ///
    /// # Returns
    ///
    /// A reference to the `HashMap` of outputs.
    fn outputs(&self) -> &std::collections::HashMap<String, serde_yaml::Value> {
        &self.outputs
    }

    /// Returns a mutable reference to the outputs mapping.
    ///
    /// # Returns
    ///
    /// A mutable reference to the `HashMap` of outputs.
    fn outputs_mut(
        &mut self,
    ) -> anyhow::Result<&mut std::collections::HashMap<String, serde_yaml::Value>> {
        Ok(&mut self.outputs)
    }

    /// Returns a reference to the options mapping.
    ///
    /// # Returns
    ///
    /// A reference to the `HashMap` of options.
    fn options(&self) -> &std::collections::HashMap<String, serde_yaml::Value> {
        &self.options
    }

    /// Returns a mutable reference to the options mapping.
    ///
    /// # Returns
    ///
    /// A mutable reference to the `HashMap` of options.
    fn options_mut(
        &mut self,
    ) -> anyhow::Result<&mut std::collections::HashMap<String, serde_yaml::Value>> {
        Ok(&mut self.options)
    }
}

#[derive(
    Debug,
    Eq,
    PartialEq,
    Clone,
    Hash,
    strum::EnumIter,
    strum::Display,
    strum::EnumString,
    Serialize,
    Deserialize,
)]
#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
pub enum MetaPluginType {
    MagicFile,
    Cwd,
    Text,
    User,
    Shell,
    ShellPid,
    KeepPid,
    Digest,
    ReadTime,
    ReadRate,
    Hostname,
    Exec,
    Env,
    Tokens,
    TreeMagicMini,
    Infer,
}

/// Central function to handle metadata output with name mapping.
///
/// # Arguments
///
/// * `internal_name` - The internal name of the metadata.
/// * `value` - The value to process.
/// * `outputs` - The outputs mapping.
///
/// # Returns
///
/// An optional `MetaData` if the output is enabled, `None` if disabled.
pub fn process_metadata_outputs(
    internal_name: &str,
    value: serde_yaml::Value,
    outputs: &std::collections::HashMap<String, serde_yaml::Value>,
) -> Option<MetaData> {
    // Check if this output is disabled
    if let Some(mapping) = outputs.get(internal_name) {
        // Check for null to disable the output
        if mapping.is_null() {
            debug!("META: Skipping disabled output (null): {internal_name}");
            return None;
        }
        // Check for boolean false to disable the output
        if let Some(false_val) = mapping.as_bool()
            && !false_val
        {
            debug!("META: Skipping disabled output: {internal_name}");
            return None;
        }
        if let Some(custom_name) = mapping.as_str() {
            // Convert the value to a string representation
            let value_str = match &value {
                serde_yaml::Value::Null => "null".to_string(),
                serde_yaml::Value::Bool(b) => b.to_string(),
                serde_yaml::Value::Number(n) => n.to_string(),
                serde_yaml::Value::String(s) => s.clone(),
                serde_yaml::Value::Sequence(_) => {
                    serde_yaml::to_string(&value).unwrap_or_else(|_| "".to_string())
                }
                serde_yaml::Value::Mapping(_) => {
                    serde_yaml::to_string(&value).unwrap_or_else(|_| "".to_string())
                }
                serde_yaml::Value::Tagged(_) => {
                    serde_yaml::to_string(&value).unwrap_or_else(|_| "".to_string())
                }
            };
            debug!(
                "META: Processing metadata: internal_name={internal_name}, custom_name={custom_name}, value={value_str}"
            );
            return Some(MetaData {
                name: custom_name.to_string(),
                value: value_str,
            });
        }
    }

    // Convert the value to a string representation
    let value_str = match &value {
        serde_yaml::Value::Null => "null".to_string(),
        serde_yaml::Value::Bool(b) => b.to_string(),
        serde_yaml::Value::Number(n) => n.to_string(),
        serde_yaml::Value::String(s) => s.clone(),
        serde_yaml::Value::Sequence(_) => {
            serde_yaml::to_string(&value).unwrap_or_else(|_| "".to_string())
        }
        serde_yaml::Value::Mapping(_) => {
            serde_yaml::to_string(&value).unwrap_or_else(|_| "".to_string())
        }
        serde_yaml::Value::Tagged(_) => {
            serde_yaml::to_string(&value).unwrap_or_else(|_| "".to_string())
        }
    };

    // Default: use internal name as output name
    debug!("META: Processing metadata: name={internal_name}, value={value_str}");
    Some(MetaData {
        name: internal_name.to_string(),
        value: value_str,
    })
}

pub trait MetaPlugin: Send
where
    Self: 'static,
{
    /// Returns the type of this meta plugin.
    ///
    /// # Returns
    ///
    /// The `MetaPluginType` enum variant for this plugin.
    fn meta_type(&self) -> MetaPluginType;

    /// Checks if the plugin is supported on the current system.
    ///
    /// # Returns
    ///
    /// `true` if supported, `false` otherwise.
    fn is_supported(&self) -> bool {
        true
    }

    /// Checks if the plugin is internal (built-in).
    ///
    /// # Returns
    ///
    /// `true` if internal, `false` otherwise.
    fn is_internal(&self) -> bool {
        true
    }

    /// Checks if the plugin is already finalized.
    ///
    /// # Returns
    ///
    /// `true` if finalized, `false` otherwise.
    fn is_finalized(&self) -> bool {
        false
    }

    /// Sets the finalized state (only for plugins that can track this).
    ///
    /// # Arguments
    ///
    /// * `_finalized` - The new finalized state (unused in default).
    fn set_finalized(&mut self, _finalized: bool) {}

    /// Updates the meta plugin with new data.
    ///
    /// # Arguments
    ///
    /// * `_data` - The data chunk to process (unused in default).
    ///
    /// # Returns
    ///
    /// A `MetaPluginResponse` with empty metadata and `is_finalized` set to `false`.
    fn update(&mut self, _data: &[u8]) -> MetaPluginResponse {
        // Default implementation does nothing
        MetaPluginResponse {
            metadata: Vec::new(),
            is_finalized: false,
        }
    }

    /// Finalizes the plugin.
    ///
    /// # Returns
    ///
    /// A `MetaPluginResponse` with empty metadata and `is_finalized` set to `true`.
    fn finalize(&mut self) -> MetaPluginResponse {
        // Default implementation does nothing
        MetaPluginResponse {
            metadata: Vec::new(),
            is_finalized: true,
        }
    }

    /// Gets program information for display in status.
    ///
    /// # Returns
    ///
    /// An optional tuple of program name and arguments, or `None`.
    fn program_info(&self) -> Option<(&str, Vec<&str>)> {
        None
    }

    /// Initializes the plugin.
    ///
    /// # Returns
    ///
    /// A `MetaPluginResponse` with empty metadata and `is_finalized` set to `false`.
    fn initialize(&mut self) -> MetaPluginResponse {
        // Default implementation does nothing
        MetaPluginResponse {
            metadata: Vec::new(),
            is_finalized: false,
        }
    }

    /// Returns a reference to the outputs mapping.
    ///
    /// # Returns
    ///
    /// An empty `HashMap` (default implementation).
    fn outputs(&self) -> &std::collections::HashMap<String, serde_yaml::Value> {
        use once_cell::sync::Lazy;
        static EMPTY: Lazy<std::collections::HashMap<String, serde_yaml::Value>> =
            Lazy::new(std::collections::HashMap::new);
        &EMPTY
    }

    /// Returns a mutable reference to the outputs mapping.
    ///
    /// # Returns
    ///
    /// A mutable reference to the outputs `HashMap`.
    ///
    /// # Errors
    ///
    /// Returns an error if the plugin does not support mutable outputs.
    fn outputs_mut(
        &mut self,
    ) -> anyhow::Result<&mut std::collections::HashMap<String, serde_yaml::Value>> {
        anyhow::bail!("outputs_mut() not supported by this plugin")
    }

    /// Returns a reference to the options mapping.
    ///
    /// # Returns
    ///
    /// An empty `HashMap` (default implementation).
    fn options(&self) -> &std::collections::HashMap<String, serde_yaml::Value> {
        use once_cell::sync::Lazy;
        static EMPTY: Lazy<std::collections::HashMap<String, serde_yaml::Value>> =
            Lazy::new(std::collections::HashMap::new);
        &EMPTY
    }

    /// Returns a mutable reference to the options mapping.
    ///
    /// # Returns
    ///
    /// A mutable reference to the options `HashMap`.
    ///
    /// # Errors
    ///
    /// Returns an error if the plugin does not support mutable options.
    fn options_mut(
        &mut self,
    ) -> anyhow::Result<&mut std::collections::HashMap<String, serde_yaml::Value>> {
        anyhow::bail!("options_mut() not supported by this plugin")
    }

    /// Gets the default output names this plugin can produce.
    ///
    /// # Returns
    ///
    /// A vector containing the meta type as a string (default).
    fn default_outputs(&self) -> Vec<String> {
        // Default implementation returns the meta type as a string
        vec![self.meta_type().to_string()]
    }

    /// Returns a description of this plugin for display in config templates.
    ///
    /// # Returns
    ///
    /// A description string (empty by default).
    fn description(&self) -> &str {
        ""
    }

    /// Returns true if this plugin can execute concurrently with other
    /// parallel-safe plugins.
    ///
    /// Plugins that do significant per-chunk work (hashing, tokenization,
    /// piping to child processes) should return true. The MetaService will
    /// run all parallel-safe plugins in separate threads per phase, then
    /// process results sequentially.
    fn parallel_safe(&self) -> bool {
        false
    }

    /// Builds the schema for this plugin from its options and outputs.
    ///
    /// Default implementation infers option types from YAML values and
    /// collects enabled outputs.
    ///
    /// # Returns
    ///
    /// A `PluginSchema` describing this plugin's configuration.
    fn schema(&self) -> crate::common::schema::PluginSchema {
        use crate::common::schema::{OptionSchema, OptionType, OutputSchema, PluginSchema};

        let options: Vec<OptionSchema> = self
            .options()
            .iter()
            .map(|(key, value)| {
                let option_type = OptionType::from_yaml_value(value);
                let (default, required) = if value.is_null() {
                    (None, true)
                } else {
                    (Some(value.clone()), false)
                };
                OptionSchema {
                    name: key.clone(),
                    option_type,
                    default,
                    required,
                }
            })
            .collect();

        let mut outputs: Vec<OutputSchema> = Vec::new();
        for (key, value) in self.outputs() {
            if !value.is_null() {
                outputs.push(OutputSchema {
                    name: key.clone(),
                    description: key.clone(),
                });
            }
        }
        if outputs.is_empty() {
            for output_name in self.default_outputs() {
                outputs.push(OutputSchema {
                    name: output_name.clone(),
                    description: output_name,
                });
            }
        }

        PluginSchema {
            name: self.meta_type().to_string(),
            description: self.description().to_string(),
            options,
            outputs,
        }
    }

    /// Method to downcast to concrete type (for checking finalization state).
    ///
    /// # Returns
    ///
    /// A mutable reference to `self` as `dyn Any`.
    fn as_any_mut(&mut self) -> &mut dyn std::any::Any
    where
        Self: Sized,
    {
        self
    }

    /// Sets the save_meta callback for this plugin.
    ///
    /// Called by MetaService to wire the plugin to the metadata storage.
    fn set_save_meta(&mut self, _save_meta: SaveMetaFn) {}

    /// Saves a metadata entry via the save_meta callback.
    ///
    /// Plugins call this during initialize/update/finalize to persist metadata.
    fn save_meta(&self, _name: &str, _value: &str) {}
}

/// Global registry for meta plugins.
static META_PLUGIN_REGISTRY: Lazy<Mutex<HashMap<MetaPluginType, PluginConstructor>>> =
    Lazy::new(|| Mutex::new(HashMap::new()));

/// Register a meta plugin with the global registry.
///
/// # Arguments
///
/// * `meta_plugin_type` - The type of the meta plugin to register.
/// * `constructor` - The constructor function for creating plugin instances.
pub fn register_meta_plugin(
    meta_plugin_type: MetaPluginType,
    constructor: PluginConstructor,
) -> anyhow::Result<()> {
    META_PLUGIN_REGISTRY
        .lock()
        .map_err(|e| anyhow::anyhow!("plugin registry poisoned: {e}"))?
        .insert(meta_plugin_type, constructor);
    Ok(())
}

pub fn get_meta_plugin(
    meta_plugin_type: MetaPluginType,
    options: Option<std::collections::HashMap<String, serde_yaml::Value>>,
    outputs: Option<std::collections::HashMap<String, serde_yaml::Value>>,
) -> anyhow::Result<Box<dyn MetaPlugin>> {
    get_meta_plugin_with_save(meta_plugin_type, options, outputs, None)
}

/// Creates a meta plugin instance with an optional save_meta callback.
///
/// If `save_meta` is provided, it is wired to the plugin so it can
/// store metadata directly during initialize/update/finalize.
pub fn get_meta_plugin_with_save(
    meta_plugin_type: MetaPluginType,
    options: Option<std::collections::HashMap<String, serde_yaml::Value>>,
    outputs: Option<std::collections::HashMap<String, serde_yaml::Value>>,
    save_meta: Option<SaveMetaFn>,
) -> anyhow::Result<Box<dyn MetaPlugin>> {
    let registry = META_PLUGIN_REGISTRY
        .lock()
        .map_err(|e| anyhow::anyhow!("plugin registry poisoned: {e}"))?;
    if let Some(constructor) = registry.get(&meta_plugin_type) {
        let mut plugin = constructor(options, outputs);
        if let Some(sm) = save_meta {
            plugin.set_save_meta(sm);
        }
        return Ok(plugin);
    }

    anyhow::bail!("Meta plugin {meta_plugin_type:?} not registered")
}
