"""S6 — hierarchical closing + the annual EraSeal.

BUILD PLAN v1.1 §8: objects → hour → day → quarter → inventory → signed
annual account → EraSeal. The one binding law: **each tier closes over the
ROOTS of the tier below, never by re-reading objects.** An hour record
links its closed (witnessed) records; a day links its hours; a quarter its
days; the annual its quarters. Time triggers the close; the position range
decides membership. An empty period is a light heartbeat record.

From the annual EraSeal one can provably descend: annual → quarter → day →
hour → the witnessed record → the individual commitment (each step a Merkle
membership proof, reusing S5's tree). The seal proves WHAT the Chamber
commission signed; it never proclaims business truth (core attests, never
decides).

Hour-close pruning lives with the shelf/sequencer (drop the in-RAM index of
a closed+archived hour); the durable log and this hour record stay the
authority, so a pruned range is still provable from the seal skeleton.
"""

from __future__ import annotations

import hashlib
import struct
from dataclasses import dataclass, field

from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey

from .witness import merkle_proof, merkle_root, verify_membership


def _sha(*p: bytes) -> bytes:
    h = hashlib.sha256()
    for x in p:
        h.update(x)
    return h.digest()


HOUR, DAY, QUARTER, ANNUAL = "HOUR", "DAY", "QUARTER", "ANNUAL"
ZERO = b"\x00" * 32


@dataclass(frozen=True)
class TierRecord:
    """One closing record at any tier. `root` is the Merkle root over the
    CHILD record hashes (roots, not objects). Empty period → heartbeat
    (n_children 0, root over the empty set), still chained."""
    tier: str
    period_id: int
    first_position: int
    last_position: int
    object_count: int
    n_children: int
    root: bytes                    # Merkle over child record_hashes
    prev_hash: bytes               # link to the previous record of this tier

    def record_hash(self) -> bytes:
        return _sha(b"NVTIER1", self.tier.encode(),
                    struct.pack(">QQQQQ", self.period_id, self.first_position,
                                self.last_position, self.object_count,
                                self.n_children),
                    self.root, self.prev_hash)


def _child_view(c):
    """A child is anything exposing first/last position, object_count and a
    record_hash(): a witness ClosedRecord (via range) or a TierRecord."""
    if hasattr(c, "range_start"):              # S5 ClosedRecord
        first = c.range_start
        last = c.range_start + c.count - 1 if c.count else c.range_start
        return first, last, c.count, c.record_hash()
    return (c.first_position, c.last_position, c.object_count,
            c.record_hash())


def close_tier(tier: str, period_id: int, children: list,
               prev_hash: bytes = ZERO) -> TierRecord:
    """Close one period over its children's ROOTS. Children must be in
    canonical (position) order — the caller closes in order."""
    views = [_child_view(c) for c in children]
    if not views:
        return TierRecord(tier, period_id, 0, 0, 0, 0,
                          merkle_root([]), prev_hash)   # heartbeat
    root = merkle_root([v[3] for v in views])           # over child hashes
    first = views[0][0]
    last = views[-1][1]
    count = sum(v[2] for v in views)
    return TierRecord(tier, period_id, first, last, count, len(views),
                      root, prev_hash)


# ------------------------------------------- descent proof (reachability)

@dataclass(frozen=True)
class DescentStep:
    child: "TierRecord"            # the chosen child record at this tier
    child_index: int
    proof: list                    # Merkle audit path of child in parent root


def descent_proof(parent_child_hashes: list, chosen: list) -> list:
    """Build the top-down audit path. `parent_child_hashes[i]` = the list of
    child record hashes closed under tier i (top-first); `chosen[i]` = the
    TierRecord we descend into at tier i. Each step proves
    `chosen[i].record_hash()` is a member of tier i's root."""
    steps = []
    for child_hashes, rec in zip(parent_child_hashes, chosen):
        idx = child_hashes.index(rec.record_hash())
        steps.append(DescentStep(rec, idx, merkle_proof(child_hashes, idx)))
    return steps


def verify_descent(top_root: bytes, steps: list) -> bool:
    """Verify a reachable path top → … → the final child. At each tier the
    chosen child's record_hash must be a member of the current root; then
    we descend into that child's OWN root (which its record_hash binds). So
    the annual root provably reaches the quarter, day, hour record — and
    from that record, S5's Merkle reaches the individual commitment."""
    expected_root = top_root
    for step in steps:
        if not verify_membership(step.child.record_hash(), step.child_index,
                                 step.proof, expected_root):
            return False
        expected_root = step.child.root        # descend into the child's root
    return True


# ------------------------------------------- annual inventory + EraSeal

@dataclass(frozen=True)
class Correction:
    """An inventory finding. NEVER an edit — a NEW linked record. kind ∈
    {shortage, surplus, duplicate, wrong_link}."""
    kind: str
    detail: str
    links_to: bytes                # the record_hash it corrects/annotates


@dataclass(frozen=True)
class AnnualInventory:
    annual_record_hash: bytes
    corrections: tuple = field(default_factory=tuple)

    def protocol_hash(self) -> bytes:
        h = hashlib.sha256()
        h.update(b"NVINV1")
        h.update(self.annual_record_hash)
        for c in self.corrections:             # order-fixed, append-only
            h.update(_sha(c.kind.encode(), c.detail.encode(), c.links_to))
        return h.digest()


class SealError(Exception):
    pass


@dataclass(frozen=True)
class EraSeal:
    annual_record_hash: bytes
    inventory_protocol_hash: bytes
    signatures: tuple              # ((signer_id, sig), ...)
    k: int

    def sealed_payload(self) -> bytes:
        return _sha(b"NVERASEAL1", self.annual_record_hash,
                    self.inventory_protocol_hash)

    def seal_hash(self) -> bytes:
        h = hashlib.sha256()
        h.update(self.sealed_payload())
        for sid, sig in self.signatures:
            h.update(_sha(sid, sig))
        return h.digest()


def create_era_seal(annual: TierRecord, inventory: AnnualInventory,
                    signatures: list, k: int,
                    commission: dict) -> EraSeal:
    """Create the annual EraSeal — only after the Chamber commission has
    signed. Requires at least `k` valid commission signatures over
    (annual account + inventory protocol). The seal proves what the
    commission signed; it never proclaims business truth."""
    if inventory.annual_record_hash != annual.record_hash():
        raise SealError("inventory does not bind THIS annual account")
    payload = _sha(b"NVERASEAL1", annual.record_hash(),
                   inventory.protocol_hash())
    valid = []
    seen = set()
    for sid, sig in signatures:
        if sid in seen or sid not in commission:
            continue                           # duplicate or non-commissioner
        try:
            commission[sid].verify(sig, payload)
            valid.append((sid, sig))
            seen.add(sid)
        except InvalidSignature:
            pass
    if len(valid) < k:
        raise SealError(
            f"only {len(valid)} valid commission signatures, need {k}")
    return EraSeal(annual.record_hash(), inventory.protocol_hash(),
                   tuple(sorted(valid)), k)


def verify_era_seal(seal: EraSeal, commission: dict) -> bool:
    """Independently verify a seal: ≥k distinct valid commission
    signatures over exactly the annual account + inventory it names."""
    payload = seal.sealed_payload()
    ok = 0
    seen = set()
    for sid, sig in seal.signatures:
        if sid in seen or sid not in commission:
            return False
        try:
            commission[sid].verify(sig, payload)
        except InvalidSignature:
            return False
        seen.add(sid)
        ok += 1
    return ok >= seal.k
