use anyhow::{Result, anyhow};

/// Represents one 16-byte idx record: little-endian (ts_millis: u64) + (offset: u64)
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IdxLine {
    ts_millis: u64,
    offset: u64,
}

impl IdxLine {
    /// constructor
    pub fn new(ts_millis: u64, offset: u64) -> Self {
        Self { ts_millis, offset }
    }

    /// Parse a 16-byte little-endian idx record (accepts any &[u8], validates length)
    pub fn parse_bytes(buf: &[u8]) -> Result<Self> {
        if buf.len() != 16 {
            return Err(anyhow!(
                "expected 16 bytes for idx record, got {}",
                buf.len()
            ));
        }
        let mut ts_b = [0u8; 8];
        let mut off_b = [0u8; 8];
        ts_b.copy_from_slice(&buf[0..8]);
        off_b.copy_from_slice(&buf[8..16]);
        let ts_millis = u64::from_le_bytes(ts_b);
        let offset = u64::from_le_bytes(off_b);
        Ok(IdxLine::new(ts_millis, offset))
    }

    /// Serialize into 16-byte little-endian representation (ts_millis + offset)
    /// Writes into the provided buffer (must be length 16).
    pub fn to_bytes(&self, out: &mut [u8]) -> Result<()> {
        if out.len() != 16 {
            return Err(anyhow!("output buffer must be 16 bytes, got {}", out.len()));
        }
        let ts_b = self.ts_millis.to_le_bytes();
        let off_b = self.offset.to_le_bytes();
        out[0..8].copy_from_slice(&ts_b);
        out[8..16].copy_from_slice(&off_b);
        Ok(())
    }

    /// accessors
    pub fn ts_millis(&self) -> u64 {
        self.ts_millis
    }

    pub fn offset(&self) -> u64 {
        self.offset
    }

    /// optional convenience: convert stored millis -> seconds (i64) with same heuristic as before
    pub fn ts_millis_to_secs_i64(&self) -> i64 {
        const MILLIS_THRESHOLD: u64 = 3_000_000_000u64; // threshold to decide ms vs s
        if self.ts_millis > MILLIS_THRESHOLD {
            (self.ts_millis / 1000) as i64
        } else {
            self.ts_millis as i64
        }
    }
}

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

    // Predefined constants to avoid repeating magic literals in tests
    const TEST_TS_MILLIS: u64 = 1_700_000_000_000u64;
    const TEST_OFFSET: u64 = 12345u64;
    // Expected little-endian byte sequence: TS (LE) then OFFSET (LE)
    const EXPECTED_BYTES: [u8; 16] = [
        0x00, 0x68, 0xE5, 0xCF, 0x8B, 0x01, 0x00, 0x00, // 1_700_000_000_000 LE
        0x39, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 12345 LE
    ];

    #[test]
    fn parse_and_serialize_roundtrip() {
        // construct using named constants
        let original = IdxLine::new(TEST_TS_MILLIS, TEST_OFFSET);
        let mut bytes = [0u8; 16];
        original
            .to_bytes(&mut bytes)
            .expect("to_bytes should succeed");

        // compare produced bytes to expected literal representation
        assert_eq!(bytes, EXPECTED_BYTES);

        let parsed = IdxLine::parse_bytes(&bytes).expect("parse should succeed");
        assert_eq!(parsed.ts_millis(), original.ts_millis());
        assert_eq!(parsed.offset(), original.offset());
    }

    #[test]
    fn ts_millis_to_secs_i64_from_millis() {
        let l = IdxLine::new(TEST_TS_MILLIS, 0);
        assert_eq!(l.ts_millis_to_secs_i64(), 1_700_000_000i64);
    }

    #[test]
    fn ts_millis_to_secs_i64_from_secs() {
        let l = IdxLine::new(1_700_000_000u64, 0);
        assert_eq!(l.ts_millis_to_secs_i64(), 1_700_000_000i64);
    }
}
