use anyhow::Result;
use std::collections::HashMap;
use std::sync::Arc;

use crate::lifecycle::BuildContext;

/// Configuration key-value pairs passed to a plugin goal at execution time.
pub type PluginConfig = HashMap<String, String>;

/// Trait representing a single build plugin goal, analogous to a Maven Mojo.
///
/// Implementors provide a human-readable name and an `execute` method that
/// receives the current [`BuildContext`] and a [`PluginConfig`].
pub trait PluginGoal: Send + Sync {
    /// Human-readable name of this goal (e.g. `"echo"`).
    fn name(&self) -> &str;

    /// Execute the goal.
    ///
    /// # Errors
    ///
    /// Returns an error if execution fails.
    fn execute(&self, context: &BuildContext, config: &PluginConfig) -> Result<()>;
}

/// A binding that associates a plugin goal with a lifecycle phase and optional
/// configuration overrides.
pub struct PluginBinding {
    /// The lifecycle phase name this binding targets (e.g. `"compile"`).
    pub phase: String,
    /// The registry key identifying the goal (e.g. `"com.example:my-plugin:echo"`).
    pub goal_key: String,
    /// Configuration entries for this specific execution, shared across goals
    /// in the same execution to avoid redundant cloning.
    pub config: Arc<PluginConfig>,
}

/// Registry of available plugin goals and their phase bindings.
///
/// The registry is created empty; callers are responsible for registering any
/// built-in or custom goals and adding bindings (for example, based on POM
/// `<plugins>` declarations) that connect goals to lifecycle phases.
pub struct PluginRegistry {
    goals: HashMap<String, Box<dyn PluginGoal>>,
    bindings: Vec<PluginBinding>,
}

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

impl PluginRegistry {
    /// Create an empty registry.
    #[must_use]
    pub fn new() -> Self {
        Self {
            goals: HashMap::new(),
            bindings: Vec::new(),
        }
    }

    /// Register a goal implementation under the given key.
    ///
    /// The key should follow Maven coordinate style:
    /// `"groupId:artifactId:goal"`.
    pub fn register_goal(&mut self, key: String, goal: Box<dyn PluginGoal>) {
        self.goals.insert(key, goal);
    }

    /// Bind a registered goal to a lifecycle phase with the given configuration.
    pub fn add_binding(&mut self, phase: String, goal_key: String, config: Arc<PluginConfig>) {
        self.bindings.push(PluginBinding {
            phase,
            goal_key,
            config,
        });
    }

    /// Return an iterator over bindings that target the given phase name.
    ///
    /// Prefer this over [`bindings_for_phase`](Self::bindings_for_phase) when
    /// you only need to iterate, as it avoids allocating a `Vec`.
    pub fn bindings_for_phase_iter<'a>(
        &'a self,
        phase: &'a str,
    ) -> impl Iterator<Item = &'a PluginBinding> + 'a {
        self.bindings
            .iter()
            .filter(move |b| b.phase.eq_ignore_ascii_case(phase))
    }

    /// Return all bindings that target the given phase name.
    ///
    /// This allocates a `Vec` and is provided for callers that need a
    /// collected list. For iteration without allocation, prefer
    /// [`bindings_for_phase_iter`](Self::bindings_for_phase_iter).
    #[must_use]
    pub fn bindings_for_phase<'a>(&'a self, phase: &'a str) -> Vec<&'a PluginBinding> {
        self.bindings_for_phase_iter(phase).collect()
    }

    /// Execute every plugin goal bound to the given phase.
    ///
    /// # Errors
    ///
    /// Returns the first error encountered from a plugin goal.
    pub fn execute_phase_plugins(&self, phase: &str, context: &BuildContext) -> Result<()> {
        for binding in self.bindings_for_phase_iter(phase) {
            if let Some(goal) = self.goals.get(&binding.goal_key) {
                println!(
                    "[INFO] Executing plugin goal: {} [{}] (phase: {phase})",
                    goal.name(),
                    binding.goal_key
                );
                goal.execute(context, &binding.config)?;
            } else {
                println!(
                    "[WARNING] No implementation found for plugin goal: {}",
                    binding.goal_key
                );
            }
        }
        Ok(())
    }
}

/// Built-in plugin goal that prints a configurable message.
///
/// Configuration key: `"message"` — the text to print (defaults to `"Hello from EchoPlugin!"`).
pub struct EchoPlugin;

impl PluginGoal for EchoPlugin {
    fn name(&self) -> &str {
        "echo"
    }

    fn execute(&self, _context: &BuildContext, config: &PluginConfig) -> Result<()> {
        let message = config
            .get("message")
            .cloned()
            .unwrap_or_else(|| "Hello from EchoPlugin!".to_string());
        println!("[ECHO] {message}");
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::pom::Pom;
    use std::collections::HashMap;
    use std::sync::Arc;
    use tempfile::TempDir;

    /// Helper to create a minimal `BuildContext` for tests.
    fn test_context() -> (BuildContext, TempDir, TempDir) {
        let work = TempDir::new().unwrap();
        let repo = TempDir::new().unwrap();
        let ctx = BuildContext::new(
            Pom {
                group_id: "com.example".to_string(),
                artifact_id: "test".to_string(),
                version: "1.0.0".to_string(),
                ..Pom::default()
            },
            work.path().to_path_buf(),
            repo.path().to_path_buf(),
            HashMap::new(),
        );
        (ctx, work, repo)
    }

    #[test]
    fn test_registry_new_is_empty() {
        let registry = PluginRegistry::new();
        assert!(registry.bindings_for_phase("compile").is_empty());
    }

    #[test]
    fn test_register_and_bind_goal() {
        let mut registry = PluginRegistry::new();
        registry.register_goal(
            "org.example:echo-plugin:echo".to_string(),
            Box::new(EchoPlugin),
        );
        registry.add_binding(
            "compile".to_string(),
            "org.example:echo-plugin:echo".to_string(),
            Arc::new(HashMap::new()),
        );

        let bindings = registry.bindings_for_phase("compile");
        assert_eq!(bindings.len(), 1);
        assert_eq!(bindings[0].goal_key, "org.example:echo-plugin:echo");
    }

    #[test]
    fn test_bindings_for_phase_case_insensitive() {
        let mut registry = PluginRegistry::new();
        registry.register_goal("key".to_string(), Box::new(EchoPlugin));
        registry.add_binding(
            "Compile".to_string(),
            "key".to_string(),
            Arc::new(HashMap::new()),
        );

        assert_eq!(registry.bindings_for_phase("compile").len(), 1);
        assert_eq!(registry.bindings_for_phase("COMPILE").len(), 1);
    }

    #[test]
    fn test_bindings_for_unbound_phase_is_empty() {
        let mut registry = PluginRegistry::new();
        registry.register_goal("key".to_string(), Box::new(EchoPlugin));
        registry.add_binding(
            "compile".to_string(),
            "key".to_string(),
            Arc::new(HashMap::new()),
        );

        assert!(registry.bindings_for_phase("test").is_empty());
    }

    #[test]
    fn test_execute_phase_plugins_empty_registry_succeeds() {
        let (ctx, _work, _repo) = test_context();
        let result = ctx.plugin_registry.execute_phase_plugins("validate", &ctx);
        // The test_context has an empty registry, so no plugins run.
        assert!(result.is_ok());
    }

    #[test]
    fn test_execute_phase_plugins_with_registered_goal() {
        let work = TempDir::new().unwrap();
        let repo = TempDir::new().unwrap();
        let mut registry = PluginRegistry::new();
        registry.register_goal("key".to_string(), Box::new(EchoPlugin));
        let mut config = HashMap::new();
        config.insert("message".to_string(), "hello".to_string());
        registry.add_binding("compile".to_string(), "key".to_string(), Arc::new(config));

        let ctx = BuildContext::with_plugin_registry(
            Pom {
                group_id: "com.example".to_string(),
                artifact_id: "test".to_string(),
                version: "1.0.0".to_string(),
                ..Pom::default()
            },
            work.path().to_path_buf(),
            repo.path().to_path_buf(),
            HashMap::new(),
            registry,
        );
        let result = ctx.plugin_registry.execute_phase_plugins("compile", &ctx);
        assert!(result.is_ok());
    }

    #[test]
    fn test_execute_phase_plugins_missing_goal_warns() {
        let work = TempDir::new().unwrap();
        let repo = TempDir::new().unwrap();
        let mut registry = PluginRegistry::new();
        // Add a binding but don't register the goal
        registry.add_binding(
            "compile".to_string(),
            "missing:goal:key".to_string(),
            Arc::new(HashMap::new()),
        );

        let ctx = BuildContext::with_plugin_registry(
            Pom {
                group_id: "com.example".to_string(),
                artifact_id: "test".to_string(),
                version: "1.0.0".to_string(),
                ..Pom::default()
            },
            work.path().to_path_buf(),
            repo.path().to_path_buf(),
            HashMap::new(),
            registry,
        );
        // Should succeed (just prints a warning)
        let result = ctx.plugin_registry.execute_phase_plugins("compile", &ctx);
        assert!(result.is_ok());
    }

    #[test]
    fn test_echo_plugin_default_message() {
        let plugin = EchoPlugin;
        assert_eq!(plugin.name(), "echo");
        let (ctx, _work, _repo) = test_context();
        let result = plugin.execute(&ctx, &HashMap::new());
        assert!(result.is_ok());
    }

    #[test]
    fn test_echo_plugin_custom_message() {
        let plugin = EchoPlugin;
        let mut config = HashMap::new();
        config.insert("message".to_string(), "custom message".to_string());
        let (ctx, _work, _repo) = test_context();
        let result = plugin.execute(&ctx, &config);
        assert!(result.is_ok());
    }

    #[test]
    fn test_multiple_bindings_same_phase() {
        let mut registry = PluginRegistry::new();
        registry.register_goal("goal-a".to_string(), Box::new(EchoPlugin));
        registry.register_goal("goal-b".to_string(), Box::new(EchoPlugin));
        registry.add_binding(
            "compile".to_string(),
            "goal-a".to_string(),
            Arc::new(HashMap::new()),
        );
        registry.add_binding(
            "compile".to_string(),
            "goal-b".to_string(),
            Arc::new(HashMap::new()),
        );

        let bindings = registry.bindings_for_phase("compile");
        assert_eq!(bindings.len(), 2);
    }

    #[test]
    fn test_default_registry_is_empty() {
        let registry = PluginRegistry::default();
        assert!(registry.bindings_for_phase("compile").is_empty());
    }
}
