#!/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.

"""Convert an SPDX template field into a regex aligned with clean_comments().

SPDX licence and exception JSON files ship templated forms of their text using
the SPDX template markup:

* ``<<beginOptional>> ... <<endOptional>>`` wraps an omissible span;
* ``<<var;name=..;original=..;match="RE">>`` marks a slot whose allowed
  variation is given by the ``match`` regex.

Three fields use this markup:

============================== =========================
template field                 corresponding text field
============================== =========================
standardLicenseTemplate        licenseText
standardLicenseHeaderTemplate  standardLicenseHeader
licenseExceptionTemplate       licenseExceptionText
============================== =========================

This converts the markup into a Python regex that matches the corresponding
text *after* decopy's ``clean_comments()`` normalisation, so the output can
seed a hand-written matcher in ``decopy/res.py``. The generated regex is
deliberately permissive (and large); it is a starting point to read the
required-vs-optional structure from, not a drop-in production matcher.

Run from the repository root, e.g.:

    python3 scripts/spdx_template_as_re.py tests/testdata/licenses/HPND.json
    python3 scripts/spdx_template_as_re.py tests/testdata/exceptions/\
GCC-exception-2.0.json --field licenseExceptionTemplate --validate
"""

from __future__ import annotations

import argparse
import json
import sys
import typing as t
from pathlib import Path

import regex as rex

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

from decopy import licenses  # noqa: E402

# The text field each template field describes (and a stable processing order).
TEMPLATE_FIELDS: dict[str, str] = {
    "standardLicenseTemplate": "licenseText",
    "standardLicenseHeaderTemplate": "standardLicenseHeader",
    "licenseExceptionTemplate": "licenseExceptionText",
}

# A template marker: an optional-span delimiter or a variable slot.
_TOKEN_RE = rex.compile(r"<<(beginOptional|endOptional|var;.*?)>>", rex.DOTALL)
_MATCH_RE = rex.compile(r'match="((?:[^"\\]|\\.)*)"')

# clean_comments() keeps these characters; everything else becomes whitespace.
_KEPT = r" \w'.,@:;()/+-"

# Unbounded wildcards in a slot's match expression are clamped to this many
# characters so the generated regex cannot backtrack catastrophically.
_WILDCARD_BOUND = 200


def _literal_to_regex(literal: str) -> str:
    """Turn template literal text into a clean_comments-tolerant regex."""
    cleaned = rex.sub(rf"[^{_KEPT}]", " ", literal)
    cleaned = rex.sub(r"\s+", " ", cleaned).strip()
    if not cleaned:
        return ""
    return "".join(r"\s+" if ch == " " else rex.escape(ch) for ch in cleaned)


def _var_to_regex(spec: str) -> str:
    """Turn a ``var;..;match="RE"`` slot into a bounded regex group."""
    match = _MATCH_RE.search(spec)
    expr = match.group(1) if match else ".*"
    expr = expr.replace(".*", rf".{{0,{_WILDCARD_BOUND}}}")
    expr = expr.replace(".+", rf".{{1,{_WILDCARD_BOUND}}}")
    return f"(?:{expr})"


def template_to_regex(template: str) -> str:
    """Convert SPDX template markup into a single regex string."""
    pieces: list[str] = []
    pos = 0
    for token in _TOKEN_RE.finditer(template):
        pieces.append(_literal_to_regex(template[pos : token.start()]))
        kind = token.group(1)
        if kind == "beginOptional":
            pieces.append("(?:")
        elif kind == "endOptional":
            pieces.append(")?")
        else:
            pieces.append(_var_to_regex(kind))
        pos = token.end()
    pieces.append(_literal_to_regex(template[pos:]))
    # Tolerate whitespace between every piece; clean_comments single-spaces.
    return r"\s*".join(piece for piece in pieces if piece)


def _read_json(path: Path) -> dict[str, object]:
    """Load the SPDX JSON file as an object, or exit on malformed input."""
    raw = json.loads(path.read_text(encoding="utf-8"))
    if not isinstance(raw, dict):
        sys.exit(f"{path}: not a JSON object")
    return t.cast("dict[str, object]", raw)


def _pick_template(data: dict[str, object], field: str, path: Path) -> str:
    """Return the requested template field, or exit with a helpful message."""
    template = data.get(field)
    if not isinstance(template, str) or not template:
        present = sorted(k for k in TEMPLATE_FIELDS if isinstance(data.get(k), str))
        hint = f"; template fields present: {present}" if present else ""
        sys.exit(f"{path}: no usable {field!r}{hint}")
    return template


def _validate(data: dict[str, object], field: str, regex_str: str) -> bool:
    """Check the generated regex matches the corresponding text field."""
    text_field = TEMPLATE_FIELDS[field]
    text = data.get(text_field)
    if not isinstance(text, str) or not text:
        print(f"validate: {text_field!r} absent; skipped", file=sys.stderr)
        return True
    pattern = rex.compile(regex_str, rex.IGNORECASE | rex.DOTALL)
    matched = bool(pattern.search(licenses.clean_comments(text)))
    status = "matches" if matched else "DOES NOT MATCH"
    print(f"validate: regex {status} {text_field}", file=sys.stderr)
    return matched


def main() -> None:
    parser = argparse.ArgumentParser(
        description="Convert an SPDX template field into a clean_comments-aligned "
        "regex (seed for a hand-written matcher).",
    )
    parser.add_argument("file", type=Path, help="SPDX licence/exception JSON file")
    parser.add_argument(
        "--field",
        choices=sorted(TEMPLATE_FIELDS),
        default="standardLicenseTemplate",
        help="template field to convert (default: standardLicenseTemplate)",
    )
    parser.add_argument(
        "--validate",
        action="store_true",
        help="check the regex matches the file's corresponding text field",
    )
    args = parser.parse_args()

    data = _read_json(args.file)
    template = _pick_template(data, args.field, args.file)
    regex_str = template_to_regex(template)
    print(regex_str)

    if args.validate and not _validate(data, args.field, regex_str):
        sys.exit(1)


if __name__ == "__main__":
    main()
