//! Binary encoding and decoding for session start records.
//!
//! A start record is written to storage on `start_session` and deleted (replaced
//! by a snapshot) when the session closes. On restart, surviving start records
//! indicate sessions that were active at crash time; their metadata is
//! rehydrated from this record during WAL replay.

use super::super::state::SessionState;
use super::super::types::SessionId;

/// Serialize a compact session start record.
///
/// Written to storage on `start_session`; deleted when the session is closed
/// and replaced by a snapshot record.
///
/// Format: `[session_id: 8][user_id: 8][started_at_ns: 8][metadata_count: u32][...kv...]`
#[must_use]
#[allow(clippy::cast_possible_truncation)]
pub fn serialize_start_record(state: &SessionState) -> Vec<u8> {
    let mut buf = Vec::new();
    buf.extend_from_slice(&state.id.as_u64().to_le_bytes());
    buf.extend_from_slice(&state.user_id.to_le_bytes());
    buf.extend_from_slice(&state.started_at_ns.to_le_bytes());
    buf.extend_from_slice(&(state.metadata.len() as u32).to_le_bytes());
    for (k, v) in &state.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());
    }
    buf
}

/// Deserialize a session start record.
///
/// Returns `(session_id, user_id, started_at_ns, metadata)`.
/// Older records written before metadata support was added return an empty map
/// rather than `None` — partial reads are treated as empty metadata, not errors.
///
/// Returns `None` only if the mandatory first 24 bytes are missing.
#[must_use]
#[allow(clippy::cast_possible_truncation)]
pub fn deserialize_start_record(
    bytes: &[u8],
) -> Option<(
    SessionId,
    u64,
    u64,
    std::collections::HashMap<String, String>,
)> {
    if bytes.len() < 24 {
        return None;
    }
    let session_id = u64::from_le_bytes([
        bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
    ]);
    let user_id = u64::from_le_bytes([
        bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15],
    ]);
    let started_at_ns = u64::from_le_bytes([
        bytes[16], bytes[17], bytes[18], bytes[19], bytes[20], bytes[21], bytes[22], bytes[23],
    ]);

    // Metadata section: [count: u32][key_len: u32][key bytes][val_len: u32][val bytes]...
    // Records written before metadata support stop at byte 24 — return empty map.
    let mut pos = 24usize;
    let read_u32 = |p: &mut usize| -> Option<u32> {
        if *p + 4 > bytes.len() {
            return None;
        }
        let v = u32::from_le_bytes([bytes[*p], bytes[*p + 1], bytes[*p + 2], bytes[*p + 3]]);
        *p += 4;
        Some(v)
    };
    let metadata = 'parse: {
        let Some(count) = read_u32(&mut pos) else {
            break 'parse std::collections::HashMap::new();
        };
        let mut map = std::collections::HashMap::with_capacity(count as usize);
        for _ in 0..count {
            let Some(key_len) = read_u32(&mut pos) else {
                break 'parse map;
            };
            let key_len = key_len as usize;
            if pos + key_len > bytes.len() {
                break 'parse map;
            }
            let key = String::from_utf8_lossy(&bytes[pos..pos + key_len]).to_string();
            pos += key_len;

            let Some(val_len) = read_u32(&mut pos) else {
                break 'parse map;
            };
            let val_len = val_len as usize;
            if pos + val_len > bytes.len() {
                break 'parse map;
            }
            let val = String::from_utf8_lossy(&bytes[pos..pos + val_len]).to_string();
            pos += val_len;

            map.insert(key, val);
        }
        map
    };

    Some((
        SessionId::from_raw(session_id),
        user_id,
        started_at_ns,
        metadata,
    ))
}

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

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
    use std::collections::HashMap;
    use std::sync::Arc;
    use std::sync::atomic::{AtomicBool, AtomicU64};

    use super::{deserialize_start_record, serialize_start_record};
    use crate::session::audit::AuditLog;
    use crate::session::state::SessionState;
    use crate::session::types::{AgentId, SessionId};

    fn make_state(metadata: HashMap<String, String>) -> SessionState {
        let closed = Arc::new(AtomicBool::new(false));
        SessionState {
            id: SessionId(42),
            user_id: 7,
            agent_id: AgentId::new("agent-a").unwrap(),
            policy_name: "default".to_string(),
            started_at: std::time::Instant::now(),
            started_at_ns: 123_456_789,
            metadata,
            signals: dashmap::DashMap::new(),
            signaled_entities: dashmap::DashMap::new(),
            annotations: std::sync::Mutex::new(Vec::new()),
            signals_written: AtomicU64::new(0),
            signals_rejected: AtomicU64::new(0),
            audit_log: std::sync::Mutex::new(AuditLog::new()),
            closed,
        }
    }

    #[test]
    fn start_record_roundtrip_with_metadata() {
        let mut metadata = HashMap::new();
        metadata.insert("tool".to_string(), "planner".to_string());
        metadata.insert("context".to_string(), "daily-feed".to_string());

        let state = make_state(metadata);
        let bytes = serialize_start_record(&state);
        let (session_id, user_id, started_at_ns, metadata) =
            deserialize_start_record(&bytes).unwrap();

        assert_eq!(session_id, SessionId(42));
        assert_eq!(user_id, 7);
        assert_eq!(started_at_ns, 123_456_789);
        assert_eq!(metadata.get("tool").map(String::as_str), Some("planner"));
        assert_eq!(
            metadata.get("context").map(String::as_str),
            Some("daily-feed")
        );
    }

    #[test]
    fn start_record_roundtrip_empty_metadata() {
        let state = make_state(HashMap::new());
        let bytes = serialize_start_record(&state);
        let (session_id, user_id, started_at_ns, metadata) =
            deserialize_start_record(&bytes).unwrap();

        assert_eq!(session_id, SessionId(42));
        assert_eq!(user_id, 7);
        assert_eq!(started_at_ns, 123_456_789);
        assert!(metadata.is_empty());
    }

    #[test]
    fn start_record_backward_compat_no_metadata() {
        // A record with only the 24-byte header (pre-metadata format) must parse
        // without error and return an empty metadata map.
        let mut buf = Vec::new();
        buf.extend_from_slice(&42u64.to_le_bytes()); // session_id
        buf.extend_from_slice(&7u64.to_le_bytes()); // user_id
        buf.extend_from_slice(&999u64.to_le_bytes()); // started_at_ns
        // No metadata bytes — simulates a record written before this fix.

        let (session_id, user_id, started_at_ns, metadata) =
            deserialize_start_record(&buf).unwrap();
        assert_eq!(session_id, SessionId(42));
        assert_eq!(user_id, 7);
        assert_eq!(started_at_ns, 999);
        assert!(
            metadata.is_empty(),
            "backward-compat: metadata should be empty"
        );
    }
}
