"""Recursive physical-file reconciliation for the v1.4 candidate seal."""

from __future__ import annotations

import stat
from collections.abc import Sequence
from pathlib import Path

from scripts.verify_nutricoach_v140_candidate_core import (
    CandidateContractError,
    JsonValue,
    canonical,
    require_digest,
    require_object,
    require_string,
    sha256_bytes,
    sha256_file,
)


def verify_physical_inventory(
    root: Path,
    raw_inventory: Sequence[JsonValue],
    expected_digest: str,
) -> int:
    """Require exact path/hash/mode/role equality for every file but manifest."""
    inventory: dict[str, dict[str, JsonValue]] = {}
    normalized: list[JsonValue] = []
    normalized_paths: list[str] = []
    for index, raw in enumerate(raw_inventory):
        entry = require_object(raw, f"physical_inventory[{index}]")
        relative = require_string(entry.get("path"), "physical path")
        digest = require_digest(entry.get("sha256"), "physical hash")
        mode = require_string(entry.get("mode"), "physical mode")
        role = require_string(entry.get("role"), "physical role")
        path = Path(relative)
        if (
            relative == "manifest.json"
            or path.is_absolute()
            or ".." in path.parts
            or relative in inventory
            or not role
        ):
            raise CandidateContractError("physical inventory entry is invalid")
        inventory[relative] = entry
        normalized.append(
            {"mode": mode, "path": relative, "role": role, "sha256": digest}
        )
        normalized_paths.append(relative)
    actual: dict[str, Path] = {}
    for path in root.rglob("*"):
        if path.is_symlink():
            raise CandidateContractError("physical candidate contains a symlink")
        if path.is_file() and path.name != "manifest.json":
            actual[path.relative_to(root).as_posix()] = path
    if set(actual) != set(inventory):
        missing = sorted(set(inventory) - set(actual))
        unknown = sorted(set(actual) - set(inventory))
        raise CandidateContractError(
            f"physical inventory set mismatch missing={missing} unknown={unknown}"
        )
    if normalized_paths != sorted(actual):
        raise CandidateContractError("physical inventory is not sorted")
    for relative, path in actual.items():
        entry = inventory[relative]
        if sha256_file(path) != entry["sha256"]:
            raise CandidateContractError(f"physical file hash mismatch: {relative}")
        observed_mode = oct(stat.S_IMODE(path.stat().st_mode))
        if observed_mode != entry["mode"]:
            raise CandidateContractError(f"physical file mode mismatch: {relative}")
    physical_digest = sha256_bytes(canonical(normalized))
    if physical_digest != expected_digest:
        raise CandidateContractError("physical seal digest mismatch")
    return len(actual)
