#!/usr/bin/env python3
"""Publish native Skylos reports for the three public regression manifests.

Requires an already installed Skylos development environment, a clean checkout
at SOURCE_REVISION, and no API keys. Does not install or execute fixture code.
Example:
  python reproduce.py --source /path/to/skylos --output regression.json

Existing outputs are never overwritten. Correctness totals are intentionally
absent: verify.mjs derives them from the original manifests and native reports.
"""

from __future__ import annotations

import argparse
import contextlib
import datetime as dt
import hashlib
import importlib.metadata
import io
import json
import os
from pathlib import Path
import platform
import shutil
import subprocess
import sys
import tempfile
import time
import traceback

SOURCE_REVISION = "0e4055c70e3d8f1b0409227e4f16f6c514b3b5a4"
SOURCE_REPOSITORY = "https://github.com/duriantaco/skylos"
SUITES = ("dead_code", "security", "quality")


def sha256(data: bytes) -> str:
    return hashlib.sha256(data).hexdigest()


def git(source: Path, *args: str) -> bytes:
    return subprocess.check_output(["git", "-C", str(source), *args])


def inventory_digest(files: list[dict]) -> str:
    return sha256("".join(
        f"{entry['path']}\0{entry['sha256']}\n"
        for entry in sorted(files, key=lambda entry: entry["path"])
    ).encode("utf-8"))


def clean_source(source: Path) -> None:
    revision = git(source, "rev-parse", "HEAD").decode().strip()
    if revision != SOURCE_REVISION:
        raise ValueError(f"Source must be {SOURCE_REVISION}; got {revision}")
    if git(source, "status", "--porcelain", "--untracked-files=all").strip():
        raise ValueError("Source checkout has local changes; use a clean worktree")


def read_tracked(source: Path, patterns: list[str], *, content: bool) -> list[dict]:
    names = git(source, "ls-files", "-z", "--", *patterns).decode().split("\0")
    result = []
    for name in sorted(set(name for name in names if name)):
        path = source / name
        if path.is_symlink() or not path.is_file():
            raise ValueError(f"Input must be a regular tracked file: {name}")
        data = path.read_bytes()
        entry = {"path": name, "sha256": sha256(data)}
        if content:
            entry["content"] = data.decode("utf-8")
        result.append(entry)
    return result


def manifests_and_inputs(source: Path) -> tuple[list[dict], list[dict], list[str]]:
    suites = []
    patterns = []
    for suite_id in SUITES:
        name = f"benchmarks/{suite_id}/manifest.json"
        path = source / name
        data = path.read_bytes()
        manifest = json.loads(data)
        cases = manifest.get("cases")
        if not isinstance(cases, list) or not cases:
            raise ValueError(f"Empty manifest: {name}")
        patterns.append(name)
        seen = set()
        for case in cases:
            if case["id"] in seen:
                raise ValueError(f"Duplicate case id in {name}: {case['id']}")
            seen.add(case["id"])
            fixture = (path.parent / case["path"]).resolve()
            fixture.relative_to(source)
            patterns.append(fixture.relative_to(source).as_posix())
        suites.append({
            "id": suite_id,
            "manifestPath": name,
            "manifestSha256": sha256(data),
            "manifest": manifest,
            "cases": [],
        })
    inputs = read_tracked(source, patterns, content=True)
    return suites, inputs, patterns


def worker(source: Path, suite_id: str, case_id: str, output: Path) -> None:
    """A fresh interpreter per case prevents scanner globals crossing cases."""
    network_attempts = []

    def network_guard(event: str, args: tuple) -> None:
        if event in {"socket.connect", "socket.getaddrinfo", "socket.gethostbyname"}:
            network_attempts.append(event)
            raise PermissionError("Benchmark runner blocks Python network access")

    sys.addaudithook(network_guard)
    sys.path.insert(0, str(source))
    import skylos
    from skylos.analyzer import analyze

    Path(skylos.__file__).resolve().relative_to(source)
    manifest_path = source / f"benchmarks/{suite_id}/manifest.json"
    manifest = json.loads(manifest_path.read_text())
    case = next(item for item in manifest["cases"] if item["id"] == case_id)
    fixture = (manifest_path.parent / case["path"]).resolve()
    captured_out = io.StringIO()
    captured_err = io.StringIO()
    started = time.perf_counter()
    record = {
        "id": case_id, "status": "error", "elapsedSeconds": 0,
        "analysisRoot": "", "nativeReport": None, "error": None,
    }
    with tempfile.TemporaryDirectory(prefix="skylos-public-case-") as temp:
        root = (Path(temp) / fixture.name).resolve()
        record["analysisRoot"] = str(root)
        # Copy tracked corpus inputs only. No configuration from the checkout's
        # ancestors, ignored files, traces or prior scan caches is inherited.
        names = git(source, "ls-files", "-z", "--", str(fixture.relative_to(source)))
        for name in names.decode().split("\0"):
            if not name:
                continue
            original = source / name
            if original.is_symlink():
                raise ValueError(f"Symlink input is unsupported: {name}")
            if fixture.is_dir():
                destination = root / original.relative_to(fixture)
            else:
                destination = root
            destination.parent.mkdir(parents=True, exist_ok=True)
            shutil.copyfile(original, destination)
        config = case.get("scan") or {}
        options = {
            "conf": int(config.get("confidence", 60)) if suite_id == "dead_code" else 0,
            "enable_quality": bool(config.get("enable_quality", suite_id == "quality")),
            "enable_danger": bool(config.get("enable_danger", suite_id == "security")),
            "enable_secrets": bool(config.get("enable_secrets", False)),
            "enable_ai_defects": False,
            "enable_dependency_hallucinations": False,
            "enable_sca": False,
            "grep_verify": bool(config.get("grep_verify", suite_id == "dead_code")),
            "grep_cache": False,
            "custom_rules_data": [],
            "all_advice": True,
        }
        scan_paths = case.get("scan_paths") or []
        if scan_paths:
            target = [str((root / path).resolve()) for path in scan_paths]
        else:
            target = str(root)
        record["effectiveScan"] = {"target": target, **options}
        try:
            with contextlib.redirect_stdout(captured_out), contextlib.redirect_stderr(captured_err):
                raw = analyze(target, **options)
            report = json.loads(raw)
            if not isinstance(report, dict):
                raise ValueError("Analyzer returned a non-object report")
            record["nativeReport"] = report
            if report.get("analysis_errors"):
                record["error"] = "Analyzer reported incomplete analysis; see nativeReport.analysis_errors"
            elif network_attempts:
                record["error"] = "Analyzer attempted network access; see networkAttempts"
            else:
                record["status"] = "complete"
        except Exception:
            record["error"] = traceback.format_exc()
        record["stdout"] = captured_out.getvalue()
        record["stderr"] = captured_err.getvalue()
        record["networkAttempts"] = network_attempts
        record["elapsedSeconds"] = round(time.perf_counter() - started, 6)
    output.write_text(json.dumps(record, ensure_ascii=False), encoding="utf-8")


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--source", required=True, type=Path)
    parser.add_argument("--output", required=True, type=Path)
    parser.add_argument("--worker", choices=SUITES, help=argparse.SUPPRESS)
    parser.add_argument("--case", help=argparse.SUPPRESS)
    args = parser.parse_args()
    source = args.source.resolve()
    if args.worker:
        worker(source, args.worker, args.case, args.output)
        return
    if args.output.exists():
        raise ValueError("Output already exists; use a new path for each run")
    runner_bytes = Path(__file__).read_bytes()
    clean_source(source)
    suites, inputs, patterns = manifests_and_inputs(source)
    source_files = read_tracked(source, ["skylos", "pyproject.toml"], content=False)
    created_at = dt.datetime.now(dt.timezone.utc).isoformat()
    dependencies = sorted({
        (dist.metadata.get("Name") or "unknown", dist.version)
        for dist in importlib.metadata.distributions()
    })
    artifact = {
        "schemaVersion": 1,
        "run": {
            "id": "regression-20261022-0e4055c7",
            "createdAt": created_at,
            "source": {
                "repository": SOURCE_REPOSITORY, "revision": SOURCE_REVISION,
                "clean": True, "trackedInputSha256": inventory_digest(inputs),
                "inputCount": len(inputs),
                "inputInventoryCommand": ["git", "ls-files", "-z", "--", *patterns],
                "files": source_files,
                "trackedSourceSha256": inventory_digest(source_files),
            },
            "environment": {
                "python": sys.version, "platform": platform.platform(),
                "machine": platform.machine(), "processor": platform.processor(),
                "cpuCount": os.cpu_count(),
                "dependencies": [{"name": name, "version": version} for name, version in dependencies],
                "pythonHashSeed": "randomized (-I ignores PYTHONHASHSEED)",
                "pythonHashSeedRequested": "0", "pythonIsolation": "-I (workers)",
            },
            "execution": {
                "command": ["python", "reproduce.py", "--source", "<clean-checkout>", "--output", "<new-output.json>"],
                "runnerSha256": sha256(runner_bytes),
                "network": "Python audit hook blocks connect/DNS; no network-dependent detectors enabled",
                "targetCodeExecuted": False, "paidCalls": False,
                "caseTimeoutSeconds": 120,
                "corpus": "All cases in the three public, project-authored regression manifests",
                "comparisonPolicy": "Full isolated fixture scan; no changed_files/diff comparison (fixture copies contain no Git before/after history)",
                "limits": [
                    "Development regression fixtures; no independent holdout or production accuracy claim",
                    "Security and quality labels are rule-level presence/absence expectations, not exhaustive finding labels",
                    "Unmatched findings remain in nativeReport and are not automatically classified as false positives",
                    "Timing is informational, one run on one machine; it is not included in correctness totals",
                    "No security or quality benchmark covers every Skylos rule, secret detector or SCA provider",
                    "Hash checks detect artifact changes; they do not establish independent label correctness",
                    "Profile differs from scripts/security_benchmark.py: changed_files is omitted because isolated fixtures have no Git comparison history; its original profile produced security_regression_unavailable ConfigError in a diagnostic run",
                    "all_advice=True retains raw advice checks; this report does not measure the terminal default noise level",
                    "The Python network audit hook does not sandbox native subprocesses; Go proxy, checksum service and toolchain downloads are disabled explicitly",
                    "Reproduction compares expected-case outcomes, not identical report bytes; temporary paths, timings and randomized Python hashing can vary",
                ],
            },
        },
        "inputs": inputs,
        "suites": suites,
    }
    script = Path(__file__).resolve()
    with tempfile.TemporaryDirectory(prefix="skylos-public-benchmark-") as temp:
        isolated_cache = Path(temp) / "cache"
        isolated_cache.mkdir()
        env = os.environ.copy()
        for name in ("PYTHONPATH", "PYTHONHOME"):
            env.pop(name, None)
        env.update({
            "PYTHONHASHSEED": "0", "XDG_CACHE_HOME": str(isolated_cache),
            "GOPROXY": "off", "GOSUMDB": "off", "GOTOOLCHAIN": "local",
        })
        for suite in suites:
            for case in suite["manifest"]["cases"]:
                result = Path(temp) / "case.json"
                if result.exists():
                    result.unlink()
                start = time.perf_counter()
                try:
                    process = subprocess.run(
                        [sys.executable, "-I", str(script), "--source", str(source),
                         "--output", str(result), "--worker", suite["id"], "--case", case["id"]],
                        cwd=temp, env=env, capture_output=True, text=True, timeout=120,
                    )
                    if process.returncode or not result.exists():
                        raise RuntimeError(f"Worker exited {process.returncode}: {process.stderr}")
                    record = json.loads(result.read_text())
                except Exception:
                    record = {
                        "id": case["id"], "status": "error",
                        "elapsedSeconds": round(time.perf_counter() - start, 6),
                        "analysisRoot": "", "nativeReport": None,
                        "error": traceback.format_exc(),
                    }
                suite["cases"].append(record)
                print(f"{suite['id']}/{case['id']}: {record['status']}", file=sys.stderr, flush=True)
    clean_source(source)
    after = read_tracked(source, patterns, content=True)
    if inventory_digest(after) != artifact["run"]["source"]["trackedInputSha256"]:
        raise ValueError("Corpus changed during the run; refusing to publish")
    after_source = read_tracked(source, ["skylos", "pyproject.toml"], content=False)
    if inventory_digest(after_source) != artifact["run"]["source"]["trackedSourceSha256"]:
        raise ValueError("Analyzer source changed during the run; refusing to publish")
    if Path(__file__).read_bytes() != runner_bytes:
        raise ValueError("Reproducer changed during the run; refusing to publish")
    artifact["run"]["finishedAt"] = dt.datetime.now(dt.timezone.utc).isoformat()
    args.output.parent.mkdir(parents=True, exist_ok=True)
    with args.output.open("x", encoding="utf-8") as target:
        json.dump(artifact, target, ensure_ascii=False, indent=2)
        target.write("\n")
    print(f"Wrote {args.output}; SHA256 {sha256(args.output.read_bytes())}", file=sys.stderr)


if __name__ == "__main__":
    main()
