#!/usr/bin/env python3
"""Machine-readable Ty gate for the Task26 delivery-capability surface."""
from __future__ import annotations

import argparse
import ast
import hashlib
import json
import subprocess
import sys
from pathlib import Path
from typing import cast


SCHEMA = "task26-ty-capability-surface-receipt-v1"
PASS_STATUS = "PASS_CHANGED_SURFACE_WITH_PREEXISTING_OUTSIDE_SCOPE_DIAGNOSTICS"
FAIL_STATUS = "FAIL_CHANGED_SURFACE_DIAGNOSTICS"
TARGET_SYMBOLS = (
    "DeliveryLaunchAuthorization",
    "DraftDeliveryCapability",
    "DeliveryDispatchClaim",
    "validate_delivery_transport",
    "claim_delivery_transport",
    "record_customer_surface_receipt",
    "issue_delivery_capability",
    "_prepare_delivery_locked",
)


def canonical(value: object) -> bytes:
    return json.dumps(
        value,
        ensure_ascii=False,
        sort_keys=True,
        separators=(",", ":"),
        allow_nan=False,
    ).encode()


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


def symbol_spans(source: Path) -> dict[str, dict[str, int]]:
    tree = ast.parse(source.read_text(encoding="utf-8"), filename=str(source))
    found: dict[str, dict[str, int]] = {}
    for node in ast.walk(tree):
        if (
            isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef))
            and node.name in TARGET_SYMBOLS
        ):
            if node.name in found or node.end_lineno is None:
                raise ValueError(f"capability symbol cardinality is invalid: {node.name}")
            found[node.name] = {
                "start_line": node.lineno,
                "end_line": node.end_lineno,
            }
    if set(found) != set(TARGET_SYMBOLS):
        missing = sorted(set(TARGET_SYMBOLS) - set(found))
        raise ValueError(f"capability symbols are missing: {missing}")
    return {name: found[name] for name in TARGET_SYMBOLS}


def diagnostic_line(diagnostic: dict[str, object]) -> int:
    location = diagnostic.get("location")
    if not isinstance(location, dict):
        raise ValueError("Ty diagnostic location is invalid")
    location_object = cast(dict[str, object], location)
    positions = location_object.get("positions")
    if not isinstance(positions, dict):
        raise ValueError("Ty diagnostic positions are invalid")
    positions_object = cast(dict[str, object], positions)
    begin = positions_object.get("begin")
    if not isinstance(begin, dict):
        raise ValueError("Ty diagnostic begin position is invalid")
    begin_object = cast(dict[str, object], begin)
    line = begin_object.get("line")
    if type(line) is not int or line < 1:
        raise ValueError("Ty diagnostic line is invalid")
    return line


def read_diagnostics(path: Path) -> list[dict[str, object]]:
    value = json.loads(path.read_text(encoding="utf-8"))
    if not isinstance(value, list) or any(not isinstance(row, dict) for row in value):
        raise ValueError("Ty GitLab report is invalid")
    return [cast(dict[str, object], row) for row in value]


def run_ty(source: Path, ty: Path) -> tuple[list[dict[str, object]], list[str]]:
    command = [
        str(ty),
        "check",
        str(source),
        "--output-format",
        "gitlab",
    ]
    completed = subprocess.run(
        command,
        text=True,
        capture_output=True,
        check=False,
        timeout=180,
    )
    if completed.returncode not in {0, 1}:
        raise RuntimeError(
            f"Ty execution failed with {completed.returncode}: {completed.stderr.strip()}"
        )
    value = json.loads(completed.stdout)
    if not isinstance(value, list) or any(not isinstance(row, dict) for row in value):
        raise ValueError("Ty GitLab output is invalid")
    return [cast(dict[str, object], row) for row in value], command


def build_receipt(
    source: Path,
    diagnostics: list[dict[str, object]],
    *,
    command: list[str],
) -> dict[str, object]:
    spans = symbol_spans(source)
    capability: list[dict[str, object]] = []
    outside: list[dict[str, object]] = []
    for diagnostic in diagnostics:
        line = diagnostic_line(diagnostic)
        if any(
            span["start_line"] <= line <= span["end_line"]
            for span in spans.values()
        ):
            capability.append(diagnostic)
        else:
            outside.append(diagnostic)
    outside_projection = sorted(
        outside,
        key=lambda row: canonical(row),
    )
    receipt: dict[str, object] = {
        "schema": SCHEMA,
        "status": PASS_STATUS if not capability else FAIL_STATUS,
        "source_path": source.as_posix(),
        "source_sha256": sha256_file(source),
        "diagnostics_format": "gitlab",
        "ty_command": command,
        "target_symbol_spans": spans,
        "total_diagnostic_count": len(diagnostics),
        "capability_diagnostic_count": len(capability),
        "outside_surface_diagnostic_count": len(outside),
        "outside_surface_fingerprint_sha256": hashlib.sha256(
            canonical(outside_projection)
        ).hexdigest(),
        "capability_diagnostics": capability,
        "history": {
            "v13_nutrition_coaching_scanned_or_sealed": False,
            "v14_first_exposed_dirty_monolith_ty_debt": True,
            "outside_scope_diagnostics_fixed_or_claimed_clean": False,
        },
    }
    receipt["receipt_sha256"] = hashlib.sha256(canonical(receipt)).hexdigest()
    return receipt


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--source", type=Path, required=True)
    parser.add_argument("--diagnostics-json", type=Path)
    parser.add_argument("--ty", type=Path)
    parser.add_argument("--receipt", type=Path)
    args = parser.parse_args()
    source = args.source.absolute()
    if args.diagnostics_json is not None:
        diagnostics = read_diagnostics(args.diagnostics_json)
        command = ["precomputed-gitlab", str(args.diagnostics_json)]
    else:
        default_ty = Path(sys.executable).with_name("ty")
        ty = args.ty or default_ty
        diagnostics, command = run_ty(source, ty)
    receipt = build_receipt(source, diagnostics, command=command)
    output = canonical(receipt) + b"\n"
    if args.receipt is not None:
        args.receipt.write_bytes(output)
    sys.stdout.buffer.write(output)
    return 0 if receipt["capability_diagnostic_count"] == 0 else 1


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