#!/usr/bin/env python3
"""Validate one or more durable omp-spt per-target release evidence records."""

from __future__ import annotations

import argparse
from datetime import datetime
from pathlib import Path
import json
import re
import sys

try:
    import tomllib as toml
except ModuleNotFoundError:  # pragma: no cover - Python <= 3.10
    try:
        import tomli as toml  # type: ignore[no-redef]
    except ModuleNotFoundError:
        print("MISSING-DEP: Python 3.11+ or tomli is required", file=sys.stderr)
        raise SystemExit(2)

try:
    import jsonschema
except ModuleNotFoundError:  # pragma: no cover - environment dependency
    print("MISSING-DEP: jsonschema is required (`pip install jsonschema`)", file=sys.stderr)
    raise SystemExit(2)


def location(error: jsonschema.ValidationError) -> str:
    return "/".join(str(part) for part in error.absolute_path) or "(root)"


def load_json(path: Path) -> object:
    with path.open("r", encoding="utf-8") as stream:
        return json.load(stream)

RFC3339 = re.compile(
    r"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}"
    r"(?:\.[0-9]+)?(?:Z|[+-][0-9]{2}:[0-9]{2})$"
)
SEMVER = re.compile(
    r"^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)"
    r"(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?"
    r"(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$"
)
MINIMUM_OMP_VERSION = "16.3.15"
SUPPORTED_TARGETS = frozenset(
    {
        "x86_64-pc-windows-msvc",
        "x86_64-unknown-linux-gnu",
        "x86_64-unknown-linux-musl",
    }
)


def valid_rfc3339(value: str) -> bool:
    if not RFC3339.fullmatch(value):
        return False
    try:
        parsed = datetime.fromisoformat(value[:-1] + "+00:00" if value.endswith("Z") else value)
    except ValueError:
        return False
    return parsed.tzinfo is not None


def semver_precedence(version: str) -> tuple[tuple[int, int, int], tuple[str, ...] | None]:
    match = SEMVER.fullmatch(version)
    if match is None:
        raise ValueError(f"not canonical SemVer: {version!r}")
    prerelease = tuple(match.group(4).split(".")) if match.group(4) is not None else None
    return (tuple(int(match.group(index)) for index in range(1, 4)), prerelease)


def compare_semver(left: str, right: str) -> int:
    left_core, left_pre = semver_precedence(left)
    right_core, right_pre = semver_precedence(right)
    if left_core != right_core:
        return 1 if left_core > right_core else -1
    if left_pre is None or right_pre is None:
        if left_pre is right_pre:
            return 0
        return 1 if left_pre is None else -1
    for left_id, right_id in zip(left_pre, right_pre):
        if left_id == right_id:
            continue
        left_numeric = left_id.isdigit()
        right_numeric = right_id.isdigit()
        if left_numeric and right_numeric:
            return 1 if int(left_id) > int(right_id) else -1
        if left_numeric != right_numeric:
            return -1 if left_numeric else 1
        return 1 if left_id > right_id else -1
    if len(left_pre) == len(right_pre):
        return 0
    return 1 if len(left_pre) > len(right_pre) else -1


# [impl->REQ-DIST-RELEASE-EVIDENCE]
def validate_records(schema_path: Path, manifest_path: Path, evidence_paths: list[Path]) -> list[str]:
    errors: list[str] = []
    try:
        schema = load_json(schema_path)
        validator_class = jsonschema.validators.validator_for(schema)
        validator_class.check_schema(schema)
        validator = validator_class(schema, format_checker=jsonschema.FormatChecker())
    except (OSError, json.JSONDecodeError, jsonschema.SchemaError) as exc:
        return [f"{schema_path}: invalid schema: {exc}"]

    try:
        with manifest_path.open("rb") as stream:
            adapter = toml.load(stream)["adapter"]
            canonical_version = adapter["version"]
            minimum_spt_core_version = adapter["min_spt_core_version"]
    except (OSError, KeyError, TypeError, toml.TOMLDecodeError) as exc:
        return [f"{manifest_path}: cannot read canonical adapter release fields: {exc}"]
    try:
        semver_precedence(minimum_spt_core_version)
    except (TypeError, ValueError) as exc:
        return [f"{manifest_path}: adapter.min_spt_core_version is invalid: {exc}"]

    valid_records: list[tuple[Path, dict]] = []
    for path in evidence_paths:
        try:
            record = load_json(path)
        except (OSError, json.JSONDecodeError) as exc:
            errors.append(f"{path}: invalid JSON: {exc}")
            continue
        schema_errors = sorted(validator.iter_errors(record), key=lambda item: list(item.absolute_path))
        if schema_errors:
            for error in schema_errors:
                errors.append(f"{path}: {location(error)}: {error.message}")
            continue
        assert isinstance(record, dict)
        if not valid_rfc3339(record["tested_at"]):
            errors.append(f"{path}: tested_at must be a valid RFC 3339 date-time")
        release = record["release"]
        if release["version"] != canonical_version:
            errors.append(
                f"{path}: release/version {release['version']!r} does not match canonical manifest version {canonical_version!r}"
            )
        if release["tag"] != f"v{release['version']}":
            errors.append(f"{path}: release/tag {release['tag']!r} must equal v{release['version']}")
        environment = record["environment"]
        if compare_semver(environment["omp_version"], MINIMUM_OMP_VERSION) < 0:
            errors.append(
                f"{path}: environment/omp_version {environment['omp_version']!r} is below supported floor {MINIMUM_OMP_VERSION}"
            )
        if compare_semver(environment["spt_core_version"], minimum_spt_core_version) < 0:
            errors.append(
                f"{path}: environment/spt_core_version {environment['spt_core_version']!r} is below manifest floor {minimum_spt_core_version}"
            )
        names = [artifact["name"] for artifact in record["artifacts"]]
        if len(names) != len(set(names)):
            errors.append(f"{path}: artifact names must be unique")
        artifact_digests = {
            artifact["name"]: artifact["sha256"] for artifact in record["artifacts"]
        }
        outcomes = [record["acquisition"], *record["native_acceptance"].values()]
        evidence_refs = {outcome["evidence"] for outcome in outcomes}
        for evidence_ref in sorted(evidence_refs):
            asset_name, evidence_digest = evidence_ref.rsplit("#sha256=", 1)
            inventoried_digest = artifact_digests.get(asset_name)
            if inventoried_digest is None:
                errors.append(
                    f"{path}: evidence asset {asset_name!r} is not present in artifacts"
                )
            elif inventoried_digest != evidence_digest:
                errors.append(
                    f"{path}: evidence digest for {asset_name!r} does not match artifacts inventory"
                )
        if "adapter.spt" not in names:
            errors.append(f"{path}: artifacts must include the acquired adapter.spt")
        helper_name = (
            f"{record['target']}/omp-spt.exe"
            if "windows" in record["target"]
            else f"{record['target']}/omp-spt"
        )
        if helper_name not in names:
            errors.append(f"{path}: artifacts must include selected helper {helper_name!r}")
        valid_records.append((path, record))

    targets: dict[str, Path] = {}
    identity: tuple[str, str, str, str] | None = None
    identity_path: Path | None = None
    adapter_digest: str | None = None
    adapter_digest_path: Path | None = None
    for path, record in valid_records:
        target = record["target"]
        if target in targets:
            errors.append(f"{path}: duplicate target {target!r}; first recorded by {targets[target]}")
        else:
            targets[target] = path
        release = record["release"]
        current_identity = (
            release["version"],
            release["tag"],
            release["source_commit"],
            release["source_tree"],
        )
        if identity is None:
            identity = current_identity
            identity_path = path
        elif current_identity != identity:
            errors.append(f"{path}: release source identity differs from {identity_path}")
        current_adapter_digest = next(
            (
                artifact["sha256"]
                for artifact in record["artifacts"]
                if artifact["name"] == "adapter.spt"
            ),
            None,
        )
        if current_adapter_digest is not None:
            if adapter_digest is None:
                adapter_digest = current_adapter_digest
                adapter_digest_path = path
            elif current_adapter_digest != adapter_digest:
                errors.append(f"{path}: adapter.spt digest differs from {adapter_digest_path}")
    missing_targets = sorted(SUPPORTED_TARGETS - targets.keys())
    unexpected_targets = sorted(targets.keys() - SUPPORTED_TARGETS)
    if missing_targets:
        errors.append(f"release evidence is missing supported target(s): {', '.join(missing_targets)}")
    if unexpected_targets:
        errors.append(f"release evidence contains unsupported target(s): {', '.join(unexpected_targets)}")
    return errors


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("schema", type=Path)
    parser.add_argument("manifest", type=Path)
    parser.add_argument("evidence", type=Path, nargs="+")
    args = parser.parse_args(argv)
    errors = validate_records(args.schema, args.manifest, args.evidence)
    if errors:
        for error in errors:
            print(f"FAIL: {error}")
        return 1
    targets = ", ".join(path.name for path in args.evidence)
    print(f"RELEASE-EVIDENCE OK: {targets}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
