# Copyright (C) 2026 Wilhelmus Dingenus de Pooter
# SPDX-License-Identifier: GPL-3.0-or-later
"""
validation/s158_report.py
-------------------------
Render an :class:`validation.s158.S158Audit` as a self-contained HTML document,
reusing the exact structure and CSS of the S-58 ENC audit report
(``validation.engine.Report.to_html``) so the two look identical.

Per S-100 Tier-1 product found in the source it shows **every** official S-158
check grouped by severity, each with a PASS / FAIL / NOT-IMPLEMENTED status and,
for failures, the offending datasets/elements — so the report always lists all
tests available for the loaded products, not just the ones that fired.
"""

import html
import os
from datetime import datetime, timezone

from . import s100
from . import s101
from .engine import SIGNATURE_HTML


_PRODUCT_LABEL = {
    "100": "S-100 core / Universal Model", "101": "S-101 ENC (vector)",
    "102": "S-102 bathymetric surface",
    "104": "S-104 water level", "111": "S-111 surface currents",
}
_SEV_LABEL = {"C": "Critical", "E": "Error", "W": "Warning"}
_SEV_COLOUR = {"C": "#c0392b", "E": "#d68910", "W": "#2471a3"}
_SEV_ORDER = ("C", "E", "W")
_STATUS_COLOUR = {"FAIL": "#c0392b", "PASS": "#1e8449", "N/I": "#999"}
_STATUS_LABEL = {"FAIL": "FAIL", "PASS": "PASS", "N/I": "N/I"}


def _aggregate(product, reports):
    """Aggregate a product's per-dataset reports: union of executed checks and
    findings keyed by check id (carrying the dataset label). Returns
    (ran_ids, fails{cid: [(label, finding)]}, n_datasets, errors)."""
    ran, fails, errors = set(), {}, []
    for rep in reports:
        ran |= rep.ran_ids
        if rep.error:
            errors.append((rep.path, rep.error))
        label = "" if product == "101" else os.path.basename(rep.path)
        for f in rep.findings:
            fails.setdefault(f.check_id, []).append((label, f))
    return ran, fails, len(reports), errors


def _status_of(cid, ran, fails):
    if cid in fails:
        return "FAIL"
    if cid in ran:
        return "PASS"
    return "N/I"


def _product_section(product, reports, esc):
    cat = s100.catalogue(product)
    ran, fails, n_ds, errors = _aggregate(product, reports)
    if product == "101":
        impl, total = s101.coverage()
    elif product == "100":
        impl, total = s101.core_coverage()
    else:
        impl, total = s100.coverage(product)

    # rows by severity, EVERY catalogue check listed (status per check)
    by_sev = {s: [] for s in _SEV_ORDER}
    n_pass = n_fail = n_ni = n_unval = 0
    n_crit_fail = 0
    for rec in cat:
        cid = rec.get("id")
        sev = s100.SEVERITY.get(rec.get("cls", ""), "W")
        status = _status_of(cid, ran, fails)
        if status == "PASS":
            n_pass += 1
        elif status == "FAIL":
            n_fail += 1
            if sev == "C":
                n_crit_fail += 1
        else:
            n_ni += 1
        # implemented-but-not-yet-validated → provisional ('*')
        if status in ("PASS", "FAIL") and not s100.is_validated(cid):
            n_unval += 1
        by_sev.setdefault(sev, []).append((rec, status))

    passed = (n_crit_fail == 0)
    banner_bg = "#1e8449" if passed else "#c0392b"
    plabel = _PRODUCT_LABEL.get(product, product)
    ds_word = ("%d cell dataset" % n_ds if product in ("100", "101")
               else "%d file(s)" % n_ds)

    out = [
        f'<h2 class="prodhdr">S-158-{esc(product)} &mdash; {esc(plabel)} '
        f'<span class="muted">({esc(ds_word)})</span></h2>',
        f'<div class="banner" style="background:{banner_bg}">'
        f'{"PASS" if passed else "FAIL"} &mdash; minimum standard '
        f'{"met" if passed else "NOT met"} ({n_crit_fail} critical finding(s))'
        '</div>',
        '<div class="tiles">'
        f'<div class="tile"><div class="n">{len(cat)}</div>'
        '<div class="l">checks in S-158</div></div>'
        f'<div class="tile"><div class="n">{impl}</div>'
        '<div class="l">implemented</div></div>'
        f'<div class="tile"><div class="n" style="color:#1e8449">{n_pass}</div>'
        '<div class="l">passed</div></div>'
        f'<div class="tile"><div class="n" style="color:#c0392b">{n_fail}</div>'
        '<div class="l">failed</div></div>'
        f'<div class="tile"><div class="n" style="color:#999">{n_ni}</div>'
        '<div class="l">not implemented</div></div>'
        '</div>',
    ]
    if n_unval:
        out.append(
            '<div class="unval" style="background:#fff8e1;border:1px solid '
            '#ffe082;color:#5d4037"><b>* Provisional:</b> '
            f'{n_unval} implemented check(s) are marked with an asterisk (*) '
            'because they are not yet validated against an official IHO S-158 '
            'test dataset. They run on every audit; treat their results as '
            'provisional pending that validation.</div>')
    if errors:
        lis = "".join(f"<li>{esc(os.path.basename(p))}: {esc(str(e))}</li>"
                      for p, e in errors)
        out.append('<div class="unval"><b>Could not read:</b>'
                   f'<ul>{lis}</ul></div>')

    for sev in _SEV_ORDER:
        rows = by_sev.get(sev) or []
        if not rows:
            continue
        n_f = sum(1 for _rec, st in rows if st == "FAIL")
        out.append(
            f'<h3 style="color:{_SEV_COLOUR[sev]}">{_SEV_LABEL[sev]} checks '
            f'<span class="muted">({len(rows)} in S-158, {n_f} failed)</span>'
            '</h3>')
        out.append('<table class="checks"><thead><tr><th>Status</th>'
                   '<th>Check</th><th>S-158</th><th>Findings</th></tr>'
                   '</thead><tbody>')
        for rec, status in rows:
            cid = rec.get("id")
            hits = fails.get(cid, [])
            sc = _STATUS_COLOUR.get(status, "#000")
            title = (rec.get("message", "") or "").split("(", 1)[0].strip() \
                or cid
            star = ("" if status == "N/I" or s100.is_validated(cid) else " *")
            out.append(
                f'<tr class="r-{status.lower().replace("/", "")}">'
                f'<td><span class="badge" style="background:{sc}">'
                f'{_STATUS_LABEL[status]}</span></td>'
                f'<td><div class="cname">{esc(title)}{star} '
                f'<span class="muted">{esc(cid)}</span></div>'
                f'<div class="cmsg">{esc(rec.get("message", ""))}</div></td>'
                f'<td class="num">{esc(cid)}</td>'
                f'<td class="num">{len(hits)}</td></tr>')
            if hits:
                items = []
                for label, f in hits[:200]:
                    loc = esc(label + " · " if label else "") + esc(f.location)
                    det = f" &mdash; {esc(f.detail)}" if f.detail else ""
                    items.append(f"<li>{loc}{det}</li>")
                more = (f'<li class="muted">… and {len(hits) - 200} more</li>'
                        if len(hits) > 200 else "")
                out.append(
                    '<tr class="detail"><td></td><td colspan="3">'
                    f'<ul>{"".join(items)}{more}</ul>'
                    f'<div class="soln"><b>Solution:</b> '
                    f'{esc(rec.get("solution") or "—")}</div></td></tr>')
        out.append('</tbody></table>')
    return "".join(out), passed, n_fail


def audit_to_html(audit, source="", generated=None, title="S-100 validation report (S-158)"):
    """Full HTML document for a whole-source S-158 audit, styled identically to
    the ENC audit report."""
    esc = html.escape
    gen = (generated or datetime.now(timezone.utc)).strftime("%Y-%m-%d %H:%M UTC")
    products = audit.products()
    sections, all_pass, total_fail = [], True, 0
    for product in products:
        sec, passed, n_fail = _product_section(product, audit.reports[product], esc)
        sections.append(sec)
        all_pass = all_pass and passed
        total_fail += n_fail

    prod_summary = ", ".join(
        f"S-158-{p} ({len(audit.reports[p])})" for p in products) or "&mdash;"
    banner_bg = "#1e8449" if all_pass else "#c0392b"
    return f"""<!DOCTYPE html>
<html lang="en"><head><meta charset="utf-8">
<title>{esc(title)}</title>
<style>
 body{{font:14px/1.5 -apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;
   color:#1a1a1a;max-width:1000px;margin:24px auto;padding:0 18px}}
 h1{{font-size:22px;margin:0 0 4px}}
 h2.prodhdr{{font-size:18px;margin:28px 0 6px;border-bottom:2px solid #ddd;
   padding-bottom:4px}}
 h3{{font-size:15px;margin:18px 0 6px;border-bottom:1px solid #eee;
   padding-bottom:4px}}
 .banner{{color:#fff;padding:10px 14px;border-radius:6px;font-weight:600;
   margin:14px 0}}
 .meta{{color:#444;font-size:13px;margin:8px 0 4px}}
 .meta b{{color:#1a1a1a}}
 .unval{{background:#fdecea;border:1px solid #f5c6c0;border-radius:6px;
   padding:8px 14px;margin:12px 0;font-size:13px;color:#7a271a}}
 .tiles{{display:flex;gap:10px;flex-wrap:wrap;margin:12px 0 6px}}
 .tile{{border:1px solid #ddd;border-radius:6px;padding:8px 14px;min-width:90px}}
 .tile .n{{font-size:20px;font-weight:700}}
 .tile .l{{font-size:12px;color:#666}}
 table.checks{{border-collapse:collapse;width:100%;margin:4px 0 8px}}
 table.checks th{{text-align:left;font-size:12px;color:#666;border-bottom:
   1px solid #ddd;padding:4px 8px}}
 table.checks td{{padding:5px 8px;vertical-align:top;border-bottom:1px solid #f2f2f2}}
 td.num{{text-align:right;font-variant-numeric:tabular-nums;white-space:nowrap}}
 .badge{{color:#fff;font-size:11px;font-weight:700;border-radius:4px;
   padding:2px 7px;letter-spacing:.3px}}
 .cname{{font-weight:600}}
 .cmsg{{color:#555;font-size:12px}}
 tr.detail td{{background:#fafafa}}
 tr.detail ul{{margin:4px 0;padding-left:18px}}
 tr.detail li{{font-size:12px;color:#333}}
 .soln{{font-size:12px;color:#444;margin-top:4px}}
 .muted{{color:#999;font-weight:400}}
 footer{{margin:24px 0 8px;color:#999;font-size:11px;border-top:1px solid #eee;
   padding-top:8px}}
</style></head><body>
<h1>{esc(title)}</h1>
<div class="banner" style="background:{banner_bg}">{"PASS" if all_pass else "FAIL"}
 — {total_fail} check(s) failed across {len(products)} product(s)</div>
<div class="meta"><b>Source:</b> {esc(source) or "&mdash;"}</div>
<div class="meta"><b>Products:</b> {prod_summary}</div>
<div class="meta"><b>Generated:</b> {gen}</div>
{"".join(sections)}
<footer>Generated by ENCoreGIS Auditor against the IHO S-158 validation-check
 catalogues. Every S-158 check for each loaded product is listed with its status;
 NOT-IMPLEMENTED means the check is catalogued but its executable predicate is not
 yet available. S-101 is read with the native ISO 8211 engine; S-102/104/111 via
 GDAL.</footer>
{SIGNATURE_HTML}
</body></html>"""
