#!/usr/bin/env python3
"""Fail-closed GitLawb main publication gate.

The gate performs read-only checks.  It does not commit, push, sign, deploy,
touch wallets, call RPCs, or broadcast transactions.
"""

from __future__ import annotations

import argparse
import hashlib
import json
import subprocess
import sys
from pathlib import Path

HERE = Path(__file__).resolve().parent
ROOT = HERE.parents[1]
INPUTS = HERE / "publication-gate-inputs.json"
CHECKSUMS = HERE / "artifact-checksums.sha256"

VALIDATION_KEYS = {
    "migrationLedger",
    "v2AbiCompatibility",
    "v2RuntimeCompatibility",
    "apiRouteCompatibility",
    "contractSuites",
    "invariantSuites",
    "arithmeticSuites",
    "sdkBuildAndTests",
    "apiBuildAndTests",
    "siteBuildAndTests",
    "solanaReproducibleChecks",
    "x402Discovery",
    "x402Unpaid402",
    "x402ReceiptBehavior",
}
CLEAN_ROOM_KEYS = {
    "cleanState",
    "noLocalUntrackedDependencies",
    "noStaleGeneratedArtifacts",
    "countsVerified",
    *VALIDATION_KEYS,
}


def run(*args: str) -> subprocess.CompletedProcess[str]:
    return subprocess.run(
        args, cwd=ROOT, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
        check=False,
    )


def file_sha256(path: Path) -> str:
    return hashlib.sha256(path.read_bytes()).hexdigest()


def verify_checksums() -> list[str]:
    errors: list[str] = []
    if not CHECKSUMS.is_file():
        return ["deterministic artifact checksum file is missing"]
    for line in CHECKSUMS.read_text().splitlines():
        if not line.strip():
            continue
        try:
            expected, relative = line.split("  ", 1)
        except ValueError:
            errors.append(f"malformed checksum line: {line!r}")
            continue
        path = ROOT / relative
        if not path.is_file():
            errors.append(f"checksummed artifact missing: {relative}")
        elif file_sha256(path) != expected:
            errors.append(f"artifact checksum mismatch: {relative}")
    return errors


def require_pass_map(label: str, values: object, keys: set[str]) -> list[str]:
    if not isinstance(values, dict):
        return [f"{label} evidence must be an object"]
    errors = []
    for key in sorted(keys):
        record = values.get(key)
        if not isinstance(record, dict) or record.get("status") != "pass":
            errors.append(f"{label}.{key} is not evidenced as pass")
        elif not record.get("evidence"):
            errors.append(f"{label}.{key} has no evidence reference")
    return errors


def check(stage: str) -> list[str]:
    errors: list[str] = []
    try:
        inputs = json.loads(INPUTS.read_text())
    except (OSError, json.JSONDecodeError) as exc:
        return [f"cannot read publication gate inputs: {exc}"]

    ledger = run(sys.executable, str(HERE / "check_ledger.py"))
    if ledger.returncode:
        errors.append("complete migration ledger check failed")

    status = run("git", "status", "--porcelain=v1", "--untracked-files=all")
    if status.returncode or status.stdout.strip():
        errors.append("worktree is not clean (tracked or untracked changes exist)")

    head = run("git", "rev-parse", "HEAD")
    tree = run("git", "rev-parse", "HEAD^{tree}")
    branch = run("git", "branch", "--show-current")
    expected = inputs.get("candidate", {})
    if not expected.get("commit") or head.stdout.strip() != expected.get("commit"):
        errors.append("exact candidate commit ID is absent or does not match HEAD")
    if not expected.get("tree") or tree.stdout.strip() != expected.get("tree"):
        errors.append("exact candidate tree ID is absent or does not match HEAD tree")
    if branch.stdout.strip() != "main":
        errors.append(f"candidate branch is {branch.stdout.strip()!r}, not 'main'")

    errors += verify_checksums()
    errors += require_pass_map("localValidation", inputs.get("localValidation"), VALIDATION_KEYS)

    review = inputs.get("finalReview", {})
    if review.get("commit") != head.stdout.strip() or review.get("tree") != tree.stdout.strip():
        errors.append("final review scope does not match exact candidate commit/tree")
    findings = review.get("activeFindings", {})
    for severity in ("critical", "high", "medium", "low"):
        if findings.get(severity) != 0:
            errors.append(f"active {severity} findings are not proven zero")

    blockers = inputs.get("externalBlockers")
    if not isinstance(blockers, dict) or blockers.get("truthfulnessReviewed") is not True:
        errors.append("unresolved external blockers have not been truthfulness-reviewed")
    else:
        for index, blocker in enumerate(blockers.get("items", [])):
            if not blocker.get("description") or "releaseBlocking" not in blocker:
                errors.append(f"external blocker {index} is incomplete")
            if blocker.get("releaseBlocking") is True:
                errors.append(f"external blocker {index} remains release-blocking")

    pre = inputs.get("cleanRoom", {}).get("prePush")
    errors += require_pass_map("cleanRoom.prePush", pre, CLEAN_ROOM_KEYS)
    if stage in {"post-push", "final"}:
        post = inputs.get("cleanRoom", {}).get("postPush")
        errors += require_pass_map("cleanRoom.postPush", post, CLEAN_ROOM_KEYS)
        published = inputs.get("published", {})
        if published.get("branch") != "main":
            errors.append("published branch is not proven to be main")
        if published.get("commit") != expected.get("commit"):
            errors.append("post-push GitLawb main commit does not match candidate")
        if published.get("tree") != expected.get("tree"):
            errors.append("post-push GitLawb main tree does not match candidate")

    return errors


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--stage", choices=("pre-push", "post-push", "final"), default="final")
    args = parser.parse_args()
    errors = check(args.stage)
    if errors:
        print(f"PUBLICATION BLOCKED ({args.stage}): {len(errors)} gate failure(s)")
        for error in errors:
            print(f"- {error}")
        return 1
    print(f"PUBLICATION GATE PASS ({args.stage})")
    return 0


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