from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
from xml.etree import ElementTree


class LoadFoldersXmlError(ValueError):
    """Raised when LoadFolders.xml does not match the expected structure."""


@dataclass(frozen=True)
class LoadFolderEntry:
    path: str
    if_mod_active: str | None = None
    if_mod_not_active: str | None = None
    if_mod_active_all: tuple[str, ...] = ()
    if_mod_active_any: tuple[str, ...] = ()


@dataclass(frozen=True)
class VersionedLoadFolderEntries:
    version: str
    entries: tuple[LoadFolderEntry, ...]


@dataclass(frozen=True)
class LoadFolders:
    versions: tuple[VersionedLoadFolderEntries, ...]


def parse_load_folders_xml(xml_text: str) -> LoadFolders:
    try:
        root = ElementTree.fromstring(xml_text)
    except ElementTree.ParseError as exc:
        raise LoadFoldersXmlError(f"invalid XML: {exc}") from exc

    if root.tag not in {"loadFolders", "LoadFolders"}:
        raise LoadFoldersXmlError(
            f"expected root tag 'loadFolders', found {root.tag!r}",
        )

    versions: list[VersionedLoadFolderEntries] = []
    seen_versions: set[str] = set()
    for child in root:
        if child.tag in seen_versions:
            raise LoadFoldersXmlError(f"duplicate version tag {child.tag!r}")
        seen_versions.add(child.tag)
        entries = _parse_version_entries(child)
        if not entries:
            continue
        versions.append(
            VersionedLoadFolderEntries(version=child.tag, entries=entries),
        )

    if not versions:
        raise LoadFoldersXmlError("tag 'loadFolders' must contain at least one version")

    return LoadFolders(versions=tuple(versions))


def load_load_folders_xml(path: Path) -> LoadFolders:
    return parse_load_folders_xml(path.read_text(encoding="utf-8"))


def _parse_version_entries(element: ElementTree.Element) -> tuple[LoadFolderEntry, ...]:
    entries: list[LoadFolderEntry] = []
    for child in element:
        if child.tag != "li":
            raise LoadFoldersXmlError(
                f"version tag {element.tag!r} must only contain 'li' elements",
            )
        entry = _parse_entry(child)
        if entry is not None:
            entries.append(entry)

    return tuple(entries)


def _parse_entry(element: ElementTree.Element) -> LoadFolderEntry | None:
    if_mod_active, if_mod_not_active, if_mod_active_all, if_mod_active_any = (
        _parse_entry_conditions(element)
    )
    child_elements = list(element)
    if child_elements:
        if len(child_elements) != 1 or child_elements[0].tag != "li":
            raise LoadFoldersXmlError("load folder entries must only contain nested 'li' elements")
        path = (element.text or "").strip()
        if path:
            raise LoadFoldersXmlError(
                "load folder entries with nested items must not contain direct text",
            )
        child_entry = _parse_entry(child_elements[0])
        if child_entry is None:
            return None
        return _merge_entry_conditions(
            child_entry,
            if_mod_active=if_mod_active,
            if_mod_not_active=if_mod_not_active,
            if_mod_active_all=if_mod_active_all,
            if_mod_active_any=if_mod_active_any,
        )

    path = (element.text or "").strip()
    if not path:
        if element.attrib:
            raise LoadFoldersXmlError("load folder entries with attributes must not be empty")
        return None

    return LoadFolderEntry(
        path=path,
        if_mod_active=if_mod_active,
        if_mod_not_active=if_mod_not_active,
        if_mod_active_all=if_mod_active_all,
        if_mod_active_any=if_mod_active_any,
    )


def _parse_entry_conditions(
    element: ElementTree.Element,
) -> tuple[str | None, str | None, tuple[str, ...], tuple[str, ...]]:
    if_mod_active = _parse_optional_attr(element, "IfModActive")
    if_mod_not_active = _parse_optional_attr(element, "IfModNotActive")
    if_mod_active_all = _parse_mod_list_attr(element, "IfModActiveAll")
    if_mod_active_any = _parse_mod_list_attr(element, "IfModActiveAny")

    supported_attrs = {
        "IfModActive",
        "IfModNotActive",
        "IfModActiveAll",
        "IfModActiveAny",
    }
    unexpected_attrs = sorted(set(element.attrib) - supported_attrs)
    if unexpected_attrs:
        formatted = ", ".join(repr(attr) for attr in unexpected_attrs)
        raise LoadFoldersXmlError(f"unsupported attribute(s) on load folder entry: {formatted}")

    return if_mod_active, if_mod_not_active, if_mod_active_all, if_mod_active_any


def _merge_entry_conditions(
    entry: LoadFolderEntry,
    *,
    if_mod_active: str | None,
    if_mod_not_active: str | None,
    if_mod_active_all: tuple[str, ...],
    if_mod_active_any: tuple[str, ...],
) -> LoadFolderEntry:
    active_conditions: list[str] = []
    if if_mod_active is not None:
        active_conditions.append(if_mod_active)
    active_conditions.extend(if_mod_active_all)
    if entry.if_mod_active is not None:
        active_conditions.append(entry.if_mod_active)
    active_conditions.extend(entry.if_mod_active_all)

    merged_if_mod_active: str | None = None
    merged_if_mod_active_all: tuple[str, ...] = ()
    deduped_active_conditions = tuple(dict.fromkeys(active_conditions))
    if len(deduped_active_conditions) == 1:
        merged_if_mod_active = deduped_active_conditions[0]
    elif deduped_active_conditions:
        merged_if_mod_active_all = deduped_active_conditions

    if_mod_not_active_values = tuple(
        value
        for value in (if_mod_not_active, entry.if_mod_not_active)
        if value is not None
    )
    deduped_not_active_values = tuple(dict.fromkeys(if_mod_not_active_values))
    if len(deduped_not_active_values) > 1:
        raise LoadFoldersXmlError(
            "load folder entries cannot combine multiple 'IfModNotActive' conditions",
        )
    merged_if_mod_not_active = (
        deduped_not_active_values[0] if deduped_not_active_values else None
    )
    merged_if_mod_active_any = tuple(
        dict.fromkeys((*if_mod_active_any, *entry.if_mod_active_any))
    )

    return LoadFolderEntry(
        path=entry.path,
        if_mod_active=merged_if_mod_active,
        if_mod_not_active=merged_if_mod_not_active,
        if_mod_active_all=merged_if_mod_active_all,
        if_mod_active_any=merged_if_mod_active_any,
    )


def _parse_optional_attr(element: ElementTree.Element, name: str) -> str | None:
    value = element.get(name)
    if value is None:
        return None
    stripped = value.strip()
    if not stripped:
        raise LoadFoldersXmlError(f"attribute {name!r} must not be empty")
    return stripped


def _parse_mod_list_attr(element: ElementTree.Element, name: str) -> tuple[str, ...]:
    value = element.get(name)
    if value is None:
        return ()

    items = tuple(part.strip() for part in value.split(",") if part.strip())
    if not items:
        raise LoadFoldersXmlError(f"attribute {name!r} must contain at least one package id")
    return items
