//! POM XML parser using quick-xml's event-based reader.
//!
//! Uses event-based parsing intentionally: serde can't handle `<properties>` blocks
//! with arbitrary child element names as a `HashMap<String, String>`.
//! Handles XML namespaces by stripping namespace prefixes from element names.

use anyhow::{Context, Result};
use quick_xml::events::Event;
use quick_xml::reader::Reader;
use std::collections::HashMap;
use std::path::Path;

use crate::error::DepupError;

/// Parsed representation of a Maven POM file.
#[derive(Debug, Default)]
pub struct Project {
    pub group_id: Option<String>,
    pub artifact_id: Option<String>,
    pub version: Option<String>,
    pub packaging: Option<String>,
    pub modules: Vec<String>,
    pub properties: HashMap<String, String>,
    pub artifacts: Vec<(Artifact, ArtifactKind)>,
    pub repositories: Vec<Repository>,
}

/// A Maven dependency or plugin artifact with optional GAV coordinates.
#[derive(Debug, Clone)]
#[allow(clippy::struct_field_names)]
pub struct Artifact {
    pub group_id: Option<String>,
    pub artifact_id: Option<String>,
    pub version: Option<String>,
}

/// Whether an artifact is a dependency or a plugin.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ArtifactKind {
    Dependency,
    Plugin,
}

impl std::fmt::Display for ArtifactKind {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Dependency => write!(f, "Dependency"),
            Self::Plugin => write!(f, "Plugin"),
        }
    }
}

/// A Maven repository declared in the POM (either standard or plugin).
#[derive(Debug, Clone)]
pub struct Repository {
    pub id: Option<String>,
    pub name: Option<String>,
    pub url: String,
    pub kind: RepositoryKind,
}

/// Distinguishes standard repositories (for dependencies) from plugin repositories.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RepositoryKind {
    Standard,
    Plugin,
}

const MAX_POM_SIZE: u64 = 10 * 1024 * 1024;

/// Reads and parses a POM file from disk.
pub fn parse_pom(path: &Path) -> Result<Project> {
    let path_str = path.display().to_string();
    let metadata = std::fs::symlink_metadata(path)
        .map_err(|e| DepupError::pom_parse_failed(&path_str, &e.to_string()))?;
    if metadata.is_symlink() {
        return Err(DepupError::pom_parse_failed(&path_str, "Symlinks not allowed").into());
    }
    if metadata.len() > MAX_POM_SIZE {
        return Err(DepupError::pom_parse_failed(
            &path_str,
            &format!(
                "POM file too large: {} bytes (max {})",
                metadata.len(),
                MAX_POM_SIZE
            ),
        )
        .into());
    }
    let content = std::fs::read_to_string(path)
        .map_err(|e| DepupError::pom_parse_failed(&path_str, &e.to_string()))?;
    parse_pom_str(&content)
        .map_err(|e| DepupError::pom_parse_failed(&path_str, &e.to_string()).into())
}

/// Parses POM XML from a string. Extracts project coordinates, properties,
/// modules, artifacts (dependencies and plugins), and repositories.
pub fn parse_pom_str(xml: &str) -> Result<Project> {
    let mut reader = Reader::from_str(xml);
    let mut state = ParseState::default();

    loop {
        match reader.read_event() {
            Ok(Event::Start(e)) => state.handle_start(&e),
            Ok(Event::End(_)) => state.handle_end(),
            Ok(Event::Text(e)) => {
                let unescaped = e.unescape().context("Failed to unescape XML text")?;
                state.text_buf.push_str(&unescaped);
            }
            Ok(Event::Eof) => break,
            Err(e) => anyhow::bail!("XML parse error: {e}"),
            _ => {}
        }
    }

    Ok(state.project)
}

/// Internal state machine for the event-based XML parser.
/// Tracks the element path stack, text buffer, and in-progress artifacts/repositories.
#[derive(Default)]
struct ParseState {
    project: Project,
    path_stack: Vec<String>,
    text_buf: String,
    artifact_stack: Vec<(Artifact, ArtifactKind)>,
    repo_stack: Vec<(Repository, bool)>,
}

impl ParseState {
    fn handle_start(&mut self, e: &quick_xml::events::BytesStart) {
        let name = local_name(e);
        self.path_stack.push(name);
        self.text_buf.clear();

        if is_dependency_element(&self.path_stack) {
            self.artifact_stack.push((
                Artifact {
                    group_id: None,
                    artifact_id: None,
                    version: None,
                },
                ArtifactKind::Dependency,
            ));
        } else if is_plugin_element(&self.path_stack) {
            self.artifact_stack.push((
                Artifact {
                    group_id: None,
                    artifact_id: None,
                    version: None,
                },
                ArtifactKind::Plugin,
            ));
        } else if is_repository_element(&self.path_stack) {
            self.repo_stack.push((
                Repository {
                    id: None,
                    name: None,
                    url: String::new(),
                    kind: RepositoryKind::Standard,
                },
                true,
            ));
        } else if is_plugin_repository_element(&self.path_stack) {
            self.repo_stack.push((
                Repository {
                    id: None,
                    name: None,
                    url: String::new(),
                    kind: RepositoryKind::Plugin,
                },
                true,
            ));
        }
    }

    fn handle_end(&mut self) {
        let current_path = self.path_stack.join("/");
        let text = self.text_buf.trim().to_string();

        self.collect_project_coordinates(&text);
        self.collect_property(&text);
        self.collect_module(&text);
        self.update_artifact_fields(&current_path, &text);
        self.update_repo_fields(&current_path, &text);
        self.finish_artifact();
        self.finish_repository();

        self.text_buf.clear();
        self.path_stack.pop();
    }

    fn collect_project_coordinates(&mut self, text: &str) {
        if self.path_stack.len() == 2 && self.path_stack[0] == "project" {
            match self.path_stack[1].as_str() {
                "groupId" => self.project.group_id = Some(text.to_string()),
                "artifactId" => self.project.artifact_id = Some(text.to_string()),
                "version" => self.project.version = Some(text.to_string()),
                "packaging" => self.project.packaging = Some(text.to_string()),
                _ => {}
            }
        }
    }

    fn collect_property(&mut self, text: &str) {
        if is_in_properties(&self.path_stack)
            && self.path_stack.len() > 2
            && let Some(prop_name) = self.path_stack.last()
        {
            self.project
                .properties
                .insert(prop_name.clone(), text.to_string());
        }
    }

    fn collect_module(&mut self, text: &str) {
        if is_module_element(&self.path_stack) {
            self.project.modules.push(text.to_string());
        }
    }

    fn update_artifact_fields(&mut self, current_path: &str, text: &str) {
        let in_nested_block = self
            .path_stack
            .iter()
            .any(|s| s == "exclusions" || s == "configuration");
        if !in_nested_block && let Some((artifact, _)) = self.artifact_stack.last_mut() {
            if current_path.ends_with("/groupId") {
                artifact.group_id = Some(text.to_string());
            } else if current_path.ends_with("/artifactId") {
                artifact.artifact_id = Some(text.to_string());
            } else if current_path.ends_with("/version") {
                artifact.version = Some(text.to_string());
            }
        }
    }

    fn update_repo_fields(&mut self, current_path: &str, text: &str) {
        if let Some((repo, true)) = self.repo_stack.last_mut() {
            if current_path.ends_with("/id") {
                repo.id = Some(text.to_string());
            } else if current_path.ends_with("/name") {
                repo.name = Some(text.to_string());
            } else if current_path.ends_with("/url") {
                repo.url = text.to_string();
            }
        }
    }

    fn finish_artifact(&mut self) {
        if (is_dependency_element(&self.path_stack) || is_plugin_element(&self.path_stack))
            && let Some((artifact, kind)) = self.artifact_stack.pop()
        {
            self.project.artifacts.push((artifact, kind));
        }
    }

    fn finish_repository(&mut self) {
        if (is_repository_element(&self.path_stack)
            || is_plugin_repository_element(&self.path_stack))
            && let Some((repo, _)) = self.repo_stack.pop()
            && !repo.url.is_empty()
        {
            self.project.repositories.push(repo);
        }
    }
}

/// Strips the namespace prefix from an XML element name (e.g., `ns:element` → `element`).
fn local_name(e: &quick_xml::events::BytesStart) -> String {
    let full = String::from_utf8_lossy(e.name().as_ref()).to_string();
    full.split(':').next_back().unwrap_or(&full).to_string()
}

fn is_in_properties(stack: &[String]) -> bool {
    stack.iter().any(|s| s == "properties")
}

fn is_module_element(stack: &[String]) -> bool {
    stack.last().map(String::as_str) == Some("module") && stack.iter().any(|s| s == "modules")
}

fn is_dependency_element(stack: &[String]) -> bool {
    stack.last().map(String::as_str) == Some("dependency")
        && stack
            .iter()
            .any(|s| s == "dependencies" || s == "dependencyManagement")
}

fn is_plugin_element(stack: &[String]) -> bool {
    stack.last().map(String::as_str) == Some("plugin")
        && stack
            .iter()
            .any(|s| s == "plugins" || s == "pluginManagement")
}

fn is_repository_element(stack: &[String]) -> bool {
    stack.last().map(String::as_str) == Some("repository")
        && stack.iter().any(|s| s == "repositories")
        && !stack.iter().any(|s| s == "pluginRepositories")
}

fn is_plugin_repository_element(stack: &[String]) -> bool {
    stack.last().map(String::as_str) == Some("pluginRepository")
        && stack.iter().any(|s| s == "pluginRepositories")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_simple_pom() {
        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<project>
    <properties>
        <version.wildfly>35.0.0.Final</version.wildfly>
        <version.lombok>1.18.30</version.lombok>
    </properties>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.wildfly.bom</groupId>
                <artifactId>wildfly-ee</artifactId>
                <version>${version.wildfly}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>${version.compiler.plugin}</version>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>
    <modules>
        <module>child-a</module>
        <module>child-b</module>
    </modules>
</project>"#;

        let project = parse_pom_str(xml).unwrap();

        assert_eq!(
            project
                .properties
                .get("version.wildfly")
                .map(String::as_str),
            Some("35.0.0.Final")
        );
        assert_eq!(
            project.properties.get("version.lombok").map(String::as_str),
            Some("1.18.30")
        );

        assert_eq!(project.modules, vec!["child-a", "child-b"]);

        assert_eq!(project.artifacts.len(), 2);
        assert_eq!(
            project.artifacts[0].0.artifact_id.as_deref(),
            Some("wildfly-ee")
        );
        assert_eq!(project.artifacts[0].1, ArtifactKind::Dependency);
        assert_eq!(
            project.artifacts[1].0.artifact_id.as_deref(),
            Some("maven-compiler-plugin")
        );
        assert_eq!(project.artifacts[1].1, ArtifactKind::Plugin);
    }

    #[test]
    fn parse_minimal_pom() {
        let xml = r#"<project></project>"#;
        let project = parse_pom_str(xml).unwrap();
        assert!(project.properties.is_empty());
        assert!(project.modules.is_empty());
        assert!(project.artifacts.is_empty());
    }

    #[test]
    fn parse_with_namespace() {
        let xml = r#"<project xmlns="http://maven.apache.org/POM/4.0.0">
    <properties>
        <version.junit>5.10.0</version.junit>
    </properties>
</project>"#;

        let project = parse_pom_str(xml).unwrap();
        assert_eq!(
            project.properties.get("version.junit").map(String::as_str),
            Some("5.10.0")
        );
    }

    #[test]
    fn finds_plugin_internal_dependencies() {
        let xml = r#"<project>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>${version.surefire}</version>
                <dependencies>
                    <dependency>
                        <groupId>org.junit.platform</groupId>
                        <artifactId>junit-platform-surefire-provider</artifactId>
                        <version>${version.junit.platform}</version>
                    </dependency>
                </dependencies>
            </plugin>
        </plugins>
    </build>
</project>"#;

        let project = parse_pom_str(xml).unwrap();
        // The plugin's nested <dependency> is correctly picked up as a dependency
        // (it has its own version property that needs checking)
        assert!(project.artifacts.len() >= 1);
        let plugin = project
            .artifacts
            .iter()
            .find(|(a, _)| a.artifact_id.as_deref() == Some("maven-surefire-plugin"));
        assert!(plugin.is_some());
        assert_eq!(plugin.unwrap().1, ArtifactKind::Plugin);
    }

    #[test]
    fn parse_repositories() {
        let xml = r#"<project>
    <repositories>
        <repository>
            <id>jboss-public</id>
            <name>JBoss Public</name>
            <url>https://repository.jboss.org/nexus/content/groups/public/</url>
        </repository>
        <repository>
            <id>central-proxy</id>
            <url>https://repo.example.com/maven2</url>
        </repository>
    </repositories>
    <pluginRepositories>
        <pluginRepository>
            <id>jboss-plugins</id>
            <url>https://repository.jboss.org/nexus/content/groups/public/</url>
        </pluginRepository>
    </pluginRepositories>
</project>"#;

        let project = parse_pom_str(xml).unwrap();
        assert_eq!(project.repositories.len(), 3);

        let jboss = &project.repositories[0];
        assert_eq!(jboss.id.as_deref(), Some("jboss-public"));
        assert_eq!(jboss.name.as_deref(), Some("JBoss Public"));
        assert!(jboss.url.contains("jboss.org"));
        assert_eq!(jboss.kind, RepositoryKind::Standard);

        let proxy = &project.repositories[1];
        assert_eq!(proxy.id.as_deref(), Some("central-proxy"));
        assert_eq!(proxy.name, None);
        assert_eq!(proxy.kind, RepositoryKind::Standard);

        let plugin_repo = &project.repositories[2];
        assert_eq!(plugin_repo.id.as_deref(), Some("jboss-plugins"));
        assert_eq!(plugin_repo.kind, RepositoryKind::Plugin);
    }

    #[test]
    fn parse_repository_without_id() {
        let xml = r#"<project>
    <repositories>
        <repository>
            <url>https://repo.example.com/maven2</url>
        </repository>
    </repositories>
</project>"#;

        let project = parse_pom_str(xml).unwrap();
        assert_eq!(project.repositories.len(), 1);
        assert_eq!(project.repositories[0].id, None);
        assert_eq!(project.repositories[0].name, None);
        assert_eq!(
            project.repositories[0].url,
            "https://repo.example.com/maven2"
        );
    }

    #[test]
    fn skips_repository_without_url() {
        let xml = r#"<project>
    <repositories>
        <repository>
            <id>broken</id>
            <name>No URL repo</name>
        </repository>
    </repositories>
</project>"#;

        let project = parse_pom_str(xml).unwrap();
        assert_eq!(project.repositories.len(), 0);
    }

    #[test]
    fn parse_direct_dependencies_and_plugins() {
        let xml = r#"<project>
    <dependencies>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>${version.guava}</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>${version.jar.plugin}</version>
            </plugin>
        </plugins>
    </build>
</project>"#;

        let project = parse_pom_str(xml).unwrap();
        assert_eq!(project.artifacts.len(), 2);
        assert_eq!(project.artifacts[0].1, ArtifactKind::Dependency);
        assert_eq!(project.artifacts[0].0.artifact_id.as_deref(), Some("guava"));
        assert_eq!(project.artifacts[1].1, ArtifactKind::Plugin);
    }

    #[test]
    fn parse_project_coordinates() {
        let xml = r#"<project>
    <groupId>org.wildfly</groupId>
    <artifactId>wildfly-parent</artifactId>
    <version>35.0.0.Final</version>
    <packaging>pom</packaging>
</project>"#;

        let project = parse_pom_str(xml).unwrap();
        assert_eq!(project.group_id.as_deref(), Some("org.wildfly"));
        assert_eq!(project.artifact_id.as_deref(), Some("wildfly-parent"));
        assert_eq!(project.version.as_deref(), Some("35.0.0.Final"));
        assert_eq!(project.packaging.as_deref(), Some("pom"));
    }

    #[test]
    fn parse_project_coordinates_missing_packaging() {
        let xml = r#"<project>
    <groupId>org.example</groupId>
    <artifactId>my-lib</artifactId>
    <version>1.0.0</version>
</project>"#;

        let project = parse_pom_str(xml).unwrap();
        assert_eq!(project.group_id.as_deref(), Some("org.example"));
        assert_eq!(project.artifact_id.as_deref(), Some("my-lib"));
        assert_eq!(project.version.as_deref(), Some("1.0.0"));
        assert_eq!(project.packaging, None);
    }

    #[test]
    fn exclusions_do_not_overwrite_dependency_coordinates() {
        let xml = r#"<project>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.jboss.elemento</groupId>
                <artifactId>elemento-core</artifactId>
                <version>${version.elemento}</version>
                <exclusions>
                    <exclusion>
                        <groupId>org.junit.jupiter</groupId>
                        <artifactId>junit-jupiter</artifactId>
                    </exclusion>
                </exclusions>
            </dependency>
        </dependencies>
    </dependencyManagement>
</project>"#;

        let project = parse_pom_str(xml).unwrap();
        assert_eq!(project.artifacts.len(), 1);
        let (artifact, kind) = &project.artifacts[0];
        assert_eq!(artifact.group_id.as_deref(), Some("org.jboss.elemento"));
        assert_eq!(artifact.artifact_id.as_deref(), Some("elemento-core"));
        assert_eq!(artifact.version.as_deref(), Some("${version.elemento}"));
        assert_eq!(*kind, ArtifactKind::Dependency);
    }

    #[test]
    fn configuration_version_does_not_overwrite_plugin_coordinates() {
        let xml = r#"<project>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-enforcer-plugin</artifactId>
                <version>${version.enforcer.plugin}</version>
                <configuration>
                    <rules>
                        <requireJavaVersion>
                            <version>${maven.compiler.source}</version>
                        </requireJavaVersion>
                    </rules>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>"#;

        let project = parse_pom_str(xml).unwrap();
        assert_eq!(project.artifacts.len(), 1);
        let (artifact, kind) = &project.artifacts[0];
        assert_eq!(
            artifact.group_id.as_deref(),
            Some("org.apache.maven.plugins")
        );
        assert_eq!(
            artifact.artifact_id.as_deref(),
            Some("maven-enforcer-plugin")
        );
        assert_eq!(
            artifact.version.as_deref(),
            Some("${version.enforcer.plugin}")
        );
        assert_eq!(*kind, ArtifactKind::Plugin);
    }

    #[test]
    fn parse_project_coordinates_not_confused_with_dependency() {
        let xml = r#"<project>
    <groupId>org.example</groupId>
    <artifactId>parent</artifactId>
    <version>1.0.0</version>
    <dependencies>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>33.0.0-jre</version>
        </dependency>
    </dependencies>
</project>"#;

        let project = parse_pom_str(xml).unwrap();
        assert_eq!(project.group_id.as_deref(), Some("org.example"));
        assert_eq!(project.artifact_id.as_deref(), Some("parent"));
        assert_eq!(project.version.as_deref(), Some("1.0.0"));
    }

    #[test]
    fn parse_profile_modules_and_properties() {
        let xml = r#"<project>
    <modules>
        <module>core</module>
    </modules>
    <profiles>
        <profile>
            <id>extras</id>
            <modules>
                <module>extra-a</module>
                <module>extra-b</module>
            </modules>
            <properties>
                <quarkus.version>3.36.2</quarkus.version>
            </properties>
            <dependencies>
                <dependency>
                    <groupId>io.quarkus</groupId>
                    <artifactId>quarkus-core</artifactId>
                    <version>${quarkus.version}</version>
                </dependency>
            </dependencies>
            <build>
                <plugins>
                    <plugin>
                        <groupId>io.quarkus</groupId>
                        <artifactId>quarkus-maven-plugin</artifactId>
                        <version>${quarkus.version}</version>
                    </plugin>
                </plugins>
            </build>
        </profile>
    </profiles>
</project>"#;

        let project = parse_pom_str(xml).unwrap();

        assert_eq!(project.modules, vec!["core", "extra-a", "extra-b"]);

        assert_eq!(
            project
                .properties
                .get("quarkus.version")
                .map(String::as_str),
            Some("3.36.2")
        );

        assert!(
            project
                .artifacts
                .iter()
                .any(|(a, k)| a.artifact_id.as_deref() == Some("quarkus-core")
                    && *k == ArtifactKind::Dependency)
        );
        assert!(
            project
                .artifacts
                .iter()
                .any(
                    |(a, k)| a.artifact_id.as_deref() == Some("quarkus-maven-plugin")
                        && *k == ArtifactKind::Plugin
                )
        );
    }

    #[test]
    fn parse_malformed_xml_fails() {
        assert!(parse_pom_str("not xml at all &&&").is_err());
    }

    #[test]
    fn parse_pom_rejects_oversized_file() {
        let tmp = tempfile::TempDir::new().unwrap();
        let pom_path = tmp.path().join("pom.xml");
        let content = "x".repeat((MAX_POM_SIZE + 1) as usize);
        std::fs::write(&pom_path, content).unwrap();
        let result = parse_pom(&pom_path);
        assert!(result.is_err());
        let msg = result.unwrap_err().to_string();
        assert!(msg.contains("too large"), "Expected 'too large' in: {msg}");
    }
}
