//! `commonware-codec` `Write + Read + EncodeSize` impls for the **consensus
//! envelope types** — `Height`, `ConsensusContext`, `BlockHeader`.
//!
//! The canonical block-identity input is
//! `keccak256(commonware_codec::encode(&block_header).to_vec())`, so the
//! hash chain shares the same codec discipline as `MessageData` (already
//! commonware-codec in `crates/makechain-crypto/src/message.rs`).
//!
//! ## Scope of this file
//!
//! Only the types reachable from `BlockHeader` are migrated here, because
//! `BlockHeader` is the **single** input to the canonical block hash
//! (see `crates/makechain-consensus/src/block.rs::compute_block_hash`).
//! The outer `Block` envelope continues to travel buffa-encoded on the
//! gRPC wire — its contents (`user_messages`, `account_messages`,
//! `dealer_log`, `dkg_outcome`) are all header-bound via dedicated hash
//! fields and do not participate in the block-identity computation directly.
//! The finalization certificate is not on the body (#651): it rides the
//! `GetBlockResponse` / `GetSyncTargetResponse` envelope siblings.
//!
//! Staying outside scope:
//!
//! - `Block` (gRPC envelope; buffa wire format pinned by `BlockEncoding`
//!   conformance) — does not need a codec impl. If a follow-up cleanup
//!   wants a codec-encoded `Block` for marshal storage, that's a separate
//!   migration.
//! - `Message` / `MessageData` — `MessageData` is already codec-encoded
//!   (canonical hash input); `Message` is the outer signed envelope and
//!   is buffa-encoded on the gRPC wire.
//! - All gRPC request/response wrapper types — these live in buffa.
//!
//! ## Wire format conventions
//!
//! Same as `crate::codec`:
//! - `u32` is 4 bytes BE; `u64` is 8 bytes BE; `bool` is 1 byte.
//! - Fixed-size byte fields encoded as raw bytes with no length prefix
//!   (panic on `Write` if length is wrong, matching `write_fixed`).
//! - Variable-length `Vec<u8>` uses commonware-codec's varint length
//!   prefix.
//! - Nested `MessageField<T>` (buffa's wrapper for proto3
//!   `optional`/sub-message) is encoded as a 1-byte presence tag plus
//!   the inner payload when present. The on-wire shape:
//!   `false` (1 byte) means absent; `true` (1 byte) followed by inner
//!   `T::write(buf)` means present. This matches the `Option<[u8; N]>`
//!   precedent in `crate::codec` and makes the encoding **total** for
//!   arbitrary in-memory states (default `MessageField::none()`
//!   round-trips losslessly).
//! - Field order follows proto tag number, matching the in-repo
//!   precedent in `crate::codec`.

use buffa::UnknownFields;
use bytes::{Buf, BufMut};
use commonware_codec::{EncodeSize, Error as CodecError, Read, ReadExt, Write};

use crate::{BlockHeader, ConsensusContext, Height};

// ---------------------------------------------------------------------------
// Sizes
// ---------------------------------------------------------------------------

const HASH_LEN: usize = 32;
const PUBLIC_KEY_LEN: usize = 32;

// `BlockHeader.dkg_outcome_hash` / `dealer_log_hash` are either 32 bytes
// (present) or 0 bytes (absent). Encoded as `Option<[u8; 32]>` via a
// 1-byte presence tag; matches the `write_optional_fixed` pattern in
// `crate::codec`.
fn write_optional_hash(field: &str, bytes: &[u8], buf: &mut impl BufMut) {
    if bytes.is_empty() {
        false.write(buf);
    } else {
        assert!(
            bytes.len() == HASH_LEN,
            "{field} has invalid length {} (expected 0 or {HASH_LEN})",
            bytes.len()
        );
        true.write(buf);
        buf.put_slice(bytes);
    }
}

fn optional_hash_encode_size(bytes: &[u8]) -> usize {
    if bytes.is_empty() { 1 } else { 1 + HASH_LEN }
}

fn read_optional_hash(buf: &mut impl Buf) -> Result<Vec<u8>, CodecError> {
    if bool::read(buf)? {
        let bytes = <[u8; HASH_LEN]>::read(buf)?;
        Ok(bytes.to_vec())
    } else {
        Ok(Vec::new())
    }
}

/// Write a fixed-length byte field, treating an empty buffa `Vec<u8>` as
/// the proto3-default zero-bytes value (which buffa elides from the
/// wire). This keeps the canonical encoding lossless across the
/// `MessageField::default()` / explicit-`vec![0; N]` boundary that
/// buffa-built fixtures freely cross.
fn write_fixed(field: &str, expected_len: usize, bytes: &[u8], buf: &mut impl BufMut) {
    if bytes.is_empty() {
        for _ in 0..expected_len {
            buf.put_u8(0);
        }
        return;
    }
    assert!(
        bytes.len() == expected_len,
        "{field} has invalid length {} (expected 0 or {expected_len})",
        bytes.len()
    );
    buf.put_slice(bytes);
}

fn read_fixed<const N: usize>(buf: &mut impl Buf) -> Result<Vec<u8>, CodecError> {
    let bytes = <[u8; N]>::read(buf)?;
    Ok(bytes.to_vec())
}

// ---------------------------------------------------------------------------
// Height
//
// fields:
//   - block_number(2) u64
//
// Single-field message; encoded as a bare 8-byte BE u64. Field tag is
// elided (the codec is canonical, not self-describing; tag 1 is
// reserved).
// ---------------------------------------------------------------------------

impl Write for Height {
    fn write(&self, buf: &mut impl BufMut) {
        self.block_number.write(buf);
    }
}

impl EncodeSize for Height {
    fn encode_size(&self) -> usize {
        8
    }
}

impl Read for Height {
    type Cfg = ();
    fn read_cfg(buf: &mut impl Buf, _cfg: &()) -> Result<Self, CodecError> {
        Ok(Self {
            block_number: u64::read(buf)?,
            __buffa_unknown_fields: UnknownFields::default(),
        })
    }
}

// ---------------------------------------------------------------------------
// ConsensusContext
//
// fields:
//   - round_epoch(1) u64
//   - round_view(2) u64
//   - leader_public_key(3) bytes[32]
//   - parent_view(4) u64
//
// Mirrors `commonware_consensus::simplex::types::Context<D, P>`:
//   * `round_epoch` / `round_view` -> `Round { epoch, view }`
//   * `leader_public_key`           -> `P` (32-byte Ed25519 PublicKey)
//   * `parent_view`                 -> `parent: (View, D)` (D recovered from `BlockHeader.parent_hash`)
//
// Encoded as 8 + 8 + 32 + 8 = 56 bytes flat, no field tags.
// ---------------------------------------------------------------------------

impl Write for ConsensusContext {
    fn write(&self, buf: &mut impl BufMut) {
        self.round_epoch.write(buf);
        self.round_view.write(buf);
        write_fixed(
            "ConsensusContext.leader_public_key",
            PUBLIC_KEY_LEN,
            &self.leader_public_key,
            buf,
        );
        self.parent_view.write(buf);
    }
}

impl EncodeSize for ConsensusContext {
    fn encode_size(&self) -> usize {
        8 + 8 + PUBLIC_KEY_LEN + 8
    }
}

impl Read for ConsensusContext {
    type Cfg = ();
    fn read_cfg(buf: &mut impl Buf, _cfg: &()) -> Result<Self, CodecError> {
        Ok(Self {
            round_epoch: u64::read(buf)?,
            round_view: u64::read(buf)?,
            leader_public_key: read_fixed::<PUBLIC_KEY_LEN>(buf)?,
            parent_view: u64::read(buf)?,
            __buffa_unknown_fields: UnknownFields::default(),
        })
    }
}

// ---------------------------------------------------------------------------
// BlockHeader
//
// The canonical block-identity input. `compute_block_hash(block) =
// keccak256(BlockHeader::encode(&block.header).to_vec())`.
//
// fields:
//   - height(1) Height                  -> 8 bytes
//   - timestamp(2) u64                  -> 8 bytes
//   - parent_hash(5) bytes[32]          -> 32 bytes
//   - state_root(6) bytes[32]           -> 32 bytes
//   - context(7) ConsensusContext       -> 1-byte presence + 56 bytes when present
//   - dkg_outcome_hash(8) bytes[0|32]   -> 1-byte presence + 32 bytes when present
//   - dealer_log_hash(9) bytes[0|32]    -> 1-byte presence + 32 bytes when present
//   - transactions_root(10) bytes[0|32] -> 1-byte presence + 32 bytes when present
//   - ops_root(11) bytes[0|32]          -> 1-byte presence + 32 bytes when present
//   - ops_range_start(12) u64           -> 8 bytes
//   - ops_range_end(13) u64             -> 8 bytes
//
// Reserved proto fields (3 = `version`, 4 = `chain_id`) do not appear on
// the wire — they were dropped by Cleanups #5 and #9.
//
// `context` is a buffa `MessageField<ConsensusContext>`; we encode it as
// `Option<ConsensusContext>` via a 1-byte presence tag. Callers either
// pass `MessageField::none()` (zero context) or the simplex
// `Context<D, P>` the proposer used. Both round-trip losslessly.
//
// `transactions_root` is the commonware-storage BMT root over
// (account_messages || user_messages) message hashes; zero-length when
// the block carries no messages. Encoded via the same
// `Option<[u8; 32]>` 1-byte-presence shape as the other optional
// 32-byte hashes on this header.
// ---------------------------------------------------------------------------

impl Write for BlockHeader {
    fn write(&self, buf: &mut impl BufMut) {
        // Field 1: height — required sub-message. We treat an absent
        // `MessageField` as `Height { block_number: 0 }` for canonical
        // encoding (matches buffa's proto3 default behaviour).
        let height = self.height.as_option().cloned().unwrap_or_default();
        height.write(buf);

        // Field 2: timestamp
        self.timestamp.write(buf);

        // Field 5: parent_hash (32 bytes)
        write_fixed("BlockHeader.parent_hash", HASH_LEN, &self.parent_hash, buf);

        // Field 6: state_root (32 bytes)
        write_fixed("BlockHeader.state_root", HASH_LEN, &self.state_root, buf);

        // Field 7: context — optional sub-message (1-byte presence tag)
        match self.context.as_option() {
            None => false.write(buf),
            Some(ctx) => {
                true.write(buf);
                ctx.write(buf);
            }
        }

        // Field 8: dkg_outcome_hash — optional 32-byte hash
        write_optional_hash("BlockHeader.dkg_outcome_hash", &self.dkg_outcome_hash, buf);

        // Field 9: dealer_log_hash — optional 32-byte hash
        write_optional_hash("BlockHeader.dealer_log_hash", &self.dealer_log_hash, buf);

        // Field 10: transactions_root — optional 32-byte BMT root
        write_optional_hash(
            "BlockHeader.transactions_root",
            &self.transactions_root,
            buf,
        );

        // #698 step 3: ops_root + op-range (glue sync target source).
        // Field 11: ops_root — optional 32-byte QMDB ops-only MMR root
        write_optional_hash("BlockHeader.ops_root", &self.ops_root, buf);
        // Field 12 / 13: op-count range `[start, end)` (8 bytes BE each)
        self.ops_range_start.write(buf);
        self.ops_range_end.write(buf);
    }
}

impl EncodeSize for BlockHeader {
    fn encode_size(&self) -> usize {
        let context_size = match self.context.as_option() {
            None => 1,
            Some(ctx) => 1 + ctx.encode_size(),
        };
        // height + timestamp + parent_hash + state_root + context +
        // dkg_outcome_hash + dealer_log_hash + transactions_root +
        // ops_root + ops_range_start + ops_range_end  (#698 step 3)
        8 + 8
            + HASH_LEN
            + HASH_LEN
            + context_size
            + optional_hash_encode_size(&self.dkg_outcome_hash)
            + optional_hash_encode_size(&self.dealer_log_hash)
            + optional_hash_encode_size(&self.transactions_root)
            + optional_hash_encode_size(&self.ops_root)
            + 8
            + 8
    }
}

impl Read for BlockHeader {
    type Cfg = ();
    fn read_cfg(buf: &mut impl Buf, _cfg: &()) -> Result<Self, CodecError> {
        let height = Height::read(buf)?;
        let timestamp = u64::read(buf)?;
        let parent_hash = read_fixed::<HASH_LEN>(buf)?;
        let state_root = read_fixed::<HASH_LEN>(buf)?;
        let context = if bool::read(buf)? {
            ::buffa::MessageField::some(ConsensusContext::read(buf)?)
        } else {
            ::buffa::MessageField::none()
        };
        let dkg_outcome_hash = read_optional_hash(buf)?;
        let dealer_log_hash = read_optional_hash(buf)?;
        let transactions_root = read_optional_hash(buf)?;
        // #698 step 3: ops_root + op-range.
        let ops_root = read_optional_hash(buf)?;
        let ops_range_start = u64::read(buf)?;
        let ops_range_end = u64::read(buf)?;

        Ok(Self {
            height: ::buffa::MessageField::some(height),
            timestamp,
            parent_hash,
            state_root,
            context,
            dkg_outcome_hash,
            dealer_log_hash,
            transactions_root,
            ops_root,
            ops_range_start,
            ops_range_end,
            __buffa_unknown_fields: UnknownFields::default(),
        })
    }
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;
    use commonware_codec::{DecodeExt, Encode};

    fn sample_context() -> ConsensusContext {
        ConsensusContext {
            round_epoch: 7,
            round_view: 42,
            leader_public_key: vec![0xAB; PUBLIC_KEY_LEN],
            parent_view: 41,
            __buffa_unknown_fields: UnknownFields::default(),
        }
    }

    fn sample_header(with_context: bool, with_optional_hashes: bool) -> BlockHeader {
        BlockHeader {
            height: ::buffa::MessageField::some(Height {
                block_number: 1234,
                __buffa_unknown_fields: UnknownFields::default(),
            }),
            timestamp: 0x0102_0304_0506_0708,
            parent_hash: vec![0x11; HASH_LEN],
            state_root: vec![0x22; HASH_LEN],
            context: if with_context {
                ::buffa::MessageField::some(sample_context())
            } else {
                ::buffa::MessageField::none()
            },
            dkg_outcome_hash: if with_optional_hashes {
                vec![0x33; HASH_LEN]
            } else {
                Vec::new()
            },
            dealer_log_hash: if with_optional_hashes {
                vec![0x44; HASH_LEN]
            } else {
                Vec::new()
            },
            transactions_root: if with_optional_hashes {
                vec![0x55; HASH_LEN]
            } else {
                Vec::new()
            },
            // #698 step 3: ops_root present only when the block carried a
            // merkleized batch (mirrors the optional-hash convention); the
            // op-range is plain u64s (zero on no-batch blocks).
            ops_root: if with_optional_hashes {
                vec![0x66; HASH_LEN]
            } else {
                Vec::new()
            },
            ops_range_start: if with_optional_hashes { 5 } else { 0 },
            ops_range_end: if with_optional_hashes { 17 } else { 0 },
            __buffa_unknown_fields: UnknownFields::default(),
        }
    }

    #[test]
    fn height_round_trip() {
        let h = Height {
            block_number: 0xDEAD_BEEF_CAFE_BABE,
            __buffa_unknown_fields: UnknownFields::default(),
        };
        let bytes = h.encode().to_vec();
        assert_eq!(bytes.len(), 8);
        assert_eq!(bytes.len(), h.encode_size());
        let decoded = Height::decode(&bytes[..]).expect("decode");
        assert_eq!(decoded.block_number, h.block_number);
    }

    #[test]
    fn height_canonical_encoding_pinned() {
        // block_number = 0x0102_0304_0506_0708 -> 8 bytes BE
        let h = Height {
            block_number: 0x0102_0304_0506_0708,
            __buffa_unknown_fields: UnknownFields::default(),
        };
        assert_eq!(
            hex_encode(&h.encode()),
            "0102030405060708",
            "Height canonical encoding must be stable across releases"
        );
    }

    #[test]
    fn consensus_context_round_trip() {
        let ctx = sample_context();
        let bytes = ctx.encode().to_vec();
        assert_eq!(bytes.len(), 8 + 8 + 32 + 8);
        assert_eq!(bytes.len(), ctx.encode_size());
        let decoded = ConsensusContext::decode(&bytes[..]).expect("decode");
        assert_eq!(decoded.round_epoch, ctx.round_epoch);
        assert_eq!(decoded.round_view, ctx.round_view);
        assert_eq!(decoded.leader_public_key, ctx.leader_public_key);
        assert_eq!(decoded.parent_view, ctx.parent_view);
    }

    #[test]
    fn block_header_round_trip_full() {
        let header = sample_header(true, true);
        let bytes = header.encode().to_vec();
        assert_eq!(bytes.len(), header.encode_size());
        let decoded = BlockHeader::decode(&bytes[..]).expect("decode");
        assert_eq!(
            decoded.height.as_option().map(|h| h.block_number),
            Some(1234)
        );
        assert_eq!(decoded.timestamp, header.timestamp);
        assert_eq!(decoded.parent_hash, header.parent_hash);
        assert_eq!(decoded.state_root, header.state_root);
        assert!(decoded.context.is_set());
        assert_eq!(decoded.dkg_outcome_hash, header.dkg_outcome_hash);
        assert_eq!(decoded.dealer_log_hash, header.dealer_log_hash);
        assert_eq!(decoded.transactions_root, header.transactions_root);
        // #698 step 3: ops_root + op-range round-trip.
        assert_eq!(decoded.ops_root, header.ops_root);
        assert_eq!(decoded.ops_range_start, header.ops_range_start);
        assert_eq!(decoded.ops_range_end, header.ops_range_end);
    }

    #[test]
    fn block_header_round_trip_minimal() {
        let header = sample_header(false, false);
        let bytes = header.encode().to_vec();
        assert_eq!(bytes.len(), header.encode_size());
        let decoded = BlockHeader::decode(&bytes[..]).expect("decode");
        assert!(decoded.context.as_option().is_none());
        assert!(decoded.dkg_outcome_hash.is_empty());
        assert!(decoded.dealer_log_hash.is_empty());
        assert!(decoded.transactions_root.is_empty());
        // #698 step 3: no-batch block carries an empty ops_root + zero range.
        assert!(decoded.ops_root.is_empty());
        assert_eq!(decoded.ops_range_start, 0);
        assert_eq!(decoded.ops_range_end, 0);
    }

    #[test]
    fn block_header_encoding_changes_with_field_mutation() {
        let h1 = sample_header(true, true);
        let mut h2 = h1.clone();
        // Mutating timestamp must produce different bytes — that's
        // sufficient to prove the canonical block hash
        // (keccak256 over these bytes) is sensitive to every header field.
        h2.timestamp = h2.timestamp.wrapping_add(1);
        assert_ne!(h1.encode().to_vec(), h2.encode().to_vec());
    }

    #[test]
    fn block_header_default_round_trips() {
        // Default BlockHeader (MessageField::none() for height + context,
        // empty Vec for all bytes fields) must encode and decode cleanly.
        // Important because `compute_block_hash` can hit this path on a
        // bare `proto::Block::default()`.
        let header = BlockHeader {
            height: ::buffa::MessageField::none(),
            timestamp: 0,
            parent_hash: vec![0u8; HASH_LEN],
            state_root: vec![0u8; HASH_LEN],
            context: ::buffa::MessageField::none(),
            dkg_outcome_hash: Vec::new(),
            dealer_log_hash: Vec::new(),
            transactions_root: Vec::new(),
            ops_root: Vec::new(),
            ops_range_start: 0,
            ops_range_end: 0,
            __buffa_unknown_fields: UnknownFields::default(),
        };
        let bytes = header.encode().to_vec();
        let _decoded = BlockHeader::decode(&bytes[..]).expect("default header decodes");
    }

    fn hex_encode(bytes: &[u8]) -> String {
        let mut s = String::with_capacity(bytes.len() * 2);
        for b in bytes {
            s.push_str(&format!("{b:02x}"));
        }
        s
    }
}
