// ALEC - Adaptive Lazy Evolving Compression
// Copyright (c) 2025 David Martin Venti
//
// Dual-licensed under AGPL-3.0 and Commercial License.
// See LICENSE file for details.

//! Encoder module for ALEC compression
//!
//! This module handles encoding data into compact binary messages
//! using various encoding strategies based on context and classification.
//!
//! # Overview
//!
//! The encoder selects the optimal encoding strategy for each message:
//! - **Delta encoding**: For values close to predictions
//! - **Repeated encoding**: For unchanged values
//! - **Pattern encoding**: For known dictionary patterns
//! - **Raw encoding**: Fallback for unpredictable data
//!
//! # Example
//!
//! ```
//! use alec::{Encoder, Classifier, Context, RawData};
//!
//! let mut encoder = Encoder::new();
//! let classifier = Classifier::default();
//! let context = Context::new();
//!
//! let data = RawData::new(22.5, 0);
//! let classification = classifier.classify(&data, &context);
//! let message = encoder.encode(&data, &classification, &context);
//!
//! assert!(message.len() <= 24); // Compressed or equal to raw
//! ```

#[cfg(not(feature = "std"))]
use alloc::{vec, vec::Vec};

use crate::classifier::{Classification, Classifier};
use crate::context::Context;
use crate::error::{EncodeError, Result};
use crate::metrics::CompressionMetrics;
use crate::protocol::{
    ChannelInput, CompactHeader, EncodedMessage, EncodingType, MessageHeader, MessageType,
    Priority, RawData, COMPACT_MARKER_DATA, COMPACT_MARKER_KEYFRAME,
};

/// Encoder for ALEC messages.
///
/// The encoder maintains internal state (sequence numbers) and provides
/// methods to encode sensor data into compact binary messages.
///
/// # Thread Safety
///
/// `Encoder` is not thread-safe. Each thread should have its own instance.
/// For multi-threaded scenarios, consider using separate encoders per thread
/// or wrapping in a `Mutex`.
#[derive(Debug, Clone)]
pub struct Encoder {
    /// Next sequence number
    sequence: u16,
    /// Whether to include checksum in encoded bytes
    include_checksum: bool,
}

impl Encoder {
    /// Create a new encoder with default settings.
    ///
    /// # Example
    ///
    /// ```
    /// use alec::Encoder;
    ///
    /// let encoder = Encoder::new();
    /// assert_eq!(encoder.sequence(), 0);
    /// assert!(!encoder.checksum_enabled());
    /// ```
    pub fn new() -> Self {
        Self {
            sequence: 0,
            include_checksum: false,
        }
    }

    /// Create an encoder with checksum verification enabled.
    ///
    /// When checksum is enabled, encoded messages include a CRC32
    /// checksum for integrity verification during decoding.
    ///
    /// # Example
    ///
    /// ```
    /// use alec::Encoder;
    ///
    /// let encoder = Encoder::with_checksum();
    /// assert!(encoder.checksum_enabled());
    /// ```
    pub fn with_checksum() -> Self {
        Self {
            sequence: 0,
            include_checksum: true,
        }
    }

    /// Check if checksum is enabled for this encoder.
    pub fn checksum_enabled(&self) -> bool {
        self.include_checksum
    }

    /// Get the current sequence number.
    ///
    /// Sequence numbers are used to detect message loss and ordering issues.
    pub fn sequence(&self) -> u16 {
        self.sequence
    }

    /// Reset the sequence number to zero.
    ///
    /// Call this when establishing a new connection or after a sync reset.
    pub fn reset_sequence(&mut self) {
        self.sequence = 0;
    }

    /// Restore the internal sequence counter to an explicit value.
    ///
    /// Used by the encoder-state save/restore path (v1.3.7 FFI) so
    /// that after rolling back a discarded frame the encoder continues
    /// numbering uplinks from where the last *transmitted* frame left
    /// off. Do NOT use this to skip sequence numbers — the decoder's
    /// gap-detection logic would report a false positive.
    pub fn restore_sequence(&mut self, seq: u16) {
        self.sequence = seq;
    }

    /// Encode data and return raw bytes.
    ///
    /// This is a convenience method that combines encoding and serialization.
    /// If checksum is enabled, the returned bytes include the checksum.
    ///
    /// # Arguments
    ///
    /// * `data` - The raw sensor data to encode
    /// * `classification` - Priority classification from the classifier
    /// * `context` - Shared context for predictions and patterns
    ///
    /// # Returns
    ///
    /// A `Vec<u8>` containing the encoded message bytes.
    pub fn encode_to_bytes(
        &mut self,
        data: &RawData,
        classification: &Classification,
        context: &Context,
    ) -> Vec<u8> {
        let message = self.encode(data, classification, context);

        if self.include_checksum {
            message.to_bytes_with_checksum()
        } else {
            message.to_bytes()
        }
    }

    /// Encode data while collecting compression metrics.
    ///
    /// Use this method to track compression performance over time.
    ///
    /// # Arguments
    ///
    /// * `data` - The raw sensor data to encode
    /// * `classification` - Priority classification
    /// * `context` - Shared context
    /// * `metrics` - Metrics collector to update
    ///
    /// # Returns
    ///
    /// The encoded message.
    pub fn encode_with_metrics(
        &mut self,
        data: &RawData,
        classification: &Classification,
        context: &Context,
        metrics: &mut CompressionMetrics,
    ) -> EncodedMessage {
        let message = self.encode(data, classification, context);

        if let Some(encoding) = message.encoding_type() {
            metrics.record_encode(data.raw_size(), message.len(), encoding);
        }

        message
    }

    /// Encode data with classification into a compact message.
    ///
    /// This method selects the optimal encoding strategy based on the
    /// context's predictions and the data's characteristics:
    ///
    /// - If value equals the last value, uses **repeated** encoding (1 byte)
    /// - If value is close to prediction, uses **delta** encoding (1-4 bytes)
    /// - Otherwise, uses **raw** encoding (8 bytes for value)
    ///
    /// # Arguments
    ///
    /// * `data` - The raw sensor data to encode
    /// * `classification` - Priority classification from the classifier
    /// * `context` - Shared context for predictions and patterns
    ///
    /// # Returns
    ///
    /// An `EncodedMessage` containing the compressed data.
    ///
    /// # Examples
    ///
    /// ```
    /// use alec::{Encoder, Classifier, Context, RawData};
    ///
    /// let mut encoder = Encoder::new();
    /// let classifier = Classifier::default();
    /// let context = Context::new();
    ///
    /// let data = RawData::new(22.5, 0);
    /// let classification = classifier.classify(&data, &context);
    /// let message = encoder.encode(&data, &classification, &context);
    ///
    /// // Message is ready to transmit
    /// println!("Encoded {} bytes", message.len());
    /// ```
    pub fn encode(
        &mut self,
        data: &RawData,
        classification: &Classification,
        context: &Context,
    ) -> EncodedMessage {
        // Check for invalid values
        if data.value.is_nan() || data.value.is_infinite() {
            // Fall back to raw encoding for invalid values
            return self.encode_raw(data, classification.priority, context);
        }

        // Choose encoding based on context
        let (encoding_type, encoded_value) = self.choose_encoding(data, context);

        // Build payload
        let mut payload = Vec::new();

        // Source ID (varint encoding)
        self.encode_varint(data.source_id, &mut payload);

        // Encoding type
        payload.push(encoding_type as u8);

        // Encoded value
        payload.extend(encoded_value);

        // Build header
        let header = MessageHeader {
            version: crate::PROTOCOL_VERSION,
            message_type: MessageType::Data,
            priority: classification.priority,
            sequence: self.next_sequence(),
            timestamp: (data.timestamp / 1000) as u32,
            context_version: context.version(),
        };

        EncodedMessage::new(header, payload)
    }

    /// Encode as raw (fallback)
    fn encode_raw(
        &mut self,
        data: &RawData,
        priority: Priority,
        context: &Context,
    ) -> EncodedMessage {
        let mut payload = Vec::new();

        // Source ID
        self.encode_varint(data.source_id, &mut payload);

        // Encoding type
        payload.push(EncodingType::Raw64 as u8);

        // Raw value
        payload.extend_from_slice(&data.value.to_be_bytes());

        let header = MessageHeader {
            version: crate::PROTOCOL_VERSION,
            message_type: MessageType::Data,
            priority,
            sequence: self.next_sequence(),
            timestamp: (data.timestamp / 1000) as u32,
            context_version: context.version(),
        };

        EncodedMessage::new(header, payload)
    }

    /// Choose the best encoding for this value
    fn choose_encoding(&self, data: &RawData, context: &Context) -> (EncodingType, Vec<u8>) {
        // Check if value matches last value exactly (repeated) — MOST COMPACT
        if let Some(last) = context.last_value(data.source_id) {
            if (data.value - last).abs() < f64::EPSILON {
                return (EncodingType::Repeated, vec![]);
            }
        }

        // Try to get prediction for delta encoding
        if let Some(prediction) = context.predict(data.source_id) {
            let delta = data.value - prediction.value;
            let scale = context.scale_factor() as f64;
            let raw = delta * scale;
            let scaled_delta = if raw >= 0.0 { raw + 0.5 } else { raw - 0.5 };
            let scaled_delta = scaled_delta as i64 as f64;

            // Check if delta fits in i8
            if scaled_delta >= i8::MIN as f64 && scaled_delta <= i8::MAX as f64 {
                let delta_i8 = scaled_delta as i8;
                return (EncodingType::Delta8, vec![delta_i8 as u8]);
            }

            // Check if delta fits in i16
            if scaled_delta >= i16::MIN as f64 && scaled_delta <= i16::MAX as f64 {
                let delta_i16 = scaled_delta as i16;
                return (EncodingType::Delta16, delta_i16.to_be_bytes().to_vec());
            }

            // Check if delta fits in i32
            if scaled_delta >= i32::MIN as f64 && scaled_delta <= i32::MAX as f64 {
                let delta_i32 = scaled_delta as i32;
                return (EncodingType::Delta32, delta_i32.to_be_bytes().to_vec());
            }
        }

        // Check if value can fit in f32 without significant loss
        let as_f32 = data.value as f32;
        if (as_f32 as f64 - data.value).abs() < 0.0001 {
            return (EncodingType::Raw32, as_f32.to_be_bytes().to_vec());
        }

        // Fall back to raw f64
        (EncodingType::Raw64, data.value.to_be_bytes().to_vec())
    }

    /// Encode a varint (variable-length integer)
    fn encode_varint(&self, value: u32, output: &mut Vec<u8>) {
        let mut v = value;
        while v >= 0x80 {
            output.push((v as u8 & 0x7F) | 0x80);
            v >>= 7;
        }
        output.push(v as u8);
    }

    /// Get next sequence number
    fn next_sequence(&mut self) -> u16 {
        let seq = self.sequence;
        self.sequence = self.sequence.wrapping_add(1);
        seq
    }

    /// Encode multiple values in one message
    pub fn encode_multi(
        &mut self,
        values: &[(u8, f64)], // (name_id, value) pairs
        source_id: u32,
        timestamp: u64,
        priority: Priority,
        context: &Context,
    ) -> EncodedMessage {
        let mut payload = Vec::new();

        // Source ID
        self.encode_varint(source_id, &mut payload);

        // Multi encoding type
        payload.push(EncodingType::Multi as u8);

        // Count
        payload.push(values.len() as u8);

        // Each value
        for (name_id, value) in values {
            // Name ID (1 byte)
            payload.push(*name_id);

            // Simple encoding for multi (just use Raw32 for simplicity)
            payload.push(EncodingType::Raw32 as u8);
            payload.extend_from_slice(&(*value as f32).to_be_bytes());
        }

        let header = MessageHeader {
            version: crate::PROTOCOL_VERSION,
            message_type: MessageType::Data,
            priority,
            sequence: self.next_sequence(),
            timestamp: (timestamp / 1000) as u32,
            context_version: context.version(),
        };

        EncodedMessage::new(header, payload)
    }

    /// Maximum frame size for P4 inclusion (BLE ATT_MTU)
    const MULTI_FRAME_CAP: usize = 127;

    /// Encode multiple channels into a single frame with adaptive per-channel
    /// compression and priority-based inclusion.
    ///
    /// - P1–P3 channels: always included, adaptively encoded
    /// - P4 channels: included only if frame stays under `MULTI_FRAME_CAP`
    /// - P5 channels: excluded from the frame (context still updated by caller)
    ///
    /// Returns the encoded message and a list of classifications (one per input
    /// channel, in the same order) so the caller can observe P5 channels.
    pub fn encode_multi_adaptive(
        &mut self,
        channels: &[ChannelInput],
        timestamp: u64,
        context: &Context,
        classifier: &Classifier,
    ) -> (EncodedMessage, Vec<Classification>) {
        // Classify every channel using name_id as context key
        // (matches decoder convention: name_id → source_id for multi frames)
        let classified: Vec<(&ChannelInput, Classification)> = channels
            .iter()
            .map(|ch| {
                let raw = RawData::with_source(ch.name_id as u32, ch.value, timestamp);
                let cls = classifier.classify(&raw, context);
                (ch, cls)
            })
            .collect();

        // Separate into buckets by priority tier
        let mut must_include: Vec<(&ChannelInput, &Classification)> = Vec::new(); // P1-P3
        let mut deferred: Vec<(&ChannelInput, &Classification)> = Vec::new(); // P4
                                                                              // P5: excluded from frame

        for (ch, cls) in &classified {
            match cls.priority {
                Priority::P1Critical | Priority::P2Important | Priority::P3Normal => {
                    must_include.push((ch, cls));
                }
                Priority::P4Deferred => {
                    deferred.push((ch, cls));
                }
                Priority::P5Disposable => {} // skip
            }
        }

        // Sort must_include by priority (P1 first)
        must_include.sort_by_key(|(_, cls)| cls.priority);

        // Build payload
        let mut payload = Vec::new();

        // Source ID 0 for the frame-level (channels carry their own)
        self.encode_varint(0, &mut payload);

        // Multi encoding tag
        payload.push(EncodingType::Multi as u8);

        // Count placeholder — we'll fill it after building entries
        let count_pos = payload.len();
        payload.push(0u8);

        let mut included_count: u8 = 0;

        // Encode must-include channels (P1–P3)
        for (ch, _cls) in &must_include {
            self.write_channel_entry(ch, context, &mut payload);
            included_count += 1;
        }

        // Try to fit P4 (deferred) channels
        let header_overhead = MessageHeader::SIZE;
        for (ch, _cls) in &deferred {
            // Speculatively encode into a temp buffer to check size
            let mut tmp = Vec::new();
            self.write_channel_entry(ch, context, &mut tmp);
            if header_overhead + payload.len() + tmp.len() <= Self::MULTI_FRAME_CAP {
                payload.extend(tmp);
                included_count += 1;
            }
        }

        // Fill the count byte
        payload[count_pos] = included_count;

        let header = MessageHeader {
            version: crate::PROTOCOL_VERSION,
            message_type: MessageType::Data,
            priority: must_include
                .first()
                .map(|(_, cls)| cls.priority)
                .unwrap_or(Priority::P3Normal),
            sequence: self.next_sequence(),
            timestamp: (timestamp / 1000) as u32,
            context_version: context.version(),
        };

        let classifications = classified.into_iter().map(|(_, cls)| cls).collect();
        (EncodedMessage::new(header, payload), classifications)
    }

    /// Write one channel entry into the multi payload.
    ///
    /// Uses `name_id as u32` as the context key for encoding decisions, since
    /// the decoder only has the name_id from the wire and must use the same key.
    fn write_channel_entry(&self, ch: &ChannelInput, context: &Context, payload: &mut Vec<u8>) {
        // name_id (1B)
        payload.push(ch.name_id);

        // Use name_id as context key (matches decoder's name_id→source_id mapping)
        let data = RawData::with_source(ch.name_id as u32, ch.value, 0);
        let (encoding_type, encoded_value) = self.choose_encoding(&data, context);

        payload.push(encoding_type as u8);
        payload.extend(encoded_value);
    }

    // ========================================================================
    // Bloc C — Keyframe lifecycle (Milesight packet-loss recovery)
    //
    // Keyframes are a packet-loss recovery mechanism, not a
    // compression mechanism. Every keyframe emits Raw32 for every
    // channel (marker 0xA2), which lets the decoder fully re-seed
    // its prediction state regardless of the context it currently
    // holds.
    //
    // The keyframe decision is made by the FFI layer
    // (alec_encode_multi_fixed in alec-ffi/src/lib.rs), NOT by the
    // core Encoder. `Encoder::encode_multi_fixed` takes the
    // `keyframe: bool` as an argument and simply honours it.
    //
    // The FFI owns three fields on AlecEncoder that drive the
    // decision:
    //
    //     force_keyframe_pending : bool   (set by alec_force_keyframe)
    //     messages_since_keyframe: u32    (incremented per encode)
    //     keyframe_interval      : u32    (from AlecEncoderConfig)
    //     smart_resync           : bool   (from AlecEncoderConfig)
    //
    // Full lifecycle (cold start → steady state → resync):
    //
    //  1. alec_encoder_new_with_config() sets keyframe_interval from
    //     AlecEncoderConfig (default 50 frames) and smart_resync
    //     (default true). Counter starts at 0.
    //
    //  2. Each call to alec_encode_multi_fixed() evaluates:
    //
    //         periodic_due     = interval > 0 &&
    //                            messages_since_keyframe >= interval
    //         downlink_forced  = force_keyframe_pending && smart_resync
    //         keyframe         = periodic_due || downlink_forced
    //
    //  3. On a keyframe: encode_multi_fixed writes marker 0xA2 and
    //     Raw32 for every channel. Counter resets to 1 (not 0) so
    //     keyframes land on a fixed modular offset — interval=10
    //     gives keyframes at frames 10, 20, 30 … and downlink-forced
    //     keyframes also "count" as frame 1 of the next cycle.
    //     The force flag is cleared.
    //
    //  4. On a regular data frame: counter += 1, force flag left
    //     untouched, marker 0xA1.
    //
    //  5. alec_force_keyframe() sets force_keyframe_pending. It is
    //     a no-op when smart_resync=false (step 2 ignores the flag
    //     in that case).
    //
    //  6. alec_downlink_handler() parses a raw downlink payload and
    //     calls alec_force_keyframe() if byte 0 == 0xFF. Any other
    //     byte returns ALEC_ERROR_INVALID_INPUT with no state change.
    //
    // Worst-case drift after a packet loss:
    //   - No smart resync          → keyframe_interval × uplink_period
    //                                (e.g. 50 × 10 min = ~8h on EM500-CO2)
    //   - With smart resync active → 1 × uplink_period   (one uplink)
    //
    // The decoder side (alec_decode_multi_fixed in alec-ffi) reacts
    // by calling Context::reset_to_baseline() on a non-keyframe with
    // gap_size > 0 or context_mismatch. See the detailed notes there.
    // ========================================================================

    // ========================================================================
    // Bloc B — Compact fixed-channel encoder (Milesight EM500-CO2)
    //
    // Wire layout:
    //
    //     byte 0        : marker        (0xA1 data / 0xA2 keyframe)
    //     byte 1..=2    : sequence      (u16 BE)
    //     byte 3..=4    : ctx_version   (u16 BE, low 16 bits of u32)
    //     byte 5..=5+B-1: encoding bitmap (2 bits per channel,
    //                     B = ceil(channel_count / 4) bytes)
    //     byte 5+B..    : per-channel encoded data
    //
    // Encoding bitmap codes:
    //
    //     00 Repeated    (0 bytes of data)
    //     01 Delta8      (1 byte)
    //     10 Delta16     (2 bytes)
    //     11 Raw32       (4 bytes)
    //
    // Keyframes (marker 0xA2) emit Raw32 for EVERY channel so the
    // decoder can resync unconditionally, regardless of the context
    // state it currently holds. For 5 channels a keyframe is always
    // 5 + 2 + 20 = 27 bytes (exceeds the 11B LoRaWAN ceiling — the
    // caller must either accept a split or fall back to TLV for that
    // specific frame).
    //
    // Steady-state frames on the EM500-CO2 slow-drift profile
    // (58 % Repeated, 42 % Delta8) fit in ~9 bytes total, well under
    // the 11B ceiling.
    // ========================================================================
    /// Context key used by the fixed-channel encoder/decoder for a
    /// channel at the given (fixed) index. Keeping the mapping in one
    /// place guarantees the encoder and decoder agree on how to look
    /// up per-channel predictions in the context.
    ///
    /// The `+1` is important: the context uses `source_id == 0` as
    /// "unspecified"; giving channel 0 the id 1 keeps every channel's
    /// prediction cache distinct.
    #[inline]
    pub(crate) fn fixed_channel_source_id(channel_index: usize) -> u32 {
        (channel_index as u32) + 1
    }
}

/// Encoding tag for a single channel in a fixed-channel frame.
/// Packed 2 bits per channel into the encoding bitmap.
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum FixedEncoding {
    Repeated = 0b00,
    Delta8 = 0b01,
    Delta16 = 0b10,
    Raw32 = 0b11,
}

impl FixedEncoding {
    /// Number of bytes this encoding contributes to the payload body.
    #[inline]
    pub(crate) const fn byte_size(self) -> usize {
        match self {
            FixedEncoding::Repeated => 0,
            FixedEncoding::Delta8 => 1,
            FixedEncoding::Delta16 => 2,
            FixedEncoding::Raw32 => 4,
        }
    }

    #[inline]
    pub(crate) fn from_bits(bits: u8) -> Self {
        match bits & 0b11 {
            0b00 => FixedEncoding::Repeated,
            0b01 => FixedEncoding::Delta8,
            0b10 => FixedEncoding::Delta16,
            _ => FixedEncoding::Raw32,
        }
    }
}

/// Number of bitmap bytes required for `channel_count` channels
/// (2 bits per channel, rounded up).
#[inline]
pub(crate) const fn fixed_bitmap_bytes(channel_count: usize) -> usize {
    // `usize::div_ceil` is stable for const fns from Rust 1.73 —
    // the crate's MSRV is 1.70, so compute `(a + 7) / 8` by hand.
    (channel_count * 2 + 7) / 8
}

impl Encoder {
    /// Encode a fixed-channel frame (compact wire format).
    ///
    /// # Arguments
    ///
    /// * `values` - Channel values, positional. `values[i]` is stored at
    ///   channel index `i` and decoded with the same positional key
    ///   (`i + 1` as a context source_id).
    /// * `context` - Shared context used for prediction-based encoding.
    ///   Not mutated — the caller is responsible for `observe()`-ing
    ///   every channel AFTER the call succeeds (so predictions reflect
    ///   the pre-encode state).
    /// * `keyframe` - If true, every channel is forced to Raw32 and the
    ///   frame gets marker byte 0xA2. Otherwise the encoder picks the
    ///   smallest encoding per channel (Repeated / Delta8 / Delta16 /
    ///   Raw32) and the frame gets marker byte 0xA1.
    /// * `output` - Destination buffer for the wire bytes.
    ///
    /// # Returns
    ///
    /// The number of bytes written to `output`.
    ///
    /// # Errors
    ///
    /// * `EncodeError::PayloadTooLarge` if `values` is empty or larger
    ///   than 64 channels (the 2-bits-per-channel bitmap is bounded).
    /// * `EncodeError::BufferTooSmall` if `output` is too small to
    ///   hold the encoded frame.
    ///
    /// # Side effects
    ///
    /// The encoder's sequence counter is incremented by 1 on success.
    /// The context is not modified (the caller observes values after).
    pub fn encode_multi_fixed(
        &mut self,
        values: &[f64],
        context: &Context,
        keyframe: bool,
        output: &mut [u8],
    ) -> Result<usize> {
        if values.is_empty() {
            return Err(EncodeError::PayloadTooLarge { size: 0, max: 64 }.into());
        }
        // 2 bits per channel packs 4 channels per byte; we cap the
        // frame at 64 channels so the bitmap stays <= 16 bytes and
        // the entire frame comfortably fits a LoRaWAN 255B payload.
        if values.len() > 64 {
            return Err(EncodeError::PayloadTooLarge {
                size: values.len(),
                max: 64,
            }
            .into());
        }

        // 1) Decide, per channel, the encoding we'll emit. For
        //    keyframes we short-circuit to Raw32 for every channel.
        let count = values.len();
        let bitmap_bytes = fixed_bitmap_bytes(count);
        let mut encodings: [FixedEncoding; 64] = [FixedEncoding::Raw32; 64];
        let mut data_bytes: usize = 0;

        // Small scratch buffer used when picking the best encoding.
        // Up to 4 bytes per channel (Raw32).
        let mut per_channel_bytes: [[u8; 4]; 64] = [[0u8; 4]; 64];
        let mut per_channel_len: [u8; 64] = [0u8; 64];

        for (i, &value) in values.iter().enumerate() {
            if value.is_nan() || value.is_infinite() {
                // Cannot compress invalid values — emit Raw32.
                let f = value as f32;
                let bytes = f.to_be_bytes();
                encodings[i] = FixedEncoding::Raw32;
                per_channel_bytes[i][..4].copy_from_slice(&bytes);
                per_channel_len[i] = 4;
                data_bytes += 4;
                continue;
            }

            if keyframe {
                let bytes = (value as f32).to_be_bytes();
                encodings[i] = FixedEncoding::Raw32;
                per_channel_bytes[i][..4].copy_from_slice(&bytes);
                per_channel_len[i] = 4;
                data_bytes += 4;
                continue;
            }

            let source_id = Self::fixed_channel_source_id(i);

            // Exact repeat: Repeated (0 bytes).
            if let Some(last) = context.last_value(source_id) {
                if (value - last).abs() < f64::EPSILON {
                    encodings[i] = FixedEncoding::Repeated;
                    per_channel_len[i] = 0;
                    continue;
                }
            }

            // Prediction-based delta encoding.
            if let Some(prediction) = context.predict(source_id) {
                let scale = context.scale_factor() as f64;
                let raw = (value - prediction.value) * scale;
                let rounded = if raw >= 0.0 { raw + 0.5 } else { raw - 0.5 };
                let scaled = rounded as i64 as f64;

                if scaled >= i8::MIN as f64 && scaled <= i8::MAX as f64 {
                    let d = scaled as i8;
                    encodings[i] = FixedEncoding::Delta8;
                    per_channel_bytes[i][0] = d as u8;
                    per_channel_len[i] = 1;
                    data_bytes += 1;
                    continue;
                }

                if scaled >= i16::MIN as f64 && scaled <= i16::MAX as f64 {
                    let d = scaled as i16;
                    let b = d.to_be_bytes();
                    encodings[i] = FixedEncoding::Delta16;
                    per_channel_bytes[i][..2].copy_from_slice(&b);
                    per_channel_len[i] = 2;
                    data_bytes += 2;
                    continue;
                }
                // Delta32 is deliberately NOT supported in the fixed
                // wire format — the 2-bit tag only encodes four
                // options. Fall through to Raw32.
            }

            // No usable prediction → Raw32.
            let b = (value as f32).to_be_bytes();
            encodings[i] = FixedEncoding::Raw32;
            per_channel_bytes[i][..4].copy_from_slice(&b);
            per_channel_len[i] = 4;
            data_bytes += 4;
        }

        let total = 1 /* marker */ + CompactHeader::SIZE + bitmap_bytes + data_bytes;
        if output.len() < total {
            return Err(EncodeError::BufferTooSmall {
                needed: total,
                available: output.len(),
            }
            .into());
        }

        // 2) Write marker byte.
        output[0] = if keyframe {
            COMPACT_MARKER_KEYFRAME
        } else {
            COMPACT_MARKER_DATA
        };

        // 3) Write the 4-byte compact header. `context_version` is
        //    truncated from u32 to u16 — the decoder reconstructs
        //    the high bits from its own tracking + wraparound logic.
        let header = CompactHeader::new(self.next_sequence(), (context.version() & 0xFFFF) as u16);
        header.write(&mut output[1..1 + CompactHeader::SIZE])?;

        // 4) Write the encoding bitmap, 2 bits per channel LSB-first
        //    within each byte (channel 0 → bits 0..=1 of byte 0).
        let bitmap_start = 1 + CompactHeader::SIZE;
        for b in &mut output[bitmap_start..bitmap_start + bitmap_bytes] {
            *b = 0;
        }
        for (i, enc) in encodings.iter().take(count).enumerate() {
            let byte_idx = bitmap_start + i / 4;
            let bit_shift = (i % 4) * 2;
            output[byte_idx] |= (*enc as u8) << bit_shift;
        }

        // 5) Write per-channel encoded data, in channel order.
        let mut cursor = bitmap_start + bitmap_bytes;
        for i in 0..count {
            let len = per_channel_len[i] as usize;
            if len > 0 {
                output[cursor..cursor + len].copy_from_slice(&per_channel_bytes[i][..len]);
                cursor += len;
            }
        }
        debug_assert_eq!(cursor, total);
        Ok(total)
    }
}

impl Default for Encoder {
    fn default() -> Self {
        Self::new()
    }
}

/// Builder for creating encoded messages manually
pub struct MessageBuilder {
    header: MessageHeader,
    payload: Vec<u8>,
}

impl MessageBuilder {
    /// Create a new message builder
    pub fn new() -> Self {
        Self {
            header: MessageHeader::default(),
            payload: Vec::new(),
        }
    }

    /// Set message type
    pub fn message_type(mut self, msg_type: MessageType) -> Self {
        self.header.message_type = msg_type;
        self
    }

    /// Set priority
    pub fn priority(mut self, priority: Priority) -> Self {
        self.header.priority = priority;
        self
    }

    /// Set sequence number
    pub fn sequence(mut self, seq: u16) -> Self {
        self.header.sequence = seq;
        self
    }

    /// Set timestamp
    pub fn timestamp(mut self, ts: u32) -> Self {
        self.header.timestamp = ts;
        self
    }

    /// Set context version
    pub fn context_version(mut self, version: u32) -> Self {
        self.header.context_version = version;
        self
    }

    /// Set payload
    pub fn payload(mut self, payload: Vec<u8>) -> Self {
        self.payload = payload;
        self
    }

    /// Build the message
    pub fn build(self) -> EncodedMessage {
        EncodedMessage::new(self.header, self.payload)
    }
}

impl Default for MessageBuilder {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::classifier::Classifier;

    #[test]
    fn test_encode_basic() {
        let mut encoder = Encoder::new();
        let classifier = Classifier::default();
        let context = Context::new();

        let data = RawData::new(42.0, 0);
        let classification = classifier.classify(&data, &context);
        let message = encoder.encode(&data, &classification, &context);

        assert!(!message.is_empty());
        assert!(message.len() < data.raw_size() + MessageHeader::SIZE);
    }

    #[test]
    fn test_encode_with_context() {
        let mut encoder = Encoder::new();
        let classifier = Classifier::default();
        let mut context = Context::new();

        // Build context
        for i in 0..10 {
            context.observe(&RawData::new(20.0 + i as f64 * 0.1, i as u64));
        }

        // Encode value close to prediction
        let data = RawData::new(21.0, 100);
        let classification = classifier.classify(&data, &context);
        let message = encoder.encode(&data, &classification, &context);

        // Should use delta encoding (smaller)
        let encoding = message.encoding_type();
        assert!(matches!(
            encoding,
            Some(EncodingType::Delta8) | Some(EncodingType::Delta16)
        ));
    }

    #[test]
    fn test_encode_repeated() {
        let mut encoder = Encoder::new();
        let classifier = Classifier::default();
        let mut context = Context::new();

        // Observe a value
        context.observe(&RawData::new(42.0, 0));

        // Encode same value
        let data = RawData::new(42.0, 1);
        let classification = classifier.classify(&data, &context);
        let message = encoder.encode(&data, &classification, &context);

        // Should use repeated encoding (very small)
        assert_eq!(message.encoding_type(), Some(EncodingType::Repeated));
    }

    #[test]
    fn test_sequence_increment() {
        let mut encoder = Encoder::new();
        let classifier = Classifier::default();
        let context = Context::new();

        let data = RawData::new(42.0, 0);
        let classification = classifier.classify(&data, &context);

        let msg1 = encoder.encode(&data, &classification, &context);
        let msg2 = encoder.encode(&data, &classification, &context);
        let msg3 = encoder.encode(&data, &classification, &context);

        assert_eq!(msg1.header.sequence, 0);
        assert_eq!(msg2.header.sequence, 1);
        assert_eq!(msg3.header.sequence, 2);
    }

    #[test]
    fn test_encode_nan() {
        let mut encoder = Encoder::new();
        let classifier = Classifier::default();
        let context = Context::new();

        let data = RawData::new(f64::NAN, 0);
        let classification = classifier.classify(&data, &context);
        let message = encoder.encode(&data, &classification, &context);

        // Should fall back to raw encoding
        assert_eq!(message.encoding_type(), Some(EncodingType::Raw64));
    }

    #[test]
    fn test_encode_multi() {
        let mut encoder = Encoder::new();
        let context = Context::new();

        let values: Vec<(u8, f64)> = vec![
            (1, 22.5),    // temperature
            (2, 65.0),    // humidity
            (3, 1013.25), // pressure
        ];

        let message = encoder.encode_multi(&values, 42, 12345, Priority::P3Normal, &context);

        assert_eq!(message.encoding_type(), Some(EncodingType::Multi));
    }

    #[test]
    fn test_varint_encoding() {
        let encoder = Encoder::new();

        // Small value (1 byte)
        let mut out1 = Vec::new();
        encoder.encode_varint(42, &mut out1);
        assert_eq!(out1.len(), 1);

        // Medium value (2 bytes)
        let mut out2 = Vec::new();
        encoder.encode_varint(200, &mut out2);
        assert_eq!(out2.len(), 2);

        // Large value
        let mut out3 = Vec::new();
        encoder.encode_varint(100000, &mut out3);
        assert!(out3.len() >= 3);
    }

    #[test]
    fn test_message_builder() {
        let message = MessageBuilder::new()
            .message_type(MessageType::Sync)
            .priority(Priority::P1Critical)
            .sequence(42)
            .timestamp(12345)
            .payload(vec![1, 2, 3])
            .build();

        assert_eq!(message.header.message_type, MessageType::Sync);
        assert_eq!(message.header.priority, Priority::P1Critical);
        assert_eq!(message.header.sequence, 42);
        assert_eq!(message.payload, vec![1, 2, 3]);
    }
}
