//! Binary encoding and decoding for session records.
//!
//! Three record types are persisted:
//! - **Snapshot** — full session state at close time (or on checkpoint).
//! - **Start record** — compact record written on `start_session`; replaced by
//!   a snapshot when the session closes.
//! - **Audit log** — standalone serialisation used for the separate audit keyspace.

mod start_record;

pub use start_record::{deserialize_start_record, serialize_start_record};

use super::audit::AuditEntry;
use super::signal_state::SignalSnapEntry;
use super::snapshot::SessionSnapshot;
use super::types::SessionId;

/// Format version byte for snapshot serialization.
///
/// v0x01: original format (annotations as plain Vec<String>)
/// v0x02: annotations as Vec<(u64, String)>, `audit_truncated` flag, per-signal stats
/// v0x03: added `started_at_ns`, `closed_at_ns` to snapshot; count to `SignalSnapEntry`
const SNAPSHOT_VERSION: u8 = 0x03;

// ── Snapshot ──────────────────────────────────────────────────────────────────

/// Serialize a `SessionSnapshot` to bytes for storage archival.
///
/// Format v0x03:
/// `[version: u8][session_id: u64][user_id: u64][signals_written: u64]`
/// `[signals_rejected: u64][duration_ms: u64][reward_velocity: f64]`
/// `[metadata_count: u32][...kv pairs...][annotations_count: u32][...(ts_u64, len, bytes)...]`
/// `[entities_count: u32][...u64s...][audit_truncated: u8]`
/// `[audit_count: u32][...audit entries...]`
/// `[signal_count: u32][...(name_len, name, decay_f64, window_1h_u64, count_u64)...]`
/// `[started_at_ns: u64][closed_at_ns: u64]`
#[must_use]
#[allow(clippy::cast_possible_truncation)]
pub fn serialize_snapshot(snap: &SessionSnapshot) -> Vec<u8> {
    let mut buf = Vec::new();
    buf.push(SNAPSHOT_VERSION);
    buf.extend_from_slice(&snap.id.as_u64().to_le_bytes());
    buf.extend_from_slice(&snap.user_id.to_le_bytes());
    buf.extend_from_slice(&snap.signals_written.to_le_bytes());
    buf.extend_from_slice(&snap.signals_rejected.to_le_bytes());
    buf.extend_from_slice(&snap.duration_ms.to_le_bytes());
    buf.extend_from_slice(&snap.reward_velocity.to_bits().to_le_bytes());

    // Metadata: count then (key_len, key, val_len, val) pairs.
    buf.extend_from_slice(&(snap.metadata.len() as u32).to_le_bytes());
    for (k, v) in &snap.metadata {
        buf.extend_from_slice(&(k.len() as u32).to_le_bytes());
        buf.extend_from_slice(k.as_bytes());
        buf.extend_from_slice(&(v.len() as u32).to_le_bytes());
        buf.extend_from_slice(v.as_bytes());
    }

    // Annotations v0x02: count then (timestamp_ns: u64, len: u32, bytes).
    buf.extend_from_slice(&(snap.annotations.len() as u32).to_le_bytes());
    for (ts, ann) in &snap.annotations {
        buf.extend_from_slice(&ts.to_le_bytes());
        buf.extend_from_slice(&(ann.len() as u32).to_le_bytes());
        buf.extend_from_slice(ann.as_bytes());
    }

    // Signaled entities: count then u64 values.
    buf.extend_from_slice(&(snap.signaled_entities.len() as u32).to_le_bytes());
    for &eid in &snap.signaled_entities {
        buf.extend_from_slice(&eid.to_le_bytes());
    }

    // Audit truncated flag (v0x02).
    buf.push(u8::from(snap.audit_truncated));

    // Audit log: count then serialized entries.
    buf.extend_from_slice(&(snap.audit_log.len() as u32).to_le_bytes());
    for e in &snap.audit_log {
        buf.extend_from_slice(&e.timestamp_ns.to_le_bytes());
        buf.extend_from_slice(&(e.signal_type.len() as u32).to_le_bytes());
        buf.extend_from_slice(e.signal_type.as_bytes());
        buf.push(u8::from(e.accepted));
        match &e.reason {
            None => buf.extend_from_slice(&0u32.to_le_bytes()),
            Some(r) => {
                buf.extend_from_slice(&(r.len() as u32).to_le_bytes());
                buf.extend_from_slice(r.as_bytes());
            }
        }
    }

    // Per-signal stats (v0x03): count then (name_len, name, decay_score_f64, window_1h_u64, count_u64).
    buf.extend_from_slice(&(snap.signals.len() as u32).to_le_bytes());
    for (name, entry) in &snap.signals {
        buf.extend_from_slice(&(name.len() as u32).to_le_bytes());
        buf.extend_from_slice(name.as_bytes());
        buf.extend_from_slice(&entry.decay_score.to_bits().to_le_bytes());
        buf.extend_from_slice(&entry.window_1h.to_le_bytes());
        buf.extend_from_slice(&entry.count.to_le_bytes());
    }

    // v0x03: session timing fields.
    buf.extend_from_slice(&snap.started_at_ns.to_le_bytes());
    buf.extend_from_slice(&snap.closed_at_ns.to_le_bytes());

    buf
}

/// Deserialize a `SessionSnapshot` from bytes (version 0x02).
#[must_use]
#[allow(clippy::too_many_lines)]
pub fn deserialize_snapshot(bytes: &[u8]) -> Option<SessionSnapshot> {
    let mut pos = 0;

    let read_u32 = |pos: &mut usize| -> Option<u32> {
        if *pos + 4 > bytes.len() {
            return None;
        }
        let v = u32::from_le_bytes([
            bytes[*pos],
            bytes[*pos + 1],
            bytes[*pos + 2],
            bytes[*pos + 3],
        ]);
        *pos += 4;
        Some(v)
    };

    let read_u64 = |pos: &mut usize| -> Option<u64> {
        if *pos + 8 > bytes.len() {
            return None;
        }
        let v = u64::from_le_bytes([
            bytes[*pos],
            bytes[*pos + 1],
            bytes[*pos + 2],
            bytes[*pos + 3],
            bytes[*pos + 4],
            bytes[*pos + 5],
            bytes[*pos + 6],
            bytes[*pos + 7],
        ]);
        *pos += 8;
        Some(v)
    };

    // Version byte check — 0x02 and 0x03 supported.
    if pos >= bytes.len() || (bytes[pos] != 0x02 && bytes[pos] != SNAPSHOT_VERSION) {
        return None;
    }
    let version = bytes[pos];
    pos += 1;

    let session_id = read_u64(&mut pos)?;
    let user_id = read_u64(&mut pos)?;
    let signals_written = read_u64(&mut pos)?;
    let signals_rejected = read_u64(&mut pos)?;
    let duration_ms = read_u64(&mut pos)?;
    let reward_velocity = f64::from_bits(read_u64(&mut pos)?);

    let meta_count = read_u32(&mut pos)? as usize;
    let mut metadata = std::collections::HashMap::with_capacity(meta_count);
    for _ in 0..meta_count {
        let key_len = read_u32(&mut pos)? as usize;
        if pos + key_len > bytes.len() {
            return None;
        }
        let key = String::from_utf8_lossy(&bytes[pos..pos + key_len]).to_string();
        pos += key_len;
        let val_len = read_u32(&mut pos)? as usize;
        if pos + val_len > bytes.len() {
            return None;
        }
        let val = String::from_utf8_lossy(&bytes[pos..pos + val_len]).to_string();
        pos += val_len;
        metadata.insert(key, val);
    }

    // Annotations v0x02: (timestamp_ns: u64, len: u32, bytes).
    let ann_count = read_u32(&mut pos)? as usize;
    let mut annotations: Vec<(u64, String)> = Vec::with_capacity(ann_count);
    for _ in 0..ann_count {
        let ts = read_u64(&mut pos)?;
        let len = read_u32(&mut pos)? as usize;
        if pos + len > bytes.len() {
            return None;
        }
        let ann = String::from_utf8_lossy(&bytes[pos..pos + len]).to_string();
        pos += len;
        annotations.push((ts, ann));
    }

    let ent_count = read_u32(&mut pos)? as usize;
    let mut signaled_entities = Vec::with_capacity(ent_count);
    for _ in 0..ent_count {
        signaled_entities.push(read_u64(&mut pos)?);
    }

    // Audit truncated flag (v0x02).
    let audit_truncated = if pos < bytes.len() {
        let v = bytes[pos] != 0;
        pos += 1;
        v
    } else {
        false
    };

    // Audit log.
    let audit_log = if pos < bytes.len() {
        let audit_count = read_u32(&mut pos)? as usize;
        let mut entries = Vec::with_capacity(audit_count);
        for _ in 0..audit_count {
            let ts = read_u64(&mut pos)?;
            let sig_len = read_u32(&mut pos)? as usize;
            if pos + sig_len > bytes.len() {
                break;
            }
            let sig_type = String::from_utf8_lossy(&bytes[pos..pos + sig_len]).to_string();
            pos += sig_len;
            if pos >= bytes.len() {
                break;
            }
            let accepted = bytes[pos] != 0;
            pos += 1;
            let reason_len = read_u32(&mut pos)? as usize;
            let reason = if reason_len == 0 {
                None
            } else {
                if pos + reason_len > bytes.len() {
                    break;
                }
                let r = String::from_utf8_lossy(&bytes[pos..pos + reason_len]).to_string();
                pos += reason_len;
                Some(r)
            };
            entries.push(AuditEntry {
                timestamp_ns: ts,
                signal_type: sig_type,
                accepted,
                reason,
            });
        }
        entries
    } else {
        Vec::new()
    };

    // Per-signal stats (v0x02/v0x03).
    let signals: std::collections::HashMap<String, SignalSnapEntry> = if pos < bytes.len() {
        let sig_count = read_u32(&mut pos)? as usize;
        let mut map = std::collections::HashMap::with_capacity(sig_count);
        for _ in 0..sig_count {
            let name_len = read_u32(&mut pos)? as usize;
            if pos + name_len > bytes.len() {
                break;
            }
            let name = String::from_utf8_lossy(&bytes[pos..pos + name_len]).to_string();
            pos += name_len;
            let decay_score = f64::from_bits(read_u64(&mut pos)?);
            let window_1h = read_u64(&mut pos)?;
            // v0x03 adds count; v0x02 defaults to 0.
            let count = if version >= 0x03 {
                read_u64(&mut pos)?
            } else {
                0
            };
            map.insert(
                name,
                SignalSnapEntry {
                    decay_score,
                    window_1h,
                    count,
                },
            );
        }
        map
    } else {
        std::collections::HashMap::new()
    };

    // v0x03: session timing fields. v0x02 defaults to 0.
    let (started_at_ns, closed_at_ns) = if version >= 0x03 && pos < bytes.len() {
        let s = read_u64(&mut pos)?;
        let c = read_u64(&mut pos)?;
        (s, c)
    } else {
        (0, 0)
    };

    Some(SessionSnapshot {
        id: SessionId(session_id),
        user_id,
        signals_written,
        signals_rejected,
        duration_ms,
        metadata,
        annotations,
        reward_velocity,
        signaled_entities,
        audit_log,
        audit_truncated,
        signals,
        started_at_ns,
        closed_at_ns,
    })
}

// ── Audit log ─────────────────────────────────────────────────────────────────

/// Serialize an audit log to bytes for storage archival.
#[must_use]
#[allow(clippy::cast_possible_truncation)]
pub fn serialize_audit_log(entries: &[AuditEntry]) -> Vec<u8> {
    let mut buf = Vec::new();
    buf.extend_from_slice(&(entries.len() as u32).to_le_bytes());
    for e in entries {
        buf.extend_from_slice(&e.timestamp_ns.to_le_bytes());
        buf.extend_from_slice(&(e.signal_type.len() as u32).to_le_bytes());
        buf.extend_from_slice(e.signal_type.as_bytes());
        buf.push(u8::from(e.accepted));
        match &e.reason {
            None => buf.extend_from_slice(&0u32.to_le_bytes()),
            Some(r) => {
                buf.extend_from_slice(&(r.len() as u32).to_le_bytes());
                buf.extend_from_slice(r.as_bytes());
            }
        }
    }
    buf
}

/// Deserialize an audit log from bytes.
#[must_use]
pub fn deserialize_audit_log(bytes: &[u8]) -> Vec<AuditEntry> {
    let mut entries = Vec::new();
    let mut pos = 0;

    if pos + 4 > bytes.len() {
        return entries;
    }
    let count =
        u32::from_le_bytes([bytes[pos], bytes[pos + 1], bytes[pos + 2], bytes[pos + 3]]) as usize;
    pos += 4;

    for _ in 0..count {
        if pos + 8 > bytes.len() {
            break;
        }
        let ts = u64::from_le_bytes([
            bytes[pos],
            bytes[pos + 1],
            bytes[pos + 2],
            bytes[pos + 3],
            bytes[pos + 4],
            bytes[pos + 5],
            bytes[pos + 6],
            bytes[pos + 7],
        ]);
        pos += 8;

        if pos + 4 > bytes.len() {
            break;
        }
        let sig_len =
            u32::from_le_bytes([bytes[pos], bytes[pos + 1], bytes[pos + 2], bytes[pos + 3]])
                as usize;
        pos += 4;

        if pos + sig_len > bytes.len() {
            break;
        }
        let sig_type = String::from_utf8_lossy(&bytes[pos..pos + sig_len]).to_string();
        pos += sig_len;

        if pos >= bytes.len() {
            break;
        }
        let accepted = bytes[pos] != 0;
        pos += 1;

        if pos + 4 > bytes.len() {
            break;
        }
        let reason_len =
            u32::from_le_bytes([bytes[pos], bytes[pos + 1], bytes[pos + 2], bytes[pos + 3]])
                as usize;
        pos += 4;

        let reason = if reason_len == 0 {
            None
        } else {
            if pos + reason_len > bytes.len() {
                break;
            }
            let r = String::from_utf8_lossy(&bytes[pos..pos + reason_len]).to_string();
            pos += reason_len;
            Some(r)
        };

        entries.push(AuditEntry {
            timestamp_ns: ts,
            signal_type: sig_type,
            accepted,
            reason,
        });
    }

    entries
}

// ── Unit tests ────────────────────────────────────────────────────────────────

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
    use super::*;
    use std::collections::HashMap;

    use super::super::signal_state::SignalSnapEntry;
    use super::super::types::SessionId;

    #[test]
    fn snapshot_roundtrip() {
        let snap = SessionSnapshot {
            id: SessionId(7),
            user_id: 42,
            signals_written: 10,
            signals_rejected: 2,
            duration_ms: 1234,
            metadata: {
                let mut m = HashMap::new();
                m.insert("agent".to_string(), "planner".to_string());
                m
            },
            annotations: vec![(1_000_000_u64, "rust programming".to_string())],
            reward_velocity: 0.75,
            signaled_entities: vec![1, 2, 3],
            audit_log: Vec::new(),
            audit_truncated: false,
            signals: {
                let mut s = HashMap::new();
                s.insert(
                    "reward".to_string(),
                    SignalSnapEntry {
                        decay_score: 0.75,
                        window_1h: 3,
                        count: 5,
                    },
                );
                s
            },
            started_at_ns: 1_700_000_000_000_000_000,
            closed_at_ns: 1_700_000_001_000_000_000,
        };

        let bytes = serialize_snapshot(&snap);
        let restored = deserialize_snapshot(&bytes).unwrap();

        assert_eq!(restored.id, snap.id);
        assert_eq!(restored.user_id, snap.user_id);
        assert_eq!(restored.signals_written, snap.signals_written);
        assert_eq!(restored.signals_rejected, snap.signals_rejected);
        assert_eq!(restored.duration_ms, snap.duration_ms);
        assert_eq!(restored.annotations, snap.annotations);
        assert!((restored.reward_velocity - snap.reward_velocity).abs() < 1e-10);
        assert_eq!(restored.signaled_entities, snap.signaled_entities);
        assert_eq!(restored.metadata.get("agent").unwrap(), "planner");
        assert!(!restored.audit_truncated);
        let reward_snap = restored.signals.get("reward").unwrap();
        assert!((reward_snap.decay_score - 0.75).abs() < 1e-10);
        assert_eq!(reward_snap.window_1h, 3);
        assert_eq!(reward_snap.count, 5);
        assert_eq!(restored.started_at_ns, 1_700_000_000_000_000_000);
        assert_eq!(restored.closed_at_ns, 1_700_000_001_000_000_000);
    }

    #[test]
    fn audit_log_roundtrip() {
        let entries = vec![
            AuditEntry {
                timestamp_ns: 1000,
                signal_type: "view".to_string(),
                accepted: true,
                reason: None,
            },
            AuditEntry {
                timestamp_ns: 2000,
                signal_type: "block".to_string(),
                accepted: false,
                reason: Some("signal denied by policy".to_string()),
            },
        ];

        let bytes = serialize_audit_log(&entries);
        let restored = deserialize_audit_log(&bytes);

        assert_eq!(restored.len(), 2);
        assert_eq!(restored[0].signal_type, "view");
        assert!(restored[0].accepted);
        assert!(restored[0].reason.is_none());
        assert_eq!(restored[1].signal_type, "block");
        assert!(!restored[1].accepted);
        assert_eq!(
            restored[1].reason.as_deref().unwrap(),
            "signal denied by policy"
        );
    }
}
