from __future__ import annotations

import hashlib
import json
import os
import pathlib
from typing import Any


EXCLUDED_DIRECTORIES = {
    ".git", ".mypy_cache", ".pytest_cache", ".venv", "__pycache__",
    "artifacts", "build", "delivery", "dist", "node_modules", "runtime_artifacts", "venv",
}
EXCLUDED_SUFFIXES = {".db", ".pyc", ".pyo", ".sqlite", ".sqlite3"}
RUNTIME_PARTS = {"__pycache__", "artifacts", "runtime_artifacts", ".pytest_cache"}


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


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


def canonical_hash(value: object) -> str:
    raw = json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
    return sha256_bytes(raw)


def source_entries(root: pathlib.Path, *, prefix: str = "source/") -> list[dict[str, Any]]:
    root = pathlib.Path(root).resolve()
    rows: list[dict[str, Any]] = []
    for directory, dirnames, filenames in os.walk(root, topdown=True, followlinks=False):
        dirnames[:] = sorted(name for name in dirnames if name not in EXCLUDED_DIRECTORIES)
        parent = pathlib.Path(directory)
        for filename in sorted(filenames):
            path = parent / filename
            if path.suffix.lower() in EXCLUDED_SUFFIXES:
                continue
            rows.append({
                "path": prefix + path.relative_to(root).as_posix(),
                "size": path.stat().st_size,
                "sha256": sha256_file(path),
            })
    return sorted(rows, key=lambda row: row["path"])


def source_tree_manifest(root: pathlib.Path) -> dict[str, Any]:
    entries = source_entries(root)
    return {
        "version": "R10_R2",
        "entries": entries,
        "tree_sha256": canonical_hash(entries),
    }


def runtime_artifacts(root: pathlib.Path) -> list[str]:
    root = pathlib.Path(root).resolve()
    rows: list[str] = []
    if not root.exists():
        return rows
    # Evidence needs to distinguish a pristine extraction (zero runtime
    # roots) from a warm one, not enumerate every per-test transcript.  A
    # recursive rglob descended into artifacts/test_workers and made nested
    # supervisors increasingly expensive as the parent suite progressed.
    # Record each runtime directory as one bounded marker and prune it.
    for directory, dirnames, filenames in os.walk(root, topdown=True, followlinks=False):
        parent = pathlib.Path(directory)
        kept: list[str] = []
        for name in sorted(dirnames):
            child = parent / name
            if name in RUNTIME_PARTS:
                rows.append(child.relative_to(root).as_posix() + "/")
            elif name not in EXCLUDED_DIRECTORIES:
                kept.append(name)
        dirnames[:] = kept
        for filename in sorted(filenames):
            path = parent / filename
            if path.suffix.lower() in EXCLUDED_SUFFIXES:
                rows.append(path.relative_to(root).as_posix())
    return sorted(rows)


def extraction_fingerprint(extraction_root: pathlib.Path, external_identity: str | None = None) -> str:
    marker = pathlib.Path(extraction_root).resolve() / ".r10r2_sealed_result.json"
    marker_hash = sha256_file(marker) if marker.is_file() else "MISSING"
    identity = external_identity or "LOCAL_PATH_IDENTITY"
    return sha256_bytes(f"{pathlib.Path(extraction_root).resolve()}\n{marker_hash}\n{identity}".encode("utf-8"))
