# Copyright (C) 2026 Wilhelmus Dingenus de Pooter
# SPDX-License-Identifier: GPL-3.0-or-later
"""
validation/checks_exchange.py
-----------------------------
Exchange-set / catalogue scrutiny — checks that look across the whole exchange
set rather than one cell. They read ``dataset.exchange`` (an ``s57.ExchangeSet``
built from CATALOG.0xx and the files present) and no-op gracefully when it's
absent.

Rules follow S-57 App B.1 §5–§6 (exchange set, file naming, CRC, CATD), encoded
as rules with our own messages.

Citations are provisional ``EXCH.*`` tokens (the stable identity is the
plain-language name); reconcile to the S-58 numbers when available. The CRC
check is additionally flagged provisional until validated on a real set.
"""

from .engine import check, Severity, Finding
try:                                                    # plugin context
    from ..s57 import crc_matches
except (ImportError, ValueError):                       # headless / top-level
    from s57 import crc_matches


def _exch(ds):
    return getattr(ds, "exchange", None)


@check("EXCH.CATALOG", Severity.CRITICAL, "Exchange set",
       "The exchange set has no readable CATALOG.0xx file.",
       "Include exactly one readable CATALOG.0xx in ENC_ROOT.",
       "S-57 App B.1 5.4.1 / 6.2",
       name="exchange-set/catalogue-missing-or-unreadable")
def catalogue_present(ds, cat, geom):
    es = _exch(ds)
    if es is None:
        return
    if not es.has_catalog():
        yield Finding(_meta("EXCH.CATALOG"), es.catalog_name or "(set)",
                      "CATALOG", None,
                      detail=es.catalog_error or "no CATD entries")


@check("EXCH.LISTED.MISSING", Severity.CRITICAL, "Exchange set",
       "A file listed in the catalogue is not present in the exchange set.",
       "Add the missing file or remove its catalogue entry.",
       "S-57 App B.1 6.2.2 (FILE)",
       name="exchange-set/listed-file-missing")
def listed_file_present(ds, cat, geom):
    es = _exch(ds)
    if es is None or not es.has_catalog():
        return
    for e in es.entries:
        if not e.basename:
            continue
        if e.basename not in es.present:
            yield Finding(_meta("EXCH.LISTED.MISSING"), e.basename, "CATD",
                          e.rcid, detail=f"FILE={e.file}")


@check("EXCH.CELL.UNLISTED", Severity.ERROR, "Exchange set",
       "A base cell present in the set is not listed in the catalogue.",
       "Add the cell to CATALOG.0xx (or remove the stray file).",
       "S-57 App B.1 6.2",
       name="exchange-set/cell-not-in-catalogue")
def cell_listed(ds, cat, geom):
    es = _exch(ds)
    if es is None or not es.has_catalog():
        return
    listed = {e.basename for e in es.entries}
    for cell in sorted(es.present_cells()):
        if cell not in listed:
            yield Finding(_meta("EXCH.CELL.UNLISTED"), cell, "FILE", None,
                          detail="present but not catalogued")


@check("EXCH.IMPL", Severity.ERROR, "Exchange set",
       "A data-set file's catalogue implementation (IMPL) is not BIN.",
       "Set IMPL=BIN for S-57 data-set files.",
       "S-57 App B.1 5.1",
       name="exchange-set/dataset-implementation-not-bin")
def impl_bin(ds, cat, geom):
    es = _exch(ds)
    if es is None or not es.has_catalog():
        return
    for e in es.entries:
        if e.is_cell and e.impl and e.impl != "BIN":
            yield Finding(_meta("EXCH.IMPL"), e.basename, "CATD", e.rcid,
                          detail=f"IMPL={e.impl}")


@check("EXCH.BOUNDS", Severity.WARNING, "Exchange set",
       "A data-set catalogue entry is missing a geographic bound "
       "(SLAT/WLON/NLAT/ELON).",
       "Populate the four bounds for data-set files.",
       "S-57 App B.1 6.2.2",
       name="exchange-set/catalogue-bounds-missing")
def bounds_present(ds, cat, geom):
    es = _exch(ds)
    if es is None or not es.has_catalog():
        return
    for e in es.entries:
        if not e.is_cell:
            continue
        missing = [k for k, v in (("SLAT", e.slat), ("WLON", e.wlon),
                                  ("NLAT", e.nlat), ("ELON", e.elon))
                   if v in (None, "")]
        if missing:
            yield Finding(_meta("EXCH.BOUNDS"), e.basename, "CATD", e.rcid,
                          detail="missing " + ",".join(missing))


@check("EXCH.NAMING", Severity.WARNING, "Exchange set",
       "A cell file name does not follow the S-57 8-character naming "
       "(CCPXXXXX.EEE).",
       "Rename to the producer/purpose/cell-code convention.",
       "S-57 App B.1 5.6.3",
       name="exchange-set/cell-name-nonconforming")
def cell_naming(ds, cat, geom):
    es = _exch(ds)
    if es is None:
        return
    import re
    ok = re.compile(r"(?i)^[A-Z0-9]{2}[1-8][A-Z0-9]{5}\.\d{3}$")
    # any present file that looks like a cell (8 chars + numeric ext) but not
    # the stricter producer/purpose/code form
    loose = re.compile(r"(?i)^[A-Z0-9]{8}\.\d{3}$")
    for base in sorted(es.present):
        if loose.match(base) and not ok.match(base):
            yield Finding(_meta("EXCH.NAMING"), base, "FILE", None,
                          detail="nonconforming cell name")


@check("EXCH.CRC", Severity.ERROR, "Exchange set",
       "A data-set file's CRC does not match its catalogue CRCS value.",
       "Recompute the CRC or correct the file/catalogue.",
       "S-57 App B.1 5.9 (provisional — verify CRC convention on real sets)",
       name="exchange-set/file-crc-mismatch")
def crc_check(ds, cat, geom):
    es = _exch(ds)
    if es is None or not es.has_catalog():
        return
    for e in es.entries:
        if not e.is_cell or not e.crcs:
            continue                                    # README/catalogue: no CRC
        data = es.file_bytes(e.basename)
        if data is None:
            continue                                    # missing file: other check
        result = crc_matches(data, e.crcs)
        if result is False:
            yield Finding(_meta("EXCH.CRC"), e.basename, "CATD", e.rcid,
                          detail=f"CRCS={e.crcs}")


# ---------------------------------------------------------------------------
def _meta(number):
    from .engine import REGISTRY, CheckMeta, Severity as _S
    for m in REGISTRY:
        if m.number == number:
            return m
    return CheckMeta(number, _S.ERROR, "", "", "", "", None)
