#!/usr/bin/env python3
"""Execute the collected gateway suite in deterministic, file-isolated shards."""

from __future__ import annotations

import hashlib
import json
import os
import re
import shutil
import subprocess
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path

ROOT = Path(os.environ["TASK12_TARGET_ROOT"])
EVIDENCE = Path(os.environ["TASK12_EVIDENCE_ROOT"])
SOURCE_EVIDENCE = Path(os.environ["TASK12_SOURCE_EVIDENCE"])
PYTHON = ROOT / ".venv/bin/python"
COLLECTION = SOURCE_EVIDENCE / "broad-gateway-nodeids-isolated.json"
RESULT = EVIDENCE / "independent-gateway-isolated-shards.json"
LOGS = EVIDENCE / "logs"
WORKERS = 12
TIMEOUT_SECONDS = 900


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


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


def shard_for(test_file: str) -> int:
    return int.from_bytes(hashlib.sha256(test_file.encode()).digest()[:8], "big") % WORKERS


def summary_count(output: str) -> int:
    return sum(
        int(count)
        for count, label in re.findall(r"(\d+)\s+(passed|skipped|xfailed|xpassed|failed|error(?:s)?)\b", output)
        if label
    )


def run_file(item: dict[str, object]) -> dict[str, object]:
    test_file = str(item["test_file"])
    file_path = ROOT / test_file
    shard = shard_for(test_file)
    log = LOGS / f"{shard:02d}-{file_path.name}.log"
    home = EVIDENCE / "gateway-shard-homes" / f"{shard:02d}" / file_path.stem
    pycache = EVIDENCE / "gateway-shard-pycache" / f"{shard:02d}" / file_path.stem
    home.mkdir(parents=True, exist_ok=True)
    pycache.mkdir(parents=True, exist_ok=True)
    expected_nodeids = list(item["nodeids"])
    stable_source = sha256(file_path) == item["test_file_sha256"]
    if not stable_source:
        return {
            "test_file": test_file,
            "shard": shard,
            "status": "source_changed_after_collection",
            "expected_nodeid_count": len(expected_nodeids),
            "expected_nodeids_sha256": hashlib.sha256(canonical(expected_nodeids)).hexdigest(),
            "test_file_sha256": sha256(file_path),
        }
    if item["outcome"] == "collection_skip":
        return {
            "test_file": test_file,
            "shard": shard,
            "status": "collection_skip",
            "expected_nodeid_count": 0,
            "expected_nodeids_sha256": hashlib.sha256(canonical(expected_nodeids)).hexdigest(),
            "test_file_sha256": sha256(file_path),
            "collection_log": item["output_log"],
        }
    env = {
        "PATH": os.environ["PATH"],
        "HOME": str(home),
        "TMPDIR": "/tmp",
        "PYTHONPATH": f"{ROOT / 'dualcoach/profile'}:{ROOT}",
        "PYTHONHASHSEED": "0",
        "TZ": "Asia/Seoul",
        "UV_OFFLINE": "1",
        "UV_CACHE_DIR": "/home/cube/.cache/uv",
        "PYTHONDONTWRITEBYTECODE": "1",
        "PYTHONPYCACHEPREFIX": str(pycache),
    }
    basetemp = EVIDENCE / "basetemp" / hashlib.sha256(test_file.encode()).hexdigest()[:10]
    basetemp.parent.mkdir(parents=True, exist_ok=True)
    if basetemp.exists():
        shutil.rmtree(basetemp)
    argv = (
        str(PYTHON), "-m", "pytest", "-q", "-p", "no:cacheprovider",
        "--import-mode=importlib", f"--basetemp={basetemp}", str(file_path),
    )
    try:
        completed = subprocess.run(
            argv, cwd=ROOT, env=env, text=True, capture_output=True,
            check=False, timeout=TIMEOUT_SECONDS,
        )
        output = completed.stdout + completed.stderr
        returncode: int | str = completed.returncode
        status = "passed" if completed.returncode == 0 else "failed"
    except subprocess.TimeoutExpired as error:
        output = (error.stdout or "") + (error.stderr or "")
        returncode = "timeout"
        status = "timeout"
    log.write_text(output, encoding="utf-8")
    actual = summary_count(output)
    count_matches = actual == len(expected_nodeids)
    if status == "passed" and not count_matches:
        status = "summary_count_mismatch"
    return {
        "test_file": test_file,
        "shard": shard,
        "status": status,
        "argv": list(argv),
        "returncode": returncode,
        "expected_nodeid_count": len(expected_nodeids),
        "expected_nodeids_sha256": hashlib.sha256(canonical(expected_nodeids)).hexdigest(),
        "summary_count": actual,
        "summary_count_matches_manifest": count_matches,
        "test_file_sha256": sha256(file_path),
        "output_log": str(log),
        "output_sha256": sha256(log),
    }


def main() -> int:
    collection = json.loads(COLLECTION.read_text(encoding="utf-8"))
    if collection["collection_failures"]:
        raise RuntimeError("cannot execute an incomplete collection manifest")
    if collection["interpreter"]["resolved"] != str(PYTHON.resolve()):
        raise RuntimeError("collection interpreter differs from execution interpreter")
    if collection["interpreter"]["sha256"] != sha256(PYTHON):
        raise RuntimeError("collection interpreter hash differs from execution interpreter")
    items = list(collection["files"])
    LOGS.mkdir(parents=True, exist_ok=True)
    with ThreadPoolExecutor(max_workers=WORKERS) as pool:
        results = list(pool.map(run_file, items))
    results.sort(key=lambda item: str(item["test_file"]))
    failures = [
        item["test_file"] for item in results
        if item["status"] not in {"passed", "collection_skip"}
    ]
    shard_counts = [
        {
            "shard": shard,
            "files": sum(item["shard"] == shard for item in results),
            "expected_nodeids": sum(int(item["expected_nodeid_count"]) for item in results if item["shard"] == shard),
        }
        for shard in range(WORKERS)
    ]
    report = {
        "schema": "nutricoach-v140-task12-isolated-gateway-execution-v1",
        "collection_manifest": str(COLLECTION),
        "collection_manifest_sha256": sha256(COLLECTION),
        "interpreter": {"entrypoint": str(PYTHON), "resolved": str(PYTHON.resolve()), "sha256": sha256(PYTHON)},
        "isolation": {
            "execution": "one fresh subprocess per collected test file",
            "shards": WORKERS,
            "assignment": "sha256(test_file) modulo shards",
            "cwd": str(ROOT),
            "pythonpath": f"{ROOT / 'dualcoach/profile'}:{ROOT}",
            "uv_cache_dir": "/home/cube/.cache/uv",
            "timeout_seconds_per_file": TIMEOUT_SECONDS,
        },
        "shards": shard_counts,
        "files": results,
        "expected_nodeid_count": sum(int(item["expected_nodeid_count"]) for item in results),
        "summary_count": sum(int(item.get("summary_count", 0)) for item in results),
        "failures": failures,
        "collection_skips": [
            item["test_file"] for item in results
            if item["status"] == "collection_skip"
        ],
    }
    RESULT.write_text(json.dumps(report, sort_keys=True, separators=(",", ":")) + "\n", encoding="utf-8")
    print(f"GATEWAY_ISOLATED_EXECUTION files={len(results)} nodeids={report['expected_nodeid_count']} failures={len(failures)}")
    return 0 if not failures else 1


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