#!/usr/bin/env python3
"""Dependency-free verifier for the receipt-witness operator kit."""

from __future__ import annotations

import hashlib
import json
import os
from pathlib import Path, PurePosixPath
from typing import Any

PROFILE = "bulla.receipt-witness-operator-kit/0.1-experimental"
SOURCE_PROFILE = "bulla.receipt-witness-source-closure/0.1"
MAX_MEMBERS = 96
MAX_MEMBER_BYTES = 2 * 1024 * 1024
MAX_TOTAL_BYTES = 8 * 1024 * 1024
MAX_DEPTH = 32


class KitError(ValueError):
    pass


def _pairs(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
    result: dict[str, Any] = {}
    for key, value in pairs:
        if key in result:
            raise KitError(f"duplicate JSON member: {key}")
        result[key] = value
    return result


def _reject(_: str) -> None:
    raise KitError("floats and non-finite numbers are not permitted")


def _depth(value: Any, level: int = 1) -> None:
    if level > MAX_DEPTH:
        raise KitError(f"JSON nesting exceeds {MAX_DEPTH}")
    if isinstance(value, dict):
        for item in value.values():
            _depth(item, level + 1)
    elif isinstance(value, list):
        for item in value:
            _depth(item, level + 1)


def parse(raw: bytes, label: str) -> Any:
    try:
        value = json.loads(
            raw.decode("utf-8"),
            object_pairs_hook=_pairs,
            parse_float=_reject,
            parse_constant=_reject,
        )
    except (UnicodeDecodeError, json.JSONDecodeError) as exc:
        raise KitError(f"{label} is not strict UTF-8 JSON") from exc
    _depth(value)
    return value


def canonical(value: Any) -> bytes:
    return (
        json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
        + "\n"
    ).encode("utf-8")


def digest(raw: bytes) -> str:
    return "sha256:" + hashlib.sha256(raw).hexdigest()


def safe(path: str) -> None:
    pure = PurePosixPath(path)
    if (
        not path
        or path != path.lower()
        or "\\" in path
        or pure.is_absolute()
        or any(part in {"", ".", ".."} for part in pure.parts)
        or len(pure.parts) > MAX_DEPTH
        or str(pure) != path
    ):
        raise KitError(f"noncanonical kit member: {path}")


def verify(root: Path) -> dict[str, Any]:
    root = Path(root)
    if root.is_symlink() or not root.is_dir():
        raise KitError("kit root must be a real directory")
    root = root.resolve(strict=True)
    manifest_path = root / "manifest.json"
    if manifest_path.is_symlink() or not manifest_path.is_file():
        raise KitError("kit manifest is missing")
    manifest_size = manifest_path.stat().st_size
    if manifest_size > MAX_MEMBER_BYTES:
        raise KitError(f"kit member exceeds {MAX_MEMBER_BYTES} bytes: manifest.json")
    manifest_raw = manifest_path.read_bytes()
    manifest = parse(manifest_raw, "kit manifest")
    if not isinstance(manifest, dict) or set(manifest) != {"profile", "root", "members"}:
        raise KitError("kit manifest fields changed")
    if manifest["profile"] != PROFILE or manifest_raw != canonical(manifest):
        raise KitError("kit manifest profile or canonical presentation changed")
    if not isinstance(manifest["members"], list) or len(manifest["members"]) > MAX_MEMBERS - 1:
        raise KitError(f"kit exceeds {MAX_MEMBERS} members")
    declared: set[str] = {"manifest.json"}
    allowed_directories: set[str] = set()
    for entry in manifest["members"]:
        if not isinstance(entry, dict) or set(entry) != {"path", "byte_length", "sha256"}:
            raise KitError("kit manifest member fields changed")
        path = entry["path"]
        if (
            not isinstance(path, str)
            or type(entry["byte_length"]) is not int
            or entry["byte_length"] < 0
            or not isinstance(entry["sha256"], str)
        ):
            raise KitError("kit manifest member types changed")
        safe(path)
        if path in declared:
            raise KitError(f"duplicate kit member path: {path}")
        declared.add(path)
        for parent in PurePosixPath(path).parents:
            if str(parent) != ".":
                allowed_directories.add(str(parent))

    actual: dict[str, bytes] = {}
    total = 0
    entries_seen = 0

    def visit(directory: Path, relative_directory: str = "") -> None:
        nonlocal total, entries_seen
        bounded: list[os.DirEntry[str]] = []
        with os.scandir(directory) as iterator:
            for entry in iterator:
                entries_seen += 1
                if entries_seen > MAX_MEMBERS * 2:
                    raise KitError("kit filesystem contains too many entries")
                bounded.append(entry)
        for entry in sorted(bounded, key=lambda item: item.name):
            relative = f"{relative_directory}/{entry.name}" if relative_directory else entry.name
            safe(relative)
            if entry.is_symlink():
                raise KitError(f"kit contains a symlink: {relative}")
            if entry.is_dir(follow_symlinks=False):
                if relative not in allowed_directories:
                    raise KitError(f"kit contains an undeclared directory: {relative}")
                visit(Path(entry.path), relative)
                continue
            if not entry.is_file(follow_symlinks=False):
                raise KitError(f"kit contains a non-regular member: {relative}")
            if relative not in declared:
                raise KitError(f"kit contains an undeclared member: {relative}")
            size = entry.stat(follow_symlinks=False).st_size
            if size > MAX_MEMBER_BYTES:
                raise KitError(f"kit member exceeds {MAX_MEMBER_BYTES} bytes: {relative}")
            total += size
            if total > MAX_TOTAL_BYTES:
                raise KitError(f"kit exceeds {MAX_TOTAL_BYTES} bytes")
            actual[relative] = Path(entry.path).read_bytes()

    visit(root)
    if set(actual) != declared:
        raise KitError("kit filesystem does not match the exact manifest member set")
    manifest_raw = actual.pop("manifest.json")
    descriptors = [
        {"path": path, "byte_length": len(raw), "sha256": digest(raw)}
        for path, raw in sorted(actual.items())
    ]
    if manifest["members"] != descriptors:
        raise KitError("kit manifest does not bind the exact member bytes")
    expected_root = digest(canonical({"profile": PROFILE, "members": descriptors}))
    if manifest["root"] != expected_root:
        raise KitError("kit root does not match its members")
    expected_directory = expected_root.replace(":", "-")
    if root.name != expected_directory:
        raise KitError("extracted directory name does not match the kit root")

    source_raw = actual.get("source-closure.json")
    if source_raw is None:
        raise KitError("source closure is missing")
    source = parse(source_raw, "source closure")
    if not isinstance(source, dict) or set(source) != {"profile", "root", "members"}:
        raise KitError("source closure fields changed")
    if source["profile"] != SOURCE_PROFILE or source_raw != canonical(source):
        raise KitError("source closure profile or canonical presentation changed")
    source_descriptors = []
    for entry in source["members"]:
        if not isinstance(entry, dict) or set(entry) != {"path", "byte_length", "sha256"}:
            raise KitError("source closure member fields changed")
        path = entry["path"]
        safe(path)
        raw = actual.get(path)
        if raw is None:
            raise KitError(f"source closure member is absent: {path}")
        source_descriptors.append(
            {"path": path, "byte_length": len(raw), "sha256": digest(raw)}
        )
    if source["members"] != source_descriptors:
        raise KitError("source closure does not bind exact bytes")
    source_root = digest(
        canonical({"profile": SOURCE_PROFILE, "members": source_descriptors})
    )
    if source["root"] != source_root:
        raise KitError("source-kit root does not match its closure")
    return {
        "profile": PROFILE,
        "kit_root": expected_root,
        "source_kit_root": source_root,
        "member_count": len(actual) + 1,
        "byte_length": total,
        "status": "VERIFIED",
    }


if __name__ == "__main__":
    import argparse

    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("root", type=Path)
    args = parser.parse_args()
    print(json.dumps(verify(args.root), indent=2))
