"""
Maven POM (Project Object Model) parsing and handling.
"""

from __future__ import annotations

from pathlib import Path
from typing import Any
from xml.etree import ElementTree

from ..parse import Coordinate


def _text(el: ElementTree.Element, path: str | None = None) -> str | None:
    """Get/find element text and strip whitespace."""
    text = el.text if path is None else el.findtext(path)
    return None if text is None else text.strip()


def _text_or_die(el: ElementTree.Element, path: str) -> str:
    text = _text(el, path)
    if text is None:
        raise RuntimeError("{path} element not found")
    return text


def _groupId(el: ElementTree.Element) -> str:
    return _text_or_die(el, "groupId")


def _artifactId(el: ElementTree.Element) -> str:
    return _text_or_die(el, "artifactId")


def parse_dependency_element_to_coordinate(
    el: ElementTree.Element,
) -> tuple[Coordinate, list[tuple[str, str]]]:
    """
    Parse a <dependency> XML element into a Coordinate plus exclusions data.

    This function bridges the XML parsing layer (pom.py) and the data layer (endpoint.py),
    allowing core.py to work with Coordinates without touching ElementTree.

    Args:
        el: The XML element to parse.

    Returns:
        Tuple of (Coordinate object, list of exclusion (groupId, artifactId) tuples)
    """
    groupId = _groupId(el)
    artifactId = _artifactId(el)
    version = _text(el, "version")  # NB: Might be None, which means managed.
    classifier = _text(el, "classifier")
    packaging = _text(el, "type")
    scope = _text(el, "scope")
    optional = _text(el, "optional") == "true"

    # Parse exclusions as list of (groupId, artifactId) tuples
    exclusions = [
        (_groupId(ex), _artifactId(ex)) for ex in el.findall("exclusions/exclusion")
    ]

    coord = Coordinate(
        groupId=groupId,
        artifactId=artifactId,
        version=version,
        classifier=classifier,
        packaging=packaging,
        scope=scope,
        optional=optional,
    )

    return coord, exclusions


class XML:
    """Base class for XML document wrappers."""

    def __init__(self, source: Path | str):
        self.source = source
        self.tree = (
            ElementTree.ElementTree(ElementTree.fromstring(source))
            if isinstance(source, str)
            else ElementTree.parse(source)
        )
        XML._strip_ns(self.tree.getroot())

    def dump(self, el: ElementTree.Element | None = None) -> str:
        """
        Get a string representation of the given XML element.

        Args:
            el: Element to stringify, or None to stringify the root node.

        Returns:
            The XML as a string, empty string if no element is given and the tree has no root.
        """
        # NB: Be careful: childless ElementTree.Element objects are falsy!
        if el is None:
            el = self.tree.getroot()
        return "" if el is None else ElementTree.tostring(el).decode()

    def elements(self, path: str) -> list[ElementTree.Element]:
        return self.tree.findall(path)

    def element(self, path: str) -> ElementTree.Element | None:
        els = self.elements(path)
        assert len(els) <= 1
        return els[0] if els else None

    def values(self, path: str) -> list[str | None]:
        return [_text(el) for el in self.elements(path)]

    def value(self, path: str) -> str | None:
        el = self.element(path)
        # NB: Be careful: childless ElementTree.Element objects are falsy!
        return None if el is None else _text(el)

    @staticmethod
    def _strip_ns(el: ElementTree.Element | None) -> None:
        """
        Remove namespace prefixes from element tags.

        This makes XPath queries simpler (no need to handle namespaces),
        but preserves namespace prefixes in attributes so that the XML
        remains valid when written back out.

        Credit: https://stackoverflow.com/a/32552776/1207769
        """
        if el is None:
            return
        if el.tag.startswith("{"):
            el.tag = el.tag[el.tag.find("}") + 1 :]
        # Note: We intentionally do NOT strip namespace prefixes from attributes.
        # Attributes like xsi:schemaLocation need to keep their namespace prefix
        # for the XML to remain valid when written back out.
        for child in el:
            XML._strip_ns(child)


class POM(XML):
    """
    Convenience wrapper around a Maven POM XML document.
    """

    @property
    def groupId(self) -> str | None:
        """The POM's <groupId> (or <parent><groupId>) value."""
        return self.value("groupId") or self.value("parent/groupId")

    @property
    def artifactId(self) -> str | None:
        """The POM's <artifactId> value."""
        return self.value("artifactId")

    @property
    def version(self) -> str | None:
        """The POM's <version> (or <parent><version>) value."""
        return self.value("version") or self.value("parent/version")

    @property
    def name(self) -> str | None:
        """The POM's <name> value."""
        return self.value("name")

    @property
    def description(self) -> str | None:
        """The POM's <description> value."""
        return self.value("description")

    @property
    def scmURL(self) -> str | None:
        """The POM's <scm><url> value."""
        return self.value("scm/url")

    @property
    def issuesURL(self) -> str | None:
        """The POM's <issueManagement><url> value."""
        return self.value("issueManagement/url")

    @property
    def ciURL(self) -> str | None:
        """The POM's <ciManagement><url> value."""
        return self.value("ciManagement/url")

    @property
    def developers(self) -> list[dict[str, Any]]:
        """Dictionary of the POM's <developer> entries."""
        return self._people("developers/developer")

    @property
    def contributors(self) -> list[dict[str, Any]]:
        """Dictionary of the POM's <contributor> entries."""
        return self._people("contributors/contributor")

    def _people(self, path: str) -> list[dict[str, Any]]:
        people = []
        for el in self.elements(path):
            person: dict[str, Any] = {}
            for child in el:
                if len(child) == 0:
                    person[child.tag] = _text(child)
                else:
                    if child.tag == "properties":
                        for grand in child:
                            person[grand.tag] = _text(grand)
                    else:
                        person[child.tag] = [_text(grand) for grand in child]
            people.append(person)
        return people

    @property
    def properties(self) -> dict[str, str]:
        """Dictionary of key/value pairs from the POM's <properties>."""
        return {el.tag: _text(el) or "" for el in self.elements("properties/*")}


def write_pom(pom: POM, dest: Path | str) -> None:
    Path(dest).write_text(pom.dump())


def write_temp_pom(pom: POM) -> Path:
    """
    Write a POM object to a temporary file.

    Args:
        pom: POM object to write

    Returns:
        Path to the generated temporary POM file
    """
    import tempfile

    temp_file = Path(tempfile.mktemp(suffix=".pom.xml", prefix="jgo-"))
    write_pom(pom, temp_file)

    return temp_file
