#!/usr/bin/env python3
from __future__ import annotations

import argparse
import hashlib
import json
import subprocess
import sys
import tempfile
from collections import Counter
from pathlib import Path
from typing import Any

import yaml
from rdflib import Graph, Namespace

EXIT_OK = 0
EXIT_RUNTIME = 2
EXIT_CLOSURE = 6

GEARMSH = Namespace("https://specs.kivanura.org/gearm/shacl#")


def sha256_file(path: Path) -> str:
    h = hashlib.sha256()
    with path.open("rb") as fh:
        for block in iter(lambda: fh.read(1024 * 1024), b""):
            h.update(block)
    return h.hexdigest()


def json_obj(path: Path) -> Any:
    return json.loads(path.read_text(encoding="utf-8"))


def yaml_obj(path: Path) -> Any:
    return yaml.safe_load(path.read_text(encoding="utf-8"))


def rel_bps(path: Path, bps: Path) -> str:
    return path.relative_to(bps).as_posix()


def parse_sha256sums(path: Path) -> dict[str, str]:
    out: dict[str, str] = {}
    for n, raw in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
        line = raw.strip()
        if not line or line.startswith("#"):
            continue
        parts = line.split(None, 1)
        if len(parts) != 2 or len(parts[0]) != 64:
            raise ValueError(f"Invalid SHA256SUMS line {n}: {raw}")
        digest, rel = parts
        rel = rel.lstrip("*").strip()
        out[rel] = digest.lower()
    return out


class ReleaseClosureRunner:
    def __init__(self, root: Path):
        self.root = root
        self.bps = root.parents[3]
        self.spec = self.bps / "docs/tools/gearm/0.1/gearm-0.1-specification.md"
        self.release = root / "release"
        self.manifest_path = self.release / "manifest.json"
        self.sums_path = self.release / "SHA256SUMS"
        self.results: list[dict[str, Any]] = []
        self.live_a14_report: dict[str, Any] | None = None

    def add(self, check_id: str, category: str, ok: bool, summary: str, detail: Any = None) -> None:
        rec: dict[str, Any] = {
            "id": check_id,
            "category": category,
            "status": "PASS" if ok else "FAIL",
            "summary": summary,
        }
        if detail is not None:
            rec["detail"] = detail
        self.results.append(rec)

    def _expected_subject_paths(self) -> list[str]:
        paths = [rel_bps(self.spec, self.bps)]
        for p in sorted(self.root.rglob("*")):
            if not p.is_file():
                continue
            if self.release in p.parents:
                continue
            paths.append(rel_bps(p, self.bps))
        return sorted(paths)

    def _check_manifest_inventory(self, manifest: dict[str, Any]) -> None:
        entries = manifest.get("subjectFiles", [])
        actual_paths = self._expected_subject_paths()
        declared_paths = sorted(x.get("path") for x in entries)
        self.add(
            "A15-020",
            "inventory",
            declared_paths == actual_paths,
            "Release manifest inventories the complete frozen A1-A14 subject plus normative specification",
            {"declaredCount": len(declared_paths), "actualCount": len(actual_paths), "missing": sorted(set(actual_paths) - set(declared_paths)), "extra": sorted(set(declared_paths) - set(actual_paths))},
        )

        defects: list[dict[str, Any]] = []
        for entry in entries:
            rel = entry["path"]
            path = self.bps / rel
            if not path.exists():
                defects.append({"path": rel, "issue": "missing"})
                continue
            digest = sha256_file(path)
            size = path.stat().st_size
            if digest != entry.get("sha256"):
                defects.append({"path": rel, "issue": "sha256", "expected": entry.get("sha256"), "actual": digest})
            if size != entry.get("sizeBytes"):
                defects.append({"path": rel, "issue": "sizeBytes", "expected": entry.get("sizeBytes"), "actual": size})
        self.add("A15-021", "inventory", not defects, "Every release-subject file matches its manifest SHA-256 and size", defects[:50])

    def _check_checksums(self, manifest: dict[str, Any]) -> None:
        sums = parse_sha256sums(self.sums_path)
        subject_paths = [x["path"] for x in manifest.get("subjectFiles", [])]
        support = manifest.get("checksumPolicy", {}).get("a15SupportFiles", [])
        expected_paths = sorted(set(subject_paths + support))
        declared_paths = sorted(sums)
        self.add(
            "A15-030",
            "checksums",
            declared_paths == expected_paths,
            "SHA256SUMS contains exactly the declared release subject and A15 support files",
            {"declaredCount": len(declared_paths), "expectedCount": len(expected_paths), "missing": sorted(set(expected_paths)-set(declared_paths)), "extra": sorted(set(declared_paths)-set(expected_paths))},
        )
        defects = []
        for rel, expected in sums.items():
            path = self.bps / rel
            if not path.exists():
                defects.append({"path": rel, "issue": "missing"})
                continue
            actual = sha256_file(path)
            if actual != expected:
                defects.append({"path": rel, "expected": expected, "actual": actual})
        self.add("A15-031", "checksums", not defects, "SHA256SUMS validates every listed file", defects[:50])

    def _check_semantic_boundary_counts(self, manifest: dict[str, Any]) -> None:
        a1 = yaml_obj(self.root / "gearm-0.1.concept-registry.yaml")
        a2 = yaml_obj(self.root / "gearm-0.1.relationship-registry.yaml")
        a3 = yaml_obj(self.root / "gearm-0.1.invariant-catalogue.yaml")
        tc = json_obj(self.root / "testcases/manifest.json")
        a6 = Graph().parse(self.root / "gearm-0.1.shacl.ttl", format="turtle")
        coverage = Counter(str(x).rsplit("#", 1)[-1] for x in a6.objects(None, GEARMSH.coverageStatus))
        required_assessment = sum(x.get("expected", {}).get("semanticAssessment") == "required" for x in tc.get("testcases", []))
        actual = {
            "conceptCount": len(a1.get("concepts", [])),
            "relationshipCount": len(a2.get("relationships", [])),
            "invariantCount": len(a3.get("invariants", [])),
            "a6Coverage": {
                "PartialExecutable": coverage.get("PartialExecutable", 0),
                "HybridAssessmentRequired": coverage.get("HybridAssessmentRequired", 0),
                "AssessmentOnly": coverage.get("AssessmentOnly", 0),
            },
            "testcaseCount": len(tc.get("testcases", [])),
            "testcasesRequiringSemanticAssessment": required_assessment,
        }
        expected = manifest.get("closureMetrics", {})
        self.add(
            "A15-040",
            "semantic-boundary",
            actual == expected,
            "Release manifest accurately records the established machine/semantic-assessment boundary metrics",
            {"expected": expected, "actual": actual},
        )

    def _run_a14(self, manifest: dict[str, Any]) -> None:
        runner = self.root / "regression/run_regression.py"
        with tempfile.TemporaryDirectory() as d:
            report_path = Path(d) / "a14-live.json"
            proc = subprocess.run(
                [sys.executable, str(runner), "--suite", "--quiet", "--json-report", str(report_path)],
                cwd=self.root,
                capture_output=True,
                text=True,
            )
            if proc.returncode != 0 or not report_path.exists():
                self.add("A15-050", "A14-live", False, "Live A14 regression executes successfully", {"exit": proc.returncode, "stdout": proc.stdout[-1500:], "stderr": proc.stderr[-1500:]})
                return
            report = json_obj(report_path)
            self.live_a14_report = report
            actual = report.get("summary", {})
            expected = manifest.get("requiredGates", {}).get("A14", {})
            self.add("A15-050", "A14-live", actual == expected, "Live A14 regression remains at the release-required result", {"expected": expected, "actual": actual})

            checks = {x.get("id"): x for x in report.get("checks", [])}
            a13_actual = checks.get("A14-150", {}).get("detail", {}).get("actual", {})
            a12_actual = checks.get("A14-151", {}).get("detail", {}).get("actual", {})
            a13_expected = manifest.get("requiredGates", {}).get("A13", {})
            a12_expected = manifest.get("requiredGates", {}).get("A12", {})
            self.add("A15-051", "A13-live", a13_actual == a13_expected, "A14 confirms the live A13 reconciliation release gate", {"expected": a13_expected, "actual": a13_actual})
            self.add("A15-052", "A12-live", a12_actual == a12_expected, "A14 confirms the live A12 testcase release gate", {"expected": a12_expected, "actual": a12_actual})

    def run(self) -> dict[str, Any]:
        manifest = json_obj(self.manifest_path)
        spec_identity = manifest.get("specification", {})
        identity_ok = (
            spec_identity.get("id") == "GEARM"
            and spec_identity.get("version") == "0.1"
            and manifest.get("artifactStage") == "GEARM-A15"
            and manifest.get("releaseIdentifier") == "GEARM-0.1-RC1"
            and manifest.get("releaseStatus") == "Executable Release Candidate"
            and manifest.get("promotionTarget") == "Released"
        )
        self.add("A15-001", "identity", identity_ok, "Release manifest identifies GEARM 0.1 RC1 / GEARM-A15")

        required_release_files = [
            "release/README.md",
            "release/RELEASE-NOTES.md",
            "release/manifest.json",
            "release/SHA256SUMS",
            "release/requirements-release.txt",
            "release/run_release_closure.py",
        ]
        missing = [p for p in required_release_files if not (self.root / p).exists()]
        self.add("A15-010", "package", not missing, "Required A15 closure files are present", missing)

        self._check_manifest_inventory(manifest)
        self._check_checksums(manifest)
        self._check_semantic_boundary_counts(manifest)
        self._run_a14(manifest)

        spec_text = self.spec.read_text(encoding="utf-8")
        docs_required = [
            "Normative Draft / Executable Release Candidate",
            "GEARM-A15",
            "release/manifest.json",
            "release/SHA256SUMS",
            "release/run_release_closure.py",
            "GEARM-0.1-RC1",
        ]
        missing_docs = [x for x in docs_required if x not in spec_text]
        self.add("A15-060", "documentation", not missing_docs, "Normative specification declares the A15 release-candidate closure and closure artifacts", missing_docs)

        limitations = manifest.get("knownLimitations", [])
        limit_text = "\n".join(str(x) for x in limitations)
        boundary_ok = all(fragment in limit_text for fragment in ["semantic assessment", "PySHACL", "not a final Released publication"])
        self.add("A15-070", "limitations", boundary_ok, "Release manifest explicitly preserves semantic-assessment, SHACL-engine, and publication-status limitations")

        policy = manifest.get("promotionPolicy", {})
        policy_ok = (
            policy.get("automaticPromotion") is False
            and policy.get("releaseCandidateClosureIsNotFinalRelease") is True
            and policy.get("finalReleaseRequiresGovernedPublicationDecision") is True
        )
        self.add("A15-080", "promotion", policy_ok, "Release closure cannot silently promote RC1 to Released")

        passes = sum(x["status"] == "PASS" for x in self.results)
        fails = sum(x["status"] == "FAIL" for x in self.results)
        return {
            "specification": {"id": "GEARM", "version": "0.1"},
            "artifactStage": "GEARM-A15",
            "releaseIdentifier": "GEARM-0.1-RC1",
            "releaseStatus": "Executable Release Candidate",
            "closureVersion": "0.1.0-rc1",
            "closureDate": "2026-09-27",
            "purpose": "Close the GEARM 0.1 executable artifact programme against a frozen manifest, checksums, explicit limitations, and live A12-A14 gates.",
            "normativeBoundary": "A15 closes an executable release candidate. It does not replace semantic assessment, establish conformance of an arbitrary implementation, or promote RC1 to final Released status.",
            "summary": {
                "checkCount": len(self.results),
                "passCount": passes,
                "failCount": fails,
                "status": "PASS" if fails == 0 else "FAIL",
                "candidateClosed": fails == 0,
                "finalReleasePromoted": False,
            },
            "checks": self.results,
        }


def main() -> int:
    parser = argparse.ArgumentParser(description="GEARM-A15 release-candidate closure gate")
    parser.add_argument("--suite", action="store_true", help="Run the complete release-closure suite (default behavior).")
    parser.add_argument("--json-report", help="Write deterministic JSON closure report.")
    parser.add_argument("--quiet", action="store_true", help="Suppress per-check console output.")
    args = parser.parse_args()

    root = Path(__file__).resolve().parents[1]
    try:
        report = ReleaseClosureRunner(root).run()
    except Exception as exc:
        print(f"GEARM-A15 runtime error: {exc}", file=sys.stderr)
        return EXIT_RUNTIME

    if args.json_report:
        out = Path(args.json_report)
        out.parent.mkdir(parents=True, exist_ok=True)
        out.write_text(json.dumps(report, indent=2, sort_keys=False) + "\n", encoding="utf-8")

    if not args.quiet:
        for check in report["checks"]:
            print(f"[{check['status']}] {check['id']} {check['summary']}")
        s = report["summary"]
        print(f"\nGEARM-A15 release closure: {s['status']} — {s['passCount']} pass, {s['failCount']} fail / {s['checkCount']} checks")
        if s["status"] == "PASS":
            print("GEARM-0.1-RC1 is closed as an Executable Release Candidate; final release promotion remains a separate governance decision.")

    return EXIT_OK if report["summary"]["failCount"] == 0 else EXIT_CLOSURE


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