"""Source-byte parity checks for NutriCoach candidate wheels."""

from __future__ import annotations

import stat
import zipfile
from pathlib import Path

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


def verify_wheel_sources(
    source_root: Path,
    hermes_wheel: Path,
    profile_wheel: Path,
) -> None:
    """Verify every wheel-hosted Python source that has a repository source file."""
    wheel_roots = (
        (hermes_wheel, source_root),
        (profile_wheel, source_root / "dualcoach/profile"),
    )
    for wheel_path, package_root in wheel_roots:
        with zipfile.ZipFile(wheel_path) as wheel:
            matched = 0
            for member in wheel.namelist():
                source = package_root / member
                if not member.endswith(".py") or not source.is_file():
                    continue
                matched += 1
                if sha256_bytes(wheel.read(member)) != sha256_file(source):
                    raise CandidateContractError(
                        f"wheel source byte mismatch: {member}"
                    )
            if matched == 0:
                raise CandidateContractError("wheel source inventory is empty")


def verify_candidate_file_set(
    root: Path,
    manifest: dict[str, JsonValue],
) -> None:
    """Verify exact physical inventory and immutable modes."""
    expected = {"manifest.json", "qualification.json"}
    for name in (
        "source_inventory",
        "input_inventory",
        "wheel_inventory",
        "evidence_inventory",
    ):
        for raw in require_list(manifest.get(name), name):
            entry = require_object(raw, name)
            expected.add(require_string(entry.get("path"), f"{name}.path"))
    actual = {
        path.relative_to(root).as_posix()
        for path in root.rglob("*")
        if path.is_file() and not path.is_symlink()
    }
    if actual != expected:
        raise CandidateContractError("candidate physical inventory mismatch")
    for path in root.rglob("*"):
        if not path.is_symlink() and stat.S_IMODE(
            path.stat(follow_symlinks=False).st_mode
        ) & 0o222:
            raise CandidateContractError("candidate contains writable entry")
