"""The bytes on the wire — NEOf1 v0.2: the frame, and exactly 32 bytes per object.

RETIRED (2026-09-05, owner: "87 bajta ne postoji u našoj arhitekturi"): the four v0.1
structured objects (37/66/75/87 B) below stay only to read old journals and for the
neofx2 side door; the edge no longer emits them. An object on the wire IS the 32-byte
commitment (`commitment()`); the door and a verifier derive `edge_seal == commitment`.
Program, finding, lock amendment and golden vectors: `hashnet-colab/neo-32b`.

The layouts are the RATIFIED NEOf1 edge contract lock (`hashnet-colab/neology`,
`docs/NEOF1_EDGE_CONTRACT_LOCK_v0_1.md`, 2026-08-04); the golden vectors vendored under
`tests/vectors/` are the conformance judge, byte for byte. Three laws from the lock:

  * **Encoding never changes derivation.** Every 32 B value here is a grammar hash
    carried raw — computed by `grammar.py` / `snapshot.py` / `shared_view.py`, never
    here. The wire carries VALUES; it never re-derives.
  * **Volume never leaves the edge; only agreement does.** These objects are attests and
    seals. A business record has no wire form at all.
  * **Structure is judged, business truth never.** Anything malformed is
    `REJECTED_STRUCTURAL` — an unknown type or version is refused, never guessed.

All multi-byte integers are big-endian. Every object begins with
`(object_type, object_version)`. Sizes are fixed: 75 / 37 / 66 / 87 B, frame header
118 B, MAC 16 B, optional Ed25519 signature 64 B.
"""

from __future__ import annotations

import hashlib
import hmac
import re
import struct
from dataclasses import dataclass

from .snapshot import Snapshot

REJECTED_STRUCTURAL = "REJECTED_STRUCTURAL"

OBJECT_VERSION = 0x01

RELATION_PERIOD_ATTEST = 0x01
HANDSHAKE_ATTEST = 0x02
EVENT_PAIR = 0x03
BOOK_SEAL = 0x04

OBJECT_SIZES = {
    RELATION_PERIOD_ATTEST: 75,
    HANDSHAKE_ATTEST: 37,
    EVENT_PAIR: 66,
    BOOK_SEAL: 87,
}
_OBJECT_NAMES = {
    RELATION_PERIOD_ATTEST: "RELATION_PERIOD_ATTEST",
    HANDSHAKE_ATTEST: "HANDSHAKE_ATTEST",
    EVENT_PAIR: "EVENT_PAIR",
    BOOK_SEAL: "BOOK_SEAL",
}

# RATIFIED P2 (2026-08-04): the owner declares the cut ("when I say"); the HANDSHAKE names
# the rule; there is no automatic trigger. Calendar month stays available as a declarable
# rule — it is what the golden vectors use.
PERIOD_DECLARED = 0x00
PERIOD_CALENDAR_MONTH = 0x01

MAGIC = b"NF"
FRAME_VERSION = 0x02          # NEOf1 v0.2: exactly 32 bytes per object (neo-32b)
FRAME_VERSION_RETIRED = 0x01  # v0.1 structured objects: RETIRED, kept readable for old journals
COMMITMENT_SIZE = 32
FLAG_SIGNED = 0x01
FRAME_HEADER_SIZE = 118
MAC_SIZE = 16
SIGNATURE_SIZE = 64

# FLOW REVIEW 5: 1,000 objects per frame — header 118 + MAC 16 over 1,000 objects is
# 0.13 B/object of framing; the frame is never the fat.
OBJECTS_PER_FRAME = 1000


class WireError(Exception):
    """A wire object or frame does not satisfy the contract. Carries a named code —
    `REJECTED_STRUCTURAL`, exactly the verdict a gate would give — never a bare message."""

    def __init__(self, code: str, detail: str):
        super().__init__(detail)
        self.code = code
        self.detail = detail


# ── field guards ──────────────────────────────────────────────────────────────────────


def _hash32(value: bytes, where: str) -> bytes:
    if not isinstance(value, bytes) or len(value) != 32:
        raise WireError(REJECTED_STRUCTURAL, f"{where}: must be 32 raw bytes")
    return value


def _uint(value: int, bits: int, where: str) -> int:
    if isinstance(value, bool) or not isinstance(value, int) or not 0 <= value < (1 << bits):
        raise WireError(REJECTED_STRUCTURAL, f"{where}: must be a uint{bits}")
    return value


# ── the four objects, byte by byte ───────────────────────────────────────────────────


@dataclass(frozen=True)
class RelationPeriodAttest:
    """Type 0x01, the workhorse, 75 B: `{relation_ref, period, event_count,
    agreement_root}`. Both parties submit theirs independently; equal roots settle the
    whole period of the relation — one comparison, however many events stood behind it."""

    relation_ref: bytes
    period_rule: int
    period_label: int
    event_count: int
    agreement_root: bytes

    def __post_init__(self):
        _hash32(self.relation_ref, "relation_ref")
        _uint(self.period_rule, 8, "period_rule")
        _uint(self.period_label, 32, "period_label")
        _uint(self.event_count, 32, "event_count")
        _hash32(self.agreement_root, "agreement_root")

    def pack(self) -> bytes:
        return (
            bytes((RELATION_PERIOD_ATTEST, OBJECT_VERSION))
            + self.relation_ref
            + bytes((self.period_rule,))
            + struct.pack(">II", self.period_label, self.event_count)
            + self.agreement_root
        )


@dataclass(frozen=True)
class HandshakeAttest:
    """Type 0x02, once per relation, 37 B. The fields GRAMMAR_S gives a handshake:
    the relation, its chamber band, and the process-version tag — `legal_contract_ref`
    names, never carries."""

    relation_ref: bytes
    chamber_band: int
    process_tag: int

    def __post_init__(self):
        _hash32(self.relation_ref, "relation_ref")
        _uint(self.chamber_band, 8, "chamber_band")
        _uint(self.process_tag, 16, "process_tag")

    def pack(self) -> bytes:
        return (
            bytes((HANDSHAKE_ATTEST, OBJECT_VERSION))
            + self.relation_ref
            + bytes((self.chamber_band,))
            + struct.pack(">H", self.process_tag)
        )


@dataclass(frozen=True)
class EventPair:
    """Type 0x03, 66 B, sent only for periods that DISAGREE: one `(event_key,
    shared_view_hash)` pair, the unit of Merkle bisection through a disputed period."""

    event_key: bytes
    shared_view_hash: bytes

    def __post_init__(self):
        _hash32(self.event_key, "event_key")
        _hash32(self.shared_view_hash, "shared_view_hash")

    def pack(self) -> bytes:
        return bytes((EVENT_PAIR, OBJECT_VERSION)) + self.event_key + self.shared_view_hash


@dataclass(frozen=True)
class BookSeal:
    """Type 0x04, per member per period, 87 B: the snapshot body in binary. Member
    identity comes from the session, never the object; `request_id` for the stamp is the
    derived `snapshot_hash`, carried by no field."""

    seq: int
    prev_snapshot: bytes
    root: bytes
    record_count: int
    from_seq: int
    to_seq: int
    period_rule: int
    period_label: int

    def __post_init__(self):
        _uint(self.seq, 32, "seq")
        _hash32(self.prev_snapshot, "prev_snapshot")
        _hash32(self.root, "root")
        _uint(self.record_count, 32, "record_count")
        _uint(self.from_seq, 32, "from_seq")
        _uint(self.to_seq, 32, "to_seq")
        _uint(self.period_rule, 8, "period_rule")
        _uint(self.period_label, 32, "period_label")

    def pack(self) -> bytes:
        return (
            bytes((BOOK_SEAL, OBJECT_VERSION))
            + struct.pack(">I", self.seq)
            + self.prev_snapshot
            + self.root
            + struct.pack(">III", self.record_count, self.from_seq, self.to_seq)
            + bytes((self.period_rule,))
            + struct.pack(">I", self.period_label)
        )


WireObject = RelationPeriodAttest | HandshakeAttest | EventPair | BookSeal


def unpack(data: bytes) -> WireObject:
    """One object back from its bytes. Fail-closed: an unknown type or version, or a
    length that is not exactly the type's, is `REJECTED_STRUCTURAL` — never guessed."""
    if not isinstance(data, (bytes, bytearray)) or len(data) < 2:
        raise WireError(REJECTED_STRUCTURAL, "object shorter than its type+version header")
    data = bytes(data)
    obj_type, version = data[0], data[1]
    size = OBJECT_SIZES.get(obj_type)
    if size is None:
        raise WireError(REJECTED_STRUCTURAL, f"unknown object_type 0x{obj_type:02x}")
    if version != OBJECT_VERSION:
        raise WireError(
            REJECTED_STRUCTURAL,
            f"{_OBJECT_NAMES[obj_type]}: unknown object_version 0x{version:02x}",
        )
    if len(data) != size:
        raise WireError(
            REJECTED_STRUCTURAL,
            f"{_OBJECT_NAMES[obj_type]} is {size} B, got {len(data)}",
        )
    if obj_type == RELATION_PERIOD_ATTEST:
        label, count = struct.unpack(">II", data[35:43])
        return RelationPeriodAttest(data[2:34], data[34], label, count, data[43:75])
    if obj_type == HANDSHAKE_ATTEST:
        return HandshakeAttest(data[2:34], data[34], struct.unpack(">H", data[35:37])[0])
    if obj_type == EVENT_PAIR:
        return EventPair(data[2:34], data[34:66])
    seq = struct.unpack(">I", data[2:6])[0]
    count, from_seq, to_seq = struct.unpack(">III", data[70:82])
    label = struct.unpack(">I", data[83:87])[0]
    return BookSeal(seq, data[6:38], data[38:70], count, from_seq, to_seq, data[82], label)


# ── the declared period, on the wire ─────────────────────────────────────────────────

_DATE_LABEL = re.compile(r"^([0-9]{4})-([0-9]{2})-([0-9]{2})$")
_MONTH_LABEL = re.compile(r"^([0-9]{4})-([0-9]{2})$")


def period_fields(label: str | int) -> tuple[int, int]:
    """RATIFIED P2 → wire form: `(period_rule, period_label)` for the label exactly as
    `edge.seal(period_label)` received it.

    An integer (or digit string) is a DECLARED period, carried as itself — the test
    periods 1, 2, 3, 4. A `YYYY-MM-DD` label is a DECLARED cut carried as YYYYMMDD.
    A `YYYY-MM` label is the calendar-month rule, carried as the period's start
    YYYYMM01 — what the golden vectors use. Anything else cannot ride the 4-byte
    field and is refused rather than truncated.
    """
    if isinstance(label, bool):
        raise WireError(REJECTED_STRUCTURAL, "period label: a bool is not a period")
    if isinstance(label, int):
        rule, value = PERIOD_DECLARED, label
    elif isinstance(label, str) and label.isdigit():
        rule, value = PERIOD_DECLARED, int(label)
    elif isinstance(label, str) and (m := _DATE_LABEL.match(label)):
        y, mo, d = (int(part) for part in m.groups())
        rule, value = PERIOD_DECLARED, y * 10000 + mo * 100 + d
    elif isinstance(label, str) and (m := _MONTH_LABEL.match(label)):
        y, mo = (int(part) for part in m.groups())
        rule, value = PERIOD_CALENDAR_MONTH, y * 10000 + mo * 100 + 1
    else:
        raise WireError(
            REJECTED_STRUCTURAL,
            f"period label {label!r} cannot ride the 4-byte wire field — declare an "
            "integer, a YYYY-MM-DD cut, or a YYYY-MM month",
        )
    _uint(value, 32, "period_label")
    return rule, value


def book_seal_from_snapshot(snapshot: Snapshot) -> BookSeal:
    """The existing snapshot, said in 87 bytes. Nothing is re-derived: the root and the
    chain link are carried raw; `member_id` stays with the session, as the lock has it."""
    rule, label = period_fields(snapshot.period)
    return BookSeal(
        seq=snapshot.seq,
        prev_snapshot=bytes.fromhex(snapshot.prev_snapshot),
        root=bytes.fromhex(snapshot.root),
        record_count=snapshot.record_count,
        from_seq=snapshot.from_seq,
        to_seq=snapshot.to_seq,
        period_rule=rule,
        period_label=label,
    )


# ── the frame — header, payload, trailer ─────────────────────────────────────────────


@dataclass(frozen=True)
class FrameHeader:
    """The 118 B fixed header, every field FLOW REVIEW 5 names. The header commits to its
    payload (`payload_hash`) and to its predecessor (`previous_frame_hash`), so the MAC
    over header‖payload covers the chain position too."""

    edge_id: int
    session_id: int
    epoch: int
    frame_seq: int
    first_object_seq: int
    last_object_seq: int
    object_count: int
    payload_hash: bytes
    previous_frame_hash: bytes
    policy_profile_id: int
    signed: bool
    version: int = FRAME_VERSION

    def __post_init__(self):
        if self.version not in (FRAME_VERSION, FRAME_VERSION_RETIRED):
            raise WireError(REJECTED_STRUCTURAL, f"unknown frame_version 0x{self.version:02x}")
        _uint(self.edge_id, 64, "edge_id")
        _uint(self.session_id, 64, "session_id")
        _uint(self.epoch, 32, "epoch")
        _uint(self.frame_seq, 64, "frame_seq")
        _uint(self.first_object_seq, 64, "first_object_seq")
        _uint(self.last_object_seq, 64, "last_object_seq")
        _uint(self.object_count, 16, "object_count")
        _hash32(self.payload_hash, "payload_hash")
        _hash32(self.previous_frame_hash, "previous_frame_hash")
        _uint(self.policy_profile_id, 16, "policy_profile_id")
        if self.object_count == 0:
            raise WireError(REJECTED_STRUCTURAL, "a frame with no objects is never emitted")
        if self.last_object_seq - self.first_object_seq + 1 != self.object_count:
            raise WireError(
                REJECTED_STRUCTURAL,
                "object_count does not match the first/last object sequence range",
            )

    def pack(self) -> bytes:
        return (
            MAGIC
            + bytes((self.version, FLAG_SIGNED if self.signed else 0x00))
            + struct.pack(">QQIQ", self.edge_id, self.session_id, self.epoch, self.frame_seq)
            + struct.pack(">QQH", self.first_object_seq, self.last_object_seq, self.object_count)
            + self.payload_hash
            + self.previous_frame_hash
            + struct.pack(">HH", self.policy_profile_id, 0)
        )

    @staticmethod
    def parse(data: bytes) -> "FrameHeader":
        if len(data) < FRAME_HEADER_SIZE:
            raise WireError(REJECTED_STRUCTURAL, f"frame header is {FRAME_HEADER_SIZE} B, got {len(data)}")
        if data[0:2] != MAGIC:
            raise WireError(REJECTED_STRUCTURAL, "not a frame: bad magic")
        if data[2] not in (FRAME_VERSION, FRAME_VERSION_RETIRED):
            raise WireError(REJECTED_STRUCTURAL, f"unknown frame_version 0x{data[2]:02x}")
        flags = data[3]
        if flags & ~FLAG_SIGNED:
            raise WireError(REJECTED_STRUCTURAL, f"unknown flag bits 0x{flags:02x}")
        edge_id, session_id, epoch, frame_seq = struct.unpack(">QQIQ", data[4:32])
        first_seq, last_seq, count = struct.unpack(">QQH", data[32:50])
        policy, reserved = struct.unpack(">HH", data[114:118])
        if reserved != 0:
            raise WireError(REJECTED_STRUCTURAL, "reserved header bytes must be zero")
        return FrameHeader(
            edge_id=edge_id,
            session_id=session_id,
            epoch=epoch,
            frame_seq=frame_seq,
            first_object_seq=first_seq,
            last_object_seq=last_seq,
            object_count=count,
            payload_hash=data[50:82],
            previous_frame_hash=data[82:114],
            policy_profile_id=policy,
            signed=bool(flags & FLAG_SIGNED),
            version=data[2],
        )


def commitment(value: bytes) -> bytes:
    """The v0.2 object: exactly 32 raw bytes — a grammar hash carried as itself. This is
    all that ever crosses the door; the structured objects above are RETIRED (they are
    kept for reading old journals and for neofx2's side door, nothing else)."""
    if not isinstance(value, (bytes, bytearray)) or len(value) != COMMITMENT_SIZE:
        raise WireError(REJECTED_STRUCTURAL,
                        f"a commitment is exactly {COMMITMENT_SIZE} B — nothing else crosses the door")
    return bytes(value)


def frame_mac(key: bytes, header_bytes: bytes, payload: bytes) -> bytes:
    """The trailer MAC: HMAC-SHA-256 over header‖payload with the session key, truncated
    to 16 B (lock [CHOICE] P5)."""
    return hmac.new(key, header_bytes + payload, hashlib.sha256).digest()[:MAC_SIZE]


def build_frame(header: FrameHeader, payload: bytes, *, key: bytes, signer=None) -> bytes:
    """Header ‖ payload ‖ MAC [‖ signature]. The caller supplies a signer exactly when the
    header says the frame is a checkpoint — a mismatch is a bug, so it is refused."""
    if hashlib.sha256(payload).digest() != header.payload_hash:
        raise WireError(REJECTED_STRUCTURAL, "header.payload_hash does not match the payload")
    if header.signed and signer is None:
        raise WireError(REJECTED_STRUCTURAL, "a checkpoint frame needs a signer")
    if not header.signed and signer is not None:
        raise WireError(REJECTED_STRUCTURAL, "a signer was given but the header is not marked signed")
    header_bytes = header.pack()
    frame = header_bytes + payload + frame_mac(key, header_bytes, payload)
    if header.signed:
        signature = signer(header_bytes + payload)
        if not isinstance(signature, bytes) or len(signature) != SIGNATURE_SIZE:
            raise WireError(REJECTED_STRUCTURAL, f"signature must be {SIGNATURE_SIZE} B")
        frame += signature
    return frame


def parse_frame(data: bytes, *, key: bytes) -> tuple[FrameHeader, list[bytes], bool, bytes | None]:
    """One frame back into `(header, objects, mac_valid, signature)`.

    Layout faults raise `REJECTED_STRUCTURAL`; MAC validity is returned, not raised,
    because rejecting is the receiving gate's verdict to give, not the parser's. Objects
    come back as raw slices — `unpack()` each when the content matters.
    """
    header = FrameHeader.parse(data[:FRAME_HEADER_SIZE])
    offset = FRAME_HEADER_SIZE
    objects: list[bytes] = []
    for _ in range(header.object_count):
        if header.version == FRAME_VERSION:
            size = COMMITMENT_SIZE                       # v0.2: every object is 32 B
        else:
            if len(data) < offset + 2:
                raise WireError(REJECTED_STRUCTURAL, "payload ends inside an object header")
            size = OBJECT_SIZES.get(data[offset])
            if size is None:
                raise WireError(REJECTED_STRUCTURAL, f"unknown object_type 0x{data[offset]:02x} in payload")
        if len(data) < offset + size:
            raise WireError(REJECTED_STRUCTURAL, "payload ends inside an object")
        objects.append(data[offset:offset + size])
        offset += size
    payload = data[FRAME_HEADER_SIZE:offset]
    trailer = data[offset:]
    expected = MAC_SIZE + (SIGNATURE_SIZE if header.signed else 0)
    if len(trailer) != expected:
        raise WireError(REJECTED_STRUCTURAL, f"trailer is {expected} B for this frame, got {len(trailer)}")
    if hashlib.sha256(payload).digest() != header.payload_hash:
        raise WireError(REJECTED_STRUCTURAL, "payload does not match header.payload_hash")
    mac_valid = hmac.compare_digest(frame_mac(key, data[:FRAME_HEADER_SIZE], payload), trailer[:MAC_SIZE])
    signature = trailer[MAC_SIZE:] if header.signed else None
    return header, objects, mac_valid, signature


def frame_hash(frame: bytes) -> bytes:
    """The chain link: SHA-256 over the full frame exactly as transmitted — header,
    payload and trailer, signature included when present ([CHOICE], pinned here and in
    the derivation vectors). The next frame carries this as `previous_frame_hash`; a
    re-sent frame is byte-identical, so its hash is too."""
    return hashlib.sha256(frame).digest()
