#!/usr/bin/env python3
# Copyright: 2026, Maximiliano Curia <maxy@debian.org>
#
# License: ISC
#  Permission to use, copy, modify, and/or distribute this software for any
#  purpose with or without fee is hereby granted, provided that the above
#  copyright notice and this permission notice appear in all copies.
#  .
#  THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
#  REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
#  AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
#  INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
#  LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
#  OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
#  PERFORMANCE OF THIS SOFTWARE.

"""Regenerate the SPDX name/exception round-trip tables from the conversions.

For every SPDX licence (and, with --exceptions, every SPDX exception) shipped
in the testdata, run the full conversion loop and record it:

    licence:    SPDX id  --replace_spdx-->  DEP-5 name  --dep5_to_spdx-->  id'
    exception:  SPDX id  --exception_to_dep5-->  label  --exception_to_spdx-->  id'

The tables (tests/testdata/spdx-name-roundtrip.tsv,
tests/testdata/spdx-exception-roundtrip.tsv) lock both conversion directions so
neither a refreshed SPDX data drop nor a conversion change can shift coverage
silently. The status column is auto-classified but PRESERVED across runs once
present, so a manual `wrong:<note>` annotation survives a regenerate (mirroring
audit-license-coverage.py).

Run from the repository root:

    python3 scripts/audit-spdx-roundtrip.py              # licences
    python3 scripts/audit-spdx-roundtrip.py --exceptions # exceptions
"""

from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))

from decopy.licensing import spdx  # noqa: E402

LICENSES_DIR = ROOT / "tests" / "testdata" / "licenses"
EXCEPTIONS_DIR = ROOT / "tests" / "testdata" / "exceptions"
NAME_TSV = ROOT / "tests" / "testdata" / "spdx-name-roundtrip.tsv"
EXCEPTION_TSV = ROOT / "tests" / "testdata" / "spdx-exception-roundtrip.tsv"
_STATUS_COL = 3  # fourth column (id, dep5, back, status)

NAME_HEADER = (
    "# SPDX licence name round-trip (regenerate with "
    "scripts/audit-spdx-roundtrip.py).\n"
    "# Columns: spdx_id <TAB> dep5_name <TAB> spdx_back <TAB> status\n"
    "# dep5_name = replace_spdx(spdx_id); spdx_back = dep5_to_spdx(dep5_name).\n"
    "# status: identity = spdx_back == spdx_id (decopy maps and round-trips);\n"
    "#         passthrough = decopy does not special-case it (dep5_name == id);\n"
    "#         deprecated = a deprecated SPDX id that collapses to its canonical\n"
    "#         form; wrong:<note> = a conversion asymmetry/defect to fix.\n"
)

EXCEPTION_HEADER = (
    "# SPDX exception round-trip (regenerate with "
    "scripts/audit-spdx-roundtrip.py --exceptions).\n"
    "# Columns: spdx_exception_id <TAB> dep5_label <TAB> spdx_back <TAB> status\n"
    "# dep5_label = exception_to_dep5(id); spdx_back = exception_to_spdx(label).\n"
    "# status: identity = spdx_back == id; collapse = many SPDX ids share one\n"
    "#         label so back is the pinned representative; unmapped = no label.\n"
)


def classify_name(spdx_id: str, dep5_name: str, back: str, *, deprecated: bool) -> str:
    if back == spdx_id:
        return "passthrough" if dep5_name == spdx_id else "identity"
    if deprecated:
        return "deprecated"
    return "wrong"


def classify_exception(spdx_id: str, label: str | None, back: str | None) -> str:
    if label is None:
        return "unmapped"
    return "identity" if back == spdx_id else "collapse"


def _load_status(tsv: Path) -> dict[str, str]:
    """Preserve only *annotated* statuses across regenerations.

    A status carrying a note (e.g. ``wrong:accepted <reason>``) is a human
    decision and is kept verbatim. A bare auto-status (``identity`` / ``wrong``
    / ...) is re-derived from the current conversions, so a row that starts
    round-tripping after a fix flips automatically instead of staying stale.
    """
    prev: dict[str, str] = {}
    if not tsv.exists():
        return prev
    for line in tsv.read_text().splitlines():
        if line.startswith("#") or not line.strip():
            continue
        cols = line.split("\t")
        if len(cols) > _STATUS_COL and ":" in cols[_STATUS_COL]:
            prev[cols[0]] = cols[_STATUS_COL]
    return prev


def _write(tsv: Path, header: str, rows: list[tuple[str, str, str, str]]) -> None:
    lines = [header]
    lines.extend(f"{a}\t{b}\t{c}\t{d}\n" for a, b, c, d in rows)
    tsv.write_text("".join(lines))


def audit_names() -> None:
    prev = _load_status(NAME_TSV)
    rows: list[tuple[str, str, str, str]] = []
    for path in sorted(LICENSES_DIR.glob("*.json")):
        spdx_id = path.stem
        deprecated = bool(json.loads(path.read_text()).get("isDeprecatedLicenseId"))
        dep5_name = spdx.replace_spdx(spdx_id)
        back = spdx.dep5_to_spdx(dep5_name)
        status = prev.get(spdx_id) or classify_name(
            spdx_id, dep5_name, back, deprecated=deprecated
        )
        rows.append((spdx_id, dep5_name, back, status))
    _write(NAME_TSV, NAME_HEADER, rows)
    _report(rows)


def audit_exceptions() -> None:
    prev = _load_status(EXCEPTION_TSV)
    rows: list[tuple[str, str, str, str]] = []
    for path in sorted(EXCEPTIONS_DIR.glob("*.json")):
        spdx_id = path.stem
        label = spdx.exception_to_dep5(spdx_id)
        back = spdx.exception_to_spdx(label) if label is not None else None
        status = prev.get(spdx_id) or classify_exception(spdx_id, label, back)
        rows.append((spdx_id, label or "", back or "", status))
    _write(EXCEPTION_TSV, EXCEPTION_HEADER, rows)
    _report(rows)


def _report(rows: list[tuple[str, str, str, str]]) -> None:
    counts: dict[str, int] = {}
    for *_, status in rows:
        key = status.split(":", 1)[0]
        counts[key] = counts.get(key, 0) + 1
    summary = ", ".join(f"{k}={v}" for k, v in sorted(counts.items()))
    print(f"{len(rows)} rows: {summary}")


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        "--exceptions",
        action="store_true",
        help="audit SPDX exceptions instead of licences",
    )
    args = parser.parse_args()
    if args.exceptions:
        audit_exceptions()
    else:
        audit_names()


if __name__ == "__main__":
    main()
