# Copyright (C) 2026 Wilhelmus Dingenus de Pooter
# SPDX-License-Identifier: GPL-3.0-or-later
"""
validation/s158_catalog.py
--------------------------
Reader for the **S-100 exchange-set catalogue** (``S100_ROOT/CATALOG.XML``), the
discovery-metadata side of an exchange set. The S-158 ``9000``-band checks (and the
S-101 ``0008`` extent check) cross-check a dataset against the discovery metadata
the catalogue declares for it — bounding box, support-file blocks — so this module
locates the catalogue governing a dataset and distils the parts those checks need.

Pure stdlib (ElementTree) — no GDAL, no QGIS. When a dataset ships without a
catalogue (the auditor's "loose mode", e.g. a bare ``.h5``/``.000`` picked on its
own) the loader returns ``None`` and the discovery checks simply skip, exactly like
the S-58 exchange-set checks.
"""

import os

try:
    from ..enc_xml_safe import parse_root
except (ImportError, ValueError):
    from enc_xml_safe import parse_root


# S-100 5.x exchange-catalogue + ISO 19115 geographic-extent namespaces.
_XC = "{http://www.iho.int/s100/xc/5.2}"
_GEX = "{http://standards.iso.org/iso/19115/-3/gex/1.0}"
_GCO = "{http://standards.iso.org/iso/19115/-3/gco/1.0}"


class DatasetDiscovery:
    """The discovery metadata a catalogue declares for one dataset file:
    ``bbox`` = (west, east, south, north) in WGS84 degrees, or ``None``."""
    __slots__ = ("filename", "bbox")

    def __init__(self, filename, bbox):
        self.filename = filename
        self.bbox = bbox


class ExchangeCatalogue:
    """A parsed ``CATALOG.XML``: the per-dataset discovery metadata (keyed by file
    basename, lower-cased) and the number of support-file discovery blocks."""
    __slots__ = ("path", "datasets", "support_file_count")

    def __init__(self, path, datasets, support_file_count):
        self.path = path
        self.datasets = datasets                # {basename_lower: DatasetDiscovery}
        self.support_file_count = support_file_count

    def for_file(self, dataset_path):
        """The discovery metadata for a dataset file, or ``None`` if the catalogue
        does not list it."""
        return self.datasets.get(os.path.basename(dataset_path).lower())


def find_catalog(dataset_path, max_up=6):
    """Walk up from a dataset file to the exchange-set root and return its
    ``CATALOG.XML`` path, or ``None``. The catalogue sits at ``S100_ROOT/`` while
    datasets live under ``S100_ROOT/<product>/DATASET_FILES/`` — a few levels up."""
    d = os.path.dirname(os.path.abspath(dataset_path))
    for _ in range(max_up):
        cand = os.path.join(d, "CATALOG.XML")
        if os.path.isfile(cand):
            return cand
        parent = os.path.dirname(d)
        if parent == d:
            break
        d = parent
    return None


def _bbox(dm):
    bb = dm.find(_XC + "boundingBox")
    if bb is None:
        return None

    def dec(tag):
        el = bb.find(_GEX + tag)
        if el is None:
            return None
        d = el.find(_GCO + "Decimal")
        try:
            return float(d.text) if d is not None and d.text else None
        except (TypeError, ValueError):
            return None

    vals = (dec("westBoundLongitude"), dec("eastBoundLongitude"),
            dec("southBoundLatitude"), dec("northBoundLatitude"))
    return vals if all(v is not None for v in vals) else None


def parse_catalog(path):
    """Parse a ``CATALOG.XML`` into an :class:`ExchangeCatalogue`, or ``None`` if it
    cannot be read."""
    try:
        root = parse_root(path)
    except Exception:                           # noqa: BLE001 — malformed/absent
        return None
    datasets = {}
    for dm in root.iter(_XC + "S100_DatasetDiscoveryMetadata"):
        fn = dm.findtext(_XC + "fileName") or ""
        base = os.path.basename(fn.replace("\\", "/")).lower()
        if base:
            datasets[base] = DatasetDiscovery(fn, _bbox(dm))
    sup = root.find(_XC + "supportFileDiscoveryMetadata")
    n_support = 0
    if sup is not None:
        n_support = sum(1 for _ in sup.iter(_XC + "S100_SupportFileDiscoveryMetadata"))
    return ExchangeCatalogue(path, datasets, n_support)


def load_for(dataset_path):
    """Locate and parse the exchange-set catalogue governing ``dataset_path``, or
    ``None`` in loose mode (no catalogue found / unparseable)."""
    cat = find_catalog(dataset_path)
    return parse_catalog(cat) if cat else None
