"""Canonical pre-execution product derivation for Task26 candidate identity."""
from __future__ import annotations

import hashlib
import json
import re
import stat
from pathlib import Path
from typing import Mapping, cast

_HEX = re.compile(r"^[0-9a-f]{64}$")
_TOOL_KEYS = {
    "candidate_derivation",
    "evidence_contract",
    "final_state",
    "installed_provenance",
    "source_golden_path",
    "verify_source_golden_path",
}
_EMPTY_WHEELHOUSE_PAYLOAD = {
    "root_mode": None,
    "directories": [],
    "entries": [],
}
EMPTY_WHEELHOUSE_INVENTORY_SHA256 = hashlib.sha256(
    json.dumps(
        _EMPTY_WHEELHOUSE_PAYLOAD,
        ensure_ascii=False,
        sort_keys=True,
        separators=(",", ":"),
        allow_nan=False,
    ).encode()
).hexdigest()


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


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


def trust_boundary_digest(trust_boundary: Mapping[str, object]) -> str:
    return hashlib.sha256(canonical(dict(trust_boundary))).hexdigest()


def qualification_tool_hashes(
    *, scripts_dir: Path, package_root: Path
) -> dict[str, str]:
    paths = {
        "candidate_derivation": package_root
        / "gateway/platforms/task26_candidate_derivation.py",
        "evidence_contract": package_root
        / "gateway/platforms/task26_evidence_contract.py",
        "final_state": package_root / "gateway/platforms/task26_final_state.py",
        "installed_provenance": scripts_dir / "installed_wheel_provenance.py",
        "source_golden_path": scripts_dir / "source_golden_path.py",
        "verify_source_golden_path": scripts_dir / "verify_source_golden_path.py",
    }
    return {name: sha256_file(path) for name, path in sorted(paths.items())}


def _inputs(value: object) -> dict[str, object]:
    if not isinstance(value, dict) or set(value) != {
        "schema",
        "generation",
        "hermes_wheel_sha256",
        "profile_wheel_sha256",
        "wheelhouse_inventory_sha256",
        "wheelhouse_entry_count",
        "trust_boundary_sha256",
        "qualification_tool_sha256",
    }:
        raise ValueError("candidate derivation input schema is invalid")
    inputs = cast(dict[str, object], value)
    tools = inputs.get("qualification_tool_sha256")
    digest_fields = (
        "hermes_wheel_sha256",
        "profile_wheel_sha256",
        "wheelhouse_inventory_sha256",
        "trust_boundary_sha256",
    )
    if (
        inputs.get("schema")
        != "task26-preexecution-product-derivation-inputs-v1"
        or inputs.get("generation") != 1
        or any(_HEX.fullmatch(str(inputs.get(field, ""))) is None for field in digest_fields)
        or type(inputs.get("wheelhouse_entry_count")) is not int
        or cast(int, inputs["wheelhouse_entry_count"]) < 0
        or not isinstance(tools, dict)
        or set(tools) != _TOOL_KEYS
        or any(_HEX.fullmatch(str(item)) is None for item in tools.values())
        or inputs["hermes_wheel_sha256"] == inputs["profile_wheel_sha256"]
        or (
            inputs["wheelhouse_entry_count"] == 0
            and inputs["wheelhouse_inventory_sha256"]
            != EMPTY_WHEELHOUSE_INVENTORY_SHA256
        )
    ):
        raise ValueError("candidate derivation inputs are invalid")
    return inputs


def candidate_digest_from_inputs(value: object) -> str:
    inputs = _inputs(value)
    return hashlib.sha256(canonical(inputs)).hexdigest()


def build_product_binding(value: object) -> dict[str, object]:
    inputs = _inputs(value)
    document: dict[str, object] = {
        "schema": "task26-preexecution-product-binding-v1",
        "derivation_inputs": inputs,
        "candidate_digest": candidate_digest_from_inputs(inputs),
    }
    document["binding_sha256"] = hashlib.sha256(canonical(document)).hexdigest()
    return document


def _private_wheel(path: Path, label: str) -> None:
    info = path.lstat()
    if (
        stat.S_ISLNK(info.st_mode)
        or not stat.S_ISREG(info.st_mode)
        or info.st_nlink != 1
        or stat.S_IMODE(info.st_mode) not in {0o600, 0o400}
    ):
        raise ValueError(f"actual sealed wheel is invalid: {label}")


def validate_product_binding(
    value: object,
    *,
    expected_tool_hashes: Mapping[str, str] | None = None,
    expected_trust_boundary_sha256: str | None = None,
    expected_wheelhouse_inventory_sha256: str | None = None,
    expected_wheelhouse_entry_count: int | None = None,
    actual_hermes_wheel: Path | None = None,
    actual_profile_wheel: Path | None = None,
) -> dict[str, object]:
    if not isinstance(value, dict) or set(value) != {
        "schema",
        "derivation_inputs",
        "candidate_digest",
        "binding_sha256",
    }:
        raise ValueError("product binding schema is invalid")
    document = cast(dict[str, object], value)
    inputs = _inputs(document.get("derivation_inputs"))
    unsigned = {
        key: item for key, item in document.items() if key != "binding_sha256"
    }
    recomputed_candidate = candidate_digest_from_inputs(inputs)
    if (
        document.get("schema") != "task26-preexecution-product-binding-v1"
        or document.get("candidate_digest") != recomputed_candidate
        or document.get("binding_sha256")
        != hashlib.sha256(canonical(unsigned)).hexdigest()
    ):
        raise ValueError("candidate does not equal canonical derivation inputs")
    if expected_tool_hashes is not None and inputs["qualification_tool_sha256"] != dict(
        expected_tool_hashes
    ):
        raise ValueError("qualification tool hashes differ")
    if (
        expected_trust_boundary_sha256 is not None
        and inputs["trust_boundary_sha256"] != expected_trust_boundary_sha256
    ):
        raise ValueError("trust boundary digest differs")
    if (
        expected_wheelhouse_inventory_sha256 is not None
        and inputs["wheelhouse_inventory_sha256"]
        != expected_wheelhouse_inventory_sha256
    ) or (
        expected_wheelhouse_entry_count is not None
        and inputs["wheelhouse_entry_count"] != expected_wheelhouse_entry_count
    ):
        raise ValueError("wheelhouse derivation binding differs")
    for path, field, label in (
        (actual_hermes_wheel, "hermes_wheel_sha256", "Hermes"),
        (actual_profile_wheel, "profile_wheel_sha256", "profile"),
    ):
        if path is not None:
            _private_wheel(path, label)
            if sha256_file(path) != inputs[field]:
                raise ValueError(f"actual sealed wheel differs: {label}")
    return document


def validate_deployment_receipt(
    receipt: object, product_binding: object
) -> dict[str, object]:
    binding = validate_product_binding(product_binding)
    inputs = cast(dict[str, object], binding["derivation_inputs"])
    if not isinstance(receipt, dict):
        raise ValueError("deployment receipt is invalid")
    document = cast(dict[str, object], receipt)
    expected = {
        "schema": "task26-source-deployment-receipt-v2",
        "candidate_digest": binding["candidate_digest"],
        "candidate_product_binding_sha256": binding["binding_sha256"],
        "hermes_wheel_sha256": inputs["hermes_wheel_sha256"],
        "profile_wheel_sha256": inputs["profile_wheel_sha256"],
    }
    if any(document.get(key) != item for key, item in expected.items()):
        raise ValueError("deployment receipt wheel or product binding differs")
    if binding["candidate_digest"] in {
        document.get("hermes_wheel_sha256"),
        document.get("profile_wheel_sha256"),
    }:
        raise ValueError("deployment wheel field cannot equal candidate digest")
    return document
