use anyhow::{Context, Result};
use quick_xml::de::from_str;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::Path;

#[derive(Debug, Clone, Deserialize, Serialize, Default)]
#[serde(rename = "project")]
pub struct Pom {
    #[serde(rename = "modelVersion", default)]
    pub model_version: String,
    #[serde(rename = "groupId", default)]
    pub group_id: String,
    #[serde(rename = "artifactId", default)]
    pub artifact_id: String,
    #[serde(rename = "version", default)]
    pub version: String,
    #[serde(rename = "packaging", default = "default_packaging")]
    pub packaging: String,
    #[serde(rename = "name", default)]
    pub name: String,
    #[serde(rename = "description", default)]
    pub description: String,
    #[serde(rename = "parent")]
    pub parent: Option<Parent>,
    #[serde(rename = "dependencies", default)]
    pub dependencies: DependenciesWrapper,
    #[serde(rename = "build")]
    pub build: Option<Build>,
    #[serde(rename = "properties", default)]
    pub properties: PropertiesWrapper,
}

fn default_packaging() -> String {
    "jar".to_string()
}

#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct DependenciesWrapper {
    #[serde(rename = "dependency", default)]
    pub items: Vec<Dependency>,
}

#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct PropertiesWrapper {
    /// Arbitrary `<key>value</key>` entries inside `<properties>`.
    /// Captured by serde flatten so any element name becomes a map key.
    #[serde(flatten, default)]
    pub entries: HashMap<String, String>,
}

#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct Parent {
    #[serde(rename = "groupId", default)]
    pub group_id: String,
    #[serde(rename = "artifactId", default)]
    pub artifact_id: String,
    #[serde(rename = "version", default)]
    pub version: String,
    #[serde(rename = "relativePath", default)]
    pub relative_path: String,
}

#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct Dependency {
    #[serde(rename = "groupId", default)]
    pub group_id: String,
    #[serde(rename = "artifactId", default)]
    pub artifact_id: String,
    #[serde(rename = "version", default)]
    pub version: String,
    #[serde(rename = "scope", default = "default_scope")]
    pub scope: String,
    #[serde(rename = "optional", default)]
    pub optional: bool,
}

fn default_scope() -> String {
    "compile".to_string()
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Build {
    #[serde(rename = "sourceDirectory", default = "default_source_dir")]
    pub source_directory: String,
    #[serde(rename = "testSourceDirectory", default = "default_test_source_dir")]
    pub test_source_directory: String,
    #[serde(rename = "outputDirectory", default = "default_output_dir")]
    pub output_directory: String,
    #[serde(rename = "testOutputDirectory", default = "default_test_output_dir")]
    pub test_output_directory: String,
    #[serde(rename = "plugins", default)]
    pub plugins: PluginsWrapper,
}

impl Default for Build {
    fn default() -> Self {
        Build {
            source_directory: default_source_dir(),
            test_source_directory: default_test_source_dir(),
            output_directory: default_output_dir(),
            test_output_directory: default_test_output_dir(),
            plugins: PluginsWrapper::default(),
        }
    }
}

#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct PluginsWrapper {
    #[serde(rename = "plugin", default)]
    pub items: Vec<PluginDecl>,
}

/// A plugin declaration inside `<build><plugins>`.
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct PluginDecl {
    #[serde(rename = "groupId", default)]
    pub group_id: String,
    #[serde(rename = "artifactId", default)]
    pub artifact_id: String,
    #[serde(rename = "version", default)]
    pub version: String,
    #[serde(rename = "configuration", default)]
    pub configuration: PluginConfiguration,
    #[serde(rename = "executions", default)]
    pub executions: ExecutionsWrapper,
}

/// Plugin configuration is represented as a flat map of `<key>value</key>` entries.
///
/// A custom [`Deserialize`] implementation is used (similar in effect to
/// [`PropertiesWrapper`]) so that arbitrary child elements of
/// `<configuration>` are captured as map entries.
#[derive(Debug, Clone, Serialize, Default)]
pub struct PluginConfiguration {
    #[serde(flatten)]
    pub entries: HashMap<String, String>,
}

/// Custom deserializer for `<configuration>` that:
/// - accepts a map of string key-value pairs (from XML child elements) and
///   collects them into `HashMap<String, String>`,
/// - preserves scalar text as a `"$text"` entry, and
/// - treats unit/none configuration as an empty map.
///
/// This avoids relying on error-message text and propagates genuine map
/// deserialization errors instead of silently swallowing them.
struct PluginConfigurationVisitor;

impl<'de> serde::de::Visitor<'de> for PluginConfigurationVisitor {
    type Value = PluginConfiguration;

    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
        formatter.write_str("a map of string key-value pairs or a scalar/empty configuration")
    }

    fn visit_map<M>(self, mut map: M) -> std::result::Result<Self::Value, M::Error>
    where
        M: serde::de::MapAccess<'de>,
    {
        let mut entries = HashMap::new();
        while let Some((key, value)) = map.next_entry::<String, String>()? {
            entries.insert(key, value);
        }
        Ok(PluginConfiguration { entries })
    }

    fn visit_unit<E>(self) -> std::result::Result<Self::Value, E>
    where
        E: serde::de::Error,
    {
        Ok(PluginConfiguration::default())
    }

    fn visit_none<E>(self) -> std::result::Result<Self::Value, E>
    where
        E: serde::de::Error,
    {
        Ok(PluginConfiguration::default())
    }

    fn visit_some<D>(self, deserializer: D) -> std::result::Result<Self::Value, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        deserializer.deserialize_any(self)
    }

    fn visit_str<E>(self, v: &str) -> std::result::Result<Self::Value, E>
    where
        E: serde::de::Error,
    {
        let mut entries = HashMap::new();
        entries.insert("$text".to_string(), v.to_string());
        Ok(PluginConfiguration { entries })
    }

    fn visit_string<E>(self, v: String) -> std::result::Result<Self::Value, E>
    where
        E: serde::de::Error,
    {
        let mut entries = HashMap::new();
        entries.insert("$text".to_string(), v);
        Ok(PluginConfiguration { entries })
    }
}

impl<'de> serde::Deserialize<'de> for PluginConfiguration {
    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        deserializer.deserialize_any(PluginConfigurationVisitor)
    }
}

#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct ExecutionsWrapper {
    #[serde(rename = "execution", default)]
    pub items: Vec<PluginExecution>,
}

/// A single `<execution>` inside a plugin declaration.
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct PluginExecution {
    #[serde(rename = "id", default)]
    pub id: String,
    #[serde(rename = "phase", default)]
    pub phase: String,
    #[serde(rename = "goals", default)]
    pub goals: GoalsWrapper,
    #[serde(rename = "configuration", default)]
    pub configuration: PluginConfiguration,
}

#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct GoalsWrapper {
    #[serde(rename = "goal", default)]
    pub items: Vec<String>,
}

fn default_source_dir() -> String {
    "src/main/java".to_string()
}

fn default_test_source_dir() -> String {
    "src/test/java".to_string()
}

fn default_output_dir() -> String {
    "target/classes".to_string()
}

fn default_test_output_dir() -> String {
    "target/test-classes".to_string()
}

impl Pom {
    /// Returns the effective `group_id`, falling back to parent `group_id` if needed.
    #[must_use]
    pub fn effective_group_id(&self) -> &str {
        if self.group_id.is_empty() {
            self.parent.as_ref().map_or("", |p| p.group_id.as_str())
        } else {
            &self.group_id
        }
    }

    /// Returns the effective version, falling back to parent version if needed.
    #[must_use]
    pub fn effective_version(&self) -> &str {
        if self.version.is_empty() {
            self.parent.as_ref().map_or("", |p| p.version.as_str())
        } else {
            &self.version
        }
    }

    /// Build a flat properties map including `project.*` properties and POM `<properties>` entries.
    #[must_use]
    pub fn properties_map(&self) -> HashMap<String, String> {
        // Start with POM <properties> entries; project.* keys override them below.
        let mut map = self.properties.entries.clone();
        map.insert(
            "project.groupId".to_string(),
            self.effective_group_id().to_string(),
        );
        map.insert("project.artifactId".to_string(), self.artifact_id.clone());
        map.insert(
            "project.version".to_string(),
            self.effective_version().to_string(),
        );
        map.insert("project.packaging".to_string(), self.packaging.clone());
        map
    }

    /// Resolve a string by substituting `${property}` placeholders.
    #[must_use]
    pub fn resolve_property(&self, value: &str) -> String {
        let props = self.properties_map();
        let mut result = value.to_string();
        for (k, v) in &props {
            let placeholder = format!("${{{k}}}");
            result = result.replace(&placeholder, v);
        }
        result
    }

    /// Get effective build config (with defaults).
    #[must_use]
    pub fn effective_build(&self) -> Build {
        self.build.clone().unwrap_or_default()
    }
}

/// Parse a pom.xml file from the given path.
///
/// # Errors
///
/// Returns an error if the file cannot be read or the XML is malformed.
pub fn parse_pom(path: &Path) -> Result<Pom> {
    let content = std::fs::read_to_string(path)
        .with_context(|| format!("Failed to read POM file: {}", path.display()))?;
    let pom: Pom = from_str(&content)
        .with_context(|| format!("Failed to parse POM file: {}", path.display()))?;
    Ok(pom)
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Write;
    use tempfile::NamedTempFile;

    #[test]
    fn test_parse_minimal_pom() {
        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>my-app</artifactId>
    <version>1.0.0</version>
</project>"#;

        let mut file = NamedTempFile::new().unwrap();
        file.write_all(xml.as_bytes()).unwrap();
        let pom = parse_pom(file.path()).unwrap();

        assert_eq!(pom.model_version, "4.0.0");
        assert_eq!(pom.group_id, "com.example");
        assert_eq!(pom.artifact_id, "my-app");
        assert_eq!(pom.version, "1.0.0");
        assert_eq!(pom.packaging, "jar"); // default
    }

    #[test]
    fn test_parse_pom_with_dependencies() {
        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>my-app</artifactId>
    <version>1.0.0</version>
    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>"#;

        let mut file = NamedTempFile::new().unwrap();
        file.write_all(xml.as_bytes()).unwrap();
        let pom = parse_pom(file.path()).unwrap();

        assert_eq!(pom.dependencies.items.len(), 1);
        let dep = &pom.dependencies.items[0];
        assert_eq!(dep.group_id, "junit");
        assert_eq!(dep.artifact_id, "junit");
        assert_eq!(dep.version, "4.13.2");
        assert_eq!(dep.scope, "test");
    }

    #[test]
    fn test_resolve_property() {
        let pom = Pom {
            group_id: "com.example".to_string(),
            artifact_id: "my-app".to_string(),
            version: "2.0.0".to_string(),
            ..Pom::default()
        };

        let resolved = pom.resolve_property("${project.version}");
        assert_eq!(resolved, "2.0.0");
    }

    #[test]
    fn test_effective_group_id_from_parent() {
        let pom = Pom {
            parent: Some(Parent {
                group_id: "com.parent".to_string(),
                artifact_id: "parent".to_string(),
                version: "1.0".to_string(),
                relative_path: String::new(),
            }),
            ..Pom::default()
        };
        assert_eq!(pom.effective_group_id(), "com.parent");
    }

    #[test]
    fn test_parse_pom_with_plugins() {
        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>my-app</artifactId>
    <version>1.0.0</version>
    <build>
        <plugins>
            <plugin>
                <groupId>org.pman</groupId>
                <artifactId>pman-echo-plugin</artifactId>
                <version>1.0</version>
                <configuration>
                    <message>hello from plugin</message>
                </configuration>
                <executions>
                    <execution>
                        <id>say-hello</id>
                        <phase>validate</phase>
                        <goals>
                            <goal>echo</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>"#;

        let mut file = NamedTempFile::new().unwrap();
        file.write_all(xml.as_bytes()).unwrap();
        let pom = parse_pom(file.path()).unwrap();

        let build = pom.effective_build();
        assert_eq!(build.plugins.items.len(), 1);

        let plugin = &build.plugins.items[0];
        assert_eq!(plugin.group_id, "org.pman");
        assert_eq!(plugin.artifact_id, "pman-echo-plugin");
        assert_eq!(plugin.version, "1.0");
        assert_eq!(
            plugin.configuration.entries.get("message").unwrap(),
            "hello from plugin"
        );

        assert_eq!(plugin.executions.items.len(), 1);
        let exec = &plugin.executions.items[0];
        assert_eq!(exec.id, "say-hello");
        assert_eq!(exec.phase, "validate");
        assert_eq!(exec.goals.items, vec!["echo"]);
    }

    #[test]
    fn test_parse_pom_empty_configuration_element() {
        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>my-app</artifactId>
    <version>1.0.0</version>
    <build>
        <plugins>
            <plugin>
                <groupId>org.pman</groupId>
                <artifactId>pman-echo-plugin</artifactId>
                <version>1.0</version>
                <configuration/>
            </plugin>
        </plugins>
    </build>
</project>"#;

        let mut file = NamedTempFile::new().unwrap();
        file.write_all(xml.as_bytes()).unwrap();
        let pom = parse_pom(file.path()).unwrap();

        let plugins = &pom.effective_build().plugins.items;
        assert_eq!(plugins.len(), 1);
        let plugin = &plugins[0];
        assert!(
            plugin.configuration.entries.is_empty(),
            "Self-closing <configuration/> should yield an empty map"
        );
    }

    #[test]
    fn test_parse_pom_scalar_configuration_element() {
        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>my-app</artifactId>
    <version>1.0.0</version>
    <build>
        <plugins>
            <plugin>
                <groupId>org.pman</groupId>
                <artifactId>pman-echo-plugin</artifactId>
                <version>1.0</version>
                <configuration>some text</configuration>
            </plugin>
        </plugins>
    </build>
</project>"#;

        let mut file = NamedTempFile::new().unwrap();
        file.write_all(xml.as_bytes()).unwrap();
        let pom = parse_pom(file.path()).unwrap();

        let plugins = &pom.effective_build().plugins.items;
        assert_eq!(plugins.len(), 1);
        let plugin = &plugins[0];
        // PluginConfigurationVisitor::visit_str/visit_string stores scalar text
        // under the special "$text" key in the configuration entries map.
        assert_eq!(
            plugin.configuration.entries.get("$text"),
            Some(&"some text".to_string()),
            "Scalar <configuration>text</configuration> should be collected as $text entry"
        );
    }

    #[test]
    fn test_serialize_pom_scalar_configuration_element_round_trip() {
        use quick_xml::se::to_string;

        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>my-app</artifactId>
    <version>1.0.0</version>
    <build>
        <plugins>
            <plugin>
                <groupId>org.pman</groupId>
                <artifactId>pman-echo-plugin</artifactId>
                <version>1.0</version>
                <configuration>some text</configuration>
            </plugin>
        </plugins>
    </build>
</project>"#;

        let mut file = NamedTempFile::new().unwrap();
        file.write_all(xml.as_bytes()).unwrap();
        let pom = parse_pom(file.path()).unwrap();

        let serialized = to_string(&pom).unwrap();
        assert!(
            serialized.contains("<configuration>some text</configuration>"),
            "Serialized POM should contain scalar <configuration>some text</configuration> element"
        );
        assert!(
            !serialized.contains("<$text>"),
            "Serialized POM must not emit an invalid <$text> element"
        );
    }

    #[test]
    fn test_parse_pom_without_plugins_has_empty_list() {
        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>my-app</artifactId>
    <version>1.0.0</version>
</project>"#;

        let mut file = NamedTempFile::new().unwrap();
        file.write_all(xml.as_bytes()).unwrap();
        let pom = parse_pom(file.path()).unwrap();

        let build = pom.effective_build();
        assert!(build.plugins.items.is_empty());
    }
}
