"""RFC 6962-style Merkle batch over `artifact_hash` leaves (§2.5, Python-side).

The batch Merkle tree + its inclusion proofs live ENTIRELY here (guardrail #2): only the `batch_root` (an
opaque 32 bytes) ever crosses to NEO. NEO never re-folds this tree — `batch_root` is one opaque leaf of NEO's
own evidence fold — so there is no cross-language byte-identity requirement. Leaf/node hashing is
domain-separated (0x00 / 0x01 prefixes, RFC 6962). The hash function is carried by a HashProfile: new batches
default to DEFAULT_PROFILE (SHA-256, Canon Lock v1); old SHA3 batches verify by passing PROFILE_SHA3_256_V1.
Only the hash function changes via the profile — the RFC 6962 structure is identical.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import List

from cl.canonical.hash_profile import DEFAULT_PROFILE, HashProfile

_LEAF = b"\x00"
_NODE = b"\x01"


def _leaf_hash(data: bytes, profile: HashProfile) -> bytes:
    return profile.digest(_LEAF + data)


def _node_hash(left: bytes, right: bytes, profile: HashProfile) -> bytes:
    return profile.digest(_NODE + left + right)


@dataclass(frozen=True)
class InclusionProof:
    """An audit path proving a leaf is in the batch. `audit_path` is bottom-up; each step carries the sibling
    and whether the sibling is on the right (the running node is then the left input)."""

    leaf_index: int
    leaf_count: int
    leaf_hash: bytes
    audit_path: tuple  # tuple[ (sibling_hash: bytes, sibling_is_right: bool) ]

    def root(self, profile: HashProfile = DEFAULT_PROFILE) -> bytes:
        node = self.leaf_hash
        for sibling, sibling_is_right in self.audit_path:
            node = (
                _node_hash(node, sibling, profile)
                if sibling_is_right
                else _node_hash(sibling, node, profile)
            )
        return node

    def verifies_to(self, batch_root: bytes, profile: HashProfile = DEFAULT_PROFILE) -> bool:
        return self.root(profile) == batch_root


def _level_root(level: List[bytes], profile: HashProfile) -> bytes:
    while len(level) > 1:
        nxt = []
        for i in range(0, len(level), 2):
            if i + 1 < len(level):
                nxt.append(_node_hash(level[i], level[i + 1], profile))
            else:
                nxt.append(level[i])  # promote the lone last node (RFC 6962)
        level = nxt
    return level[0]


def batch_root(artifact_hashes: List[bytes], profile: HashProfile = DEFAULT_PROFILE) -> bytes:
    """The opaque batch root over the leaves. Empty batch is rejected (nothing to anchor)."""
    if not artifact_hashes:
        raise ValueError("cannot anchor an empty batch")
    return _level_root([_leaf_hash(h, profile) for h in artifact_hashes], profile)


def inclusion_proof(
    artifact_hashes: List[bytes], leaf_index: int, profile: HashProfile = DEFAULT_PROFILE
) -> InclusionProof:
    """Build the audit path for `leaf_index` over the batch leaves."""
    if not (0 <= leaf_index < len(artifact_hashes)):
        raise IndexError("leaf_index out of range")
    leaves = [_leaf_hash(h, profile) for h in artifact_hashes]
    target_leaf = leaves[leaf_index]
    path = []
    level = leaves
    idx = leaf_index
    while len(level) > 1:
        nxt = []
        for i in range(0, len(level), 2):
            if i + 1 < len(level):
                parent = _node_hash(level[i], level[i + 1], profile)
                if i == idx:  # our node is the left child → sibling on the right
                    path.append((level[i + 1], True))
                elif i + 1 == idx:  # our node is the right child → sibling on the left
                    path.append((level[i], False))
            else:
                parent = level[i]  # promoted; no sibling at this level
            nxt.append(parent)
        idx //= 2
        level = nxt
    return InclusionProof(
        leaf_index=leaf_index,
        leaf_count=len(artifact_hashes),
        leaf_hash=target_leaf,
        audit_path=tuple(path),
    )
