use crate::error::{Result, SdJournalError};
use crate::util::{hex_decode, hex_encode};
use std::fmt;

const PREFIX_V1: &str = "SJ1:";

const TAG_FILE_OFFSET: u8 = 0x01;
const TAG_SYSTEMD_RAW: u8 = 0x02;
const TAG_SYSTEMD_FIELDS: u8 = 0x03;
const TAG_ENTRY_KEY: u8 = 0x04;

const SYSTEMD_MASK_SEQNUM_ID: u8 = 1 << 0;
const SYSTEMD_MASK_SEQNUM: u8 = 1 << 1;
const SYSTEMD_MASK_BOOT_ID: u8 = 1 << 2;
const SYSTEMD_MASK_MONOTONIC: u8 = 1 << 3;
const SYSTEMD_MASK_REALTIME: u8 = 1 << 4;
const SYSTEMD_MASK_XOR_HASH: u8 = 1 << 5;

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum CursorKind {
    FileOffset {
        file_id: [u8; 16],
        entry_offset: u64,
    },
    EntryKey(SdJournalEntryKey),
    Systemd(SystemdCursor),
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) struct SdJournalEntryKey {
    pub(crate) file_id: [u8; 16],
    pub(crate) entry_offset: u64,
    pub(crate) seqnum: u64,
    pub(crate) realtime_usec: u64,
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) struct SystemdCursor {
    pub(crate) seqnum_id: Option<[u8; 16]>,
    pub(crate) seqnum: Option<u64>,
    pub(crate) boot_id: Option<[u8; 16]>,
    pub(crate) monotonic_usec: Option<u64>,
    pub(crate) realtime_usec: Option<u64>,
    pub(crate) xor_hash: Option<u64>,
}

impl SystemdCursor {
    fn is_valid(&self) -> bool {
        (self.seqnum_id.is_some() && self.seqnum.is_some())
            || (self.boot_id.is_some() && self.monotonic_usec.is_some())
            || self.realtime_usec.is_some()
    }

    fn systemd_wire_string(&self) -> Option<String> {
        let seqnum_id = self.seqnum_id?;
        let seqnum = self.seqnum?;
        let boot_id = self.boot_id?;
        let monotonic_usec = self.monotonic_usec?;
        let realtime_usec = self.realtime_usec?;
        let xor_hash = self.xor_hash?;

        Some(format!(
            "s={};i={seqnum:x};b={};m={monotonic_usec:x};t={realtime_usec:x};x={xor_hash:x}",
            crate::util::hex_encode(&seqnum_id),
            crate::util::hex_encode(&boot_id),
        ))
    }
}

/// Opaque cursor for checkpointing and resuming journal iteration.
///
/// A cursor can be round-tripped through [`std::string::ToString::to_string`] and
/// [`Cursor::parse`]. The textual representation is stable for `sdjournal`-generated cursors and
/// also accepts native systemd cursor strings for resume interoperability.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Cursor {
    kind: CursorKind,
}

impl Cursor {
    pub(crate) fn new_entry_key(
        file_id: [u8; 16],
        entry_offset: u64,
        seqnum: u64,
        realtime_usec: u64,
    ) -> Self {
        Self {
            kind: CursorKind::EntryKey(SdJournalEntryKey {
                file_id,
                entry_offset,
                seqnum,
                realtime_usec,
            }),
        }
    }

    pub(crate) fn file_offset(&self) -> Option<([u8; 16], u64)> {
        match &self.kind {
            CursorKind::FileOffset {
                file_id,
                entry_offset,
            } => Some((*file_id, *entry_offset)),
            CursorKind::EntryKey(k) => Some((k.file_id, k.entry_offset)),
            CursorKind::Systemd(_) => None,
        }
    }

    pub(crate) fn sdjournal_entry_key(&self) -> Option<SdJournalEntryKey> {
        match &self.kind {
            CursorKind::EntryKey(k) => Some(*k),
            CursorKind::FileOffset { .. } | CursorKind::Systemd(_) => None,
        }
    }

    pub(crate) fn systemd(&self) -> Option<&SystemdCursor> {
        match &self.kind {
            CursorKind::Systemd(v) => Some(v),
            CursorKind::FileOffset { .. } | CursorKind::EntryKey(_) => None,
        }
    }

    /// Parse a cursor from a string.
    ///
    /// This accepts both the `sdjournal` versioned format (`SJ1:...`) and native systemd cursor
    /// strings.
    ///
    /// The returned cursor can be passed to [`crate::Journal::seek_cursor`] or
    /// [`crate::JournalQuery::after_cursor`].
    pub fn parse(s: &str) -> Result<Self> {
        let s = s.trim();
        if let Some(rest) = s.strip_prefix(PREFIX_V1) {
            let bytes = hex_decode(rest)?;
            let tag = *bytes.first().ok_or_else(|| SdJournalError::InvalidQuery {
                reason: "empty cursor payload".to_string(),
            })?;
            match tag {
                TAG_FILE_OFFSET => {
                    if bytes.len() != 1 + 16 + 8 {
                        return Err(SdJournalError::InvalidQuery {
                            reason: "invalid cursor length for SJ1 FileOffset".to_string(),
                        });
                    }
                    let mut file_id = [0u8; 16];
                    file_id.copy_from_slice(&bytes[1..17]);
                    let mut off = [0u8; 8];
                    off.copy_from_slice(&bytes[17..25]);
                    Ok(Cursor {
                        kind: CursorKind::FileOffset {
                            file_id,
                            entry_offset: u64::from_le_bytes(off),
                        },
                    })
                }
                TAG_ENTRY_KEY => {
                    if bytes.len() != 1 + 16 + 8 + 8 + 8 {
                        return Err(SdJournalError::InvalidQuery {
                            reason: "invalid cursor length for SJ1 EntryKey".to_string(),
                        });
                    }

                    let mut file_id = [0u8; 16];
                    file_id.copy_from_slice(&bytes[1..17]);

                    let mut entry_offset = [0u8; 8];
                    entry_offset.copy_from_slice(&bytes[17..25]);

                    let mut seqnum = [0u8; 8];
                    seqnum.copy_from_slice(&bytes[25..33]);

                    let mut realtime = [0u8; 8];
                    realtime.copy_from_slice(&bytes[33..41]);

                    Ok(Cursor {
                        kind: CursorKind::EntryKey(SdJournalEntryKey {
                            file_id,
                            entry_offset: u64::from_le_bytes(entry_offset),
                            seqnum: u64::from_le_bytes(seqnum),
                            realtime_usec: u64::from_le_bytes(realtime),
                        }),
                    })
                }
                TAG_SYSTEMD_RAW => {
                    let raw = String::from_utf8(bytes[1..].to_vec()).map_err(|_| {
                        SdJournalError::InvalidQuery {
                            reason: "invalid UTF-8 in SJ1 systemd cursor".to_string(),
                        }
                    })?;
                    Ok(Cursor {
                        kind: CursorKind::Systemd(parse_systemd_cursor(&raw)?),
                    })
                }
                TAG_SYSTEMD_FIELDS => Ok(Cursor {
                    kind: CursorKind::Systemd(parse_systemd_fields(&bytes[1..])?),
                }),
                _ => Err(SdJournalError::Unsupported {
                    reason: "unknown SJ1 cursor variant".to_string(),
                }),
            }
        } else {
            Ok(Cursor {
                kind: CursorKind::Systemd(parse_systemd_cursor(s)?),
            })
        }
    }
}

impl fmt::Display for Cursor {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let mut bytes = Vec::new();
        match &self.kind {
            CursorKind::FileOffset {
                file_id,
                entry_offset,
            } => {
                bytes.push(TAG_FILE_OFFSET);
                bytes.extend_from_slice(file_id);
                bytes.extend_from_slice(&entry_offset.to_le_bytes());
            }
            CursorKind::EntryKey(k) => {
                bytes.push(TAG_ENTRY_KEY);
                bytes.extend_from_slice(&k.file_id);
                bytes.extend_from_slice(&k.entry_offset.to_le_bytes());
                bytes.extend_from_slice(&k.seqnum.to_le_bytes());
                bytes.extend_from_slice(&k.realtime_usec.to_le_bytes());
            }
            CursorKind::Systemd(fields) => {
                if let Some(raw) = fields.systemd_wire_string() {
                    bytes.push(TAG_SYSTEMD_RAW);
                    bytes.extend_from_slice(raw.as_bytes());
                } else {
                    bytes.push(TAG_SYSTEMD_FIELDS);
                    let mut mask = 0u8;
                    if fields.seqnum_id.is_some() {
                        mask |= SYSTEMD_MASK_SEQNUM_ID;
                    }
                    if fields.seqnum.is_some() {
                        mask |= SYSTEMD_MASK_SEQNUM;
                    }
                    if fields.boot_id.is_some() {
                        mask |= SYSTEMD_MASK_BOOT_ID;
                    }
                    if fields.monotonic_usec.is_some() {
                        mask |= SYSTEMD_MASK_MONOTONIC;
                    }
                    if fields.realtime_usec.is_some() {
                        mask |= SYSTEMD_MASK_REALTIME;
                    }
                    if fields.xor_hash.is_some() {
                        mask |= SYSTEMD_MASK_XOR_HASH;
                    }

                    bytes.push(mask);
                    if let Some(v) = fields.seqnum_id {
                        bytes.extend_from_slice(&v);
                    }
                    if let Some(v) = fields.seqnum {
                        bytes.extend_from_slice(&v.to_le_bytes());
                    }
                    if let Some(v) = fields.boot_id {
                        bytes.extend_from_slice(&v);
                    }
                    if let Some(v) = fields.monotonic_usec {
                        bytes.extend_from_slice(&v.to_le_bytes());
                    }
                    if let Some(v) = fields.realtime_usec {
                        bytes.extend_from_slice(&v.to_le_bytes());
                    }
                    if let Some(v) = fields.xor_hash {
                        bytes.extend_from_slice(&v.to_le_bytes());
                    }
                }
            }
        }
        write!(f, "{PREFIX_V1}{}", hex_encode(&bytes))
    }
}

fn parse_systemd_fields(mut bytes: &[u8]) -> Result<SystemdCursor> {
    let mask = *bytes.first().ok_or_else(|| SdJournalError::InvalidQuery {
        reason: "missing systemd cursor mask".to_string(),
    })?;
    bytes = &bytes[1..];

    fn take<const N: usize>(bytes: &mut &[u8], reason: &'static str) -> Result<[u8; N]> {
        if bytes.len() < N {
            return Err(SdJournalError::InvalidQuery {
                reason: reason.to_string(),
            });
        }
        let mut out = [0u8; N];
        out.copy_from_slice(&bytes[..N]);
        *bytes = &bytes[N..];
        Ok(out)
    }

    let seqnum_id = if mask & SYSTEMD_MASK_SEQNUM_ID != 0 {
        Some(take::<16>(&mut bytes, "systemd cursor missing seqnum_id")?)
    } else {
        None
    };
    let seqnum = if mask & SYSTEMD_MASK_SEQNUM != 0 {
        Some(u64::from_le_bytes(take::<8>(
            &mut bytes,
            "systemd cursor missing seqnum",
        )?))
    } else {
        None
    };
    let boot_id = if mask & SYSTEMD_MASK_BOOT_ID != 0 {
        Some(take::<16>(&mut bytes, "systemd cursor missing boot_id")?)
    } else {
        None
    };
    let monotonic_usec = if mask & SYSTEMD_MASK_MONOTONIC != 0 {
        Some(u64::from_le_bytes(take::<8>(
            &mut bytes,
            "systemd cursor missing monotonic_usec",
        )?))
    } else {
        None
    };
    let realtime_usec = if mask & SYSTEMD_MASK_REALTIME != 0 {
        Some(u64::from_le_bytes(take::<8>(
            &mut bytes,
            "systemd cursor missing realtime_usec",
        )?))
    } else {
        None
    };
    let xor_hash = if mask & SYSTEMD_MASK_XOR_HASH != 0 {
        Some(u64::from_le_bytes(take::<8>(
            &mut bytes,
            "systemd cursor missing xor_hash",
        )?))
    } else {
        None
    };

    if !bytes.is_empty() {
        return Err(SdJournalError::InvalidQuery {
            reason: "trailing bytes in systemd cursor payload".to_string(),
        });
    }

    let out = SystemdCursor {
        seqnum_id,
        seqnum,
        boot_id,
        monotonic_usec,
        realtime_usec,
        xor_hash,
    };
    if !out.is_valid() {
        return Err(SdJournalError::InvalidQuery {
            reason: "systemd cursor missing required fields".to_string(),
        });
    }

    Ok(out)
}

fn parse_systemd_cursor(s: &str) -> Result<SystemdCursor> {
    let s = s.trim();
    if s.is_empty() {
        return Err(SdJournalError::InvalidQuery {
            reason: "empty cursor".to_string(),
        });
    }

    fn parse_id128(s: &str, kind: &'static str) -> Result<[u8; 16]> {
        use std::borrow::Cow;

        let cleaned = if s.contains('-') {
            Cow::Owned(s.chars().filter(|&c| c != '-').collect::<String>())
        } else {
            Cow::Borrowed(s)
        };

        let bytes = hex_decode(cleaned.as_ref())?;
        if bytes.len() != 16 {
            return Err(SdJournalError::InvalidQuery {
                reason: format!("{kind} must be 16 bytes (32 hex chars)"),
            });
        }
        let mut out = [0u8; 16];
        out.copy_from_slice(&bytes);
        Ok(out)
    }

    fn parse_hex_u64(s: &str, kind: &'static str) -> Result<u64> {
        u64::from_str_radix(s, 16).map_err(|_| SdJournalError::InvalidQuery {
            reason: format!("{kind} is not valid hex"),
        })
    }

    let mut out = SystemdCursor {
        seqnum_id: None,
        seqnum: None,
        boot_id: None,
        monotonic_usec: None,
        realtime_usec: None,
        xor_hash: None,
    };

    for part in s.split(';') {
        if part.is_empty() {
            return Err(SdJournalError::InvalidQuery {
                reason: "invalid systemd cursor: empty segment".to_string(),
            });
        }
        let bytes = part.as_bytes();
        if bytes.len() < 2 || bytes[1] != b'=' {
            return Err(SdJournalError::InvalidQuery {
                reason: "invalid systemd cursor segment".to_string(),
            });
        }
        let key = bytes[0];
        let value = &part[2..];
        match key {
            b's' => out.seqnum_id = Some(parse_id128(value, "seqnum_id")?),
            b'i' => out.seqnum = Some(parse_hex_u64(value, "seqnum")?),
            b'b' => out.boot_id = Some(parse_id128(value, "boot_id")?),
            b'm' => out.monotonic_usec = Some(parse_hex_u64(value, "monotonic_usec")?),
            b't' => out.realtime_usec = Some(parse_hex_u64(value, "realtime_usec")?),
            b'x' => out.xor_hash = Some(parse_hex_u64(value, "xor_hash")?),
            _ => {}
        }
    }

    if !out.is_valid() {
        return Err(SdJournalError::InvalidQuery {
            reason: "systemd cursor missing required fields".to_string(),
        });
    }

    Ok(out)
}

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

    #[test]
    fn cursor_file_offset_roundtrip() {
        let file_id = [0x11u8; 16];
        let entry_offset = 0x1122334455667788u64;

        let mut bytes = Vec::new();
        bytes.push(TAG_FILE_OFFSET);
        bytes.extend_from_slice(&file_id);
        bytes.extend_from_slice(&entry_offset.to_le_bytes());
        let s = format!("{PREFIX_V1}{}", hex_encode(&bytes));
        assert!(s.starts_with(PREFIX_V1));

        let parsed = Cursor::parse(&s).unwrap();
        assert_eq!(parsed.file_offset(), Some((file_id, entry_offset)));
        assert_eq!(parsed.to_string(), s);
    }

    #[test]
    fn cursor_systemd_string_is_parsed() {
        let c = Cursor::parse(
            "s=0123456789abcdef0123456789abcdef;i=1;b=11111111111111111111111111111111;m=2;t=3;x=4",
        )
        .unwrap();
        let s = c.to_string();
        assert!(s.starts_with(PREFIX_V1));

        let parsed = Cursor::parse(&s).unwrap();
        assert_eq!(parsed.to_string(), s);
        assert!(parsed.file_offset().is_none());
        assert!(parsed.systemd().is_some());
    }

    #[test]
    fn cursor_entry_key_roundtrip() {
        let file_id = [0x11u8; 16];
        let entry_offset = 0x1122334455667788u64;
        let seqnum = 0x99aabbccddu64;
        let realtime_usec = 0x11220000u64;
        let c = Cursor::new_entry_key(file_id, entry_offset, seqnum, realtime_usec);
        let s = c.to_string();
        assert!(s.starts_with(PREFIX_V1));

        let parsed = Cursor::parse(&s).unwrap();
        assert_eq!(
            parsed.sdjournal_entry_key(),
            Some(SdJournalEntryKey {
                file_id,
                entry_offset,
                seqnum,
                realtime_usec
            })
        );
        assert_eq!(parsed.to_string(), s);
    }

    #[test]
    fn cursor_systemd_fields_variant_roundtrip() {
        let c = Cursor::parse("t=3").unwrap();
        let s = c.to_string();
        assert!(s.starts_with(PREFIX_V1));

        let parsed = Cursor::parse(&s).unwrap();
        assert_eq!(parsed.to_string(), s);
        assert!(parsed.systemd().is_some());
        assert!(parsed.file_offset().is_none());
    }

    #[test]
    fn cursor_rejects_empty_systemd_cursor() {
        match Cursor::parse("") {
            Ok(_) => panic!("expected InvalidQuery"),
            Err(err) => assert!(matches!(err, SdJournalError::InvalidQuery { .. })),
        }
    }

    #[test]
    fn cursor_rejects_empty_sj1_payload() {
        match Cursor::parse(PREFIX_V1) {
            Ok(_) => panic!("expected InvalidQuery"),
            Err(err) => assert!(matches!(err, SdJournalError::InvalidQuery { .. })),
        }
    }

    #[test]
    fn cursor_rejects_invalid_entry_key_length() {
        let mut bytes = Vec::new();
        bytes.push(TAG_ENTRY_KEY);
        bytes.extend_from_slice(&[0x11u8; 16]);
        let s = format!("{PREFIX_V1}{}", hex_encode(&bytes));

        match Cursor::parse(&s) {
            Ok(_) => panic!("expected InvalidQuery"),
            Err(err) => assert!(matches!(err, SdJournalError::InvalidQuery { .. })),
        }
    }

    #[test]
    fn cursor_rejects_unknown_sj1_variant() {
        let s = format!("{PREFIX_V1}{}", hex_encode(&[0xff]));

        match Cursor::parse(&s) {
            Err(SdJournalError::Unsupported { reason }) => {
                assert_eq!(reason, "unknown SJ1 cursor variant");
            }
            other => panic!("unexpected result: {other:?}"),
        }
    }

    #[test]
    fn cursor_rejects_invalid_utf8_in_systemd_raw_variant() {
        let s = format!("{PREFIX_V1}{}", hex_encode(&[TAG_SYSTEMD_RAW, 0xff]));

        match Cursor::parse(&s) {
            Err(SdJournalError::InvalidQuery { reason }) => {
                assert_eq!(reason, "invalid UTF-8 in SJ1 systemd cursor");
            }
            other => panic!("unexpected result: {other:?}"),
        }
    }

    #[test]
    fn cursor_rejects_trailing_bytes_in_systemd_fields_payload() {
        let mut bytes = Vec::new();
        bytes.push(TAG_SYSTEMD_FIELDS);
        bytes.push(SYSTEMD_MASK_REALTIME);
        bytes.extend_from_slice(&3u64.to_le_bytes());
        bytes.push(0xaa);
        let s = format!("{PREFIX_V1}{}", hex_encode(&bytes));

        match Cursor::parse(&s) {
            Err(SdJournalError::InvalidQuery { reason }) => {
                assert_eq!(reason, "trailing bytes in systemd cursor payload");
            }
            other => panic!("unexpected result: {other:?}"),
        }
    }

    #[test]
    fn cursor_rejects_systemd_cursor_without_required_fields() {
        match Cursor::parse("x=1") {
            Err(SdJournalError::InvalidQuery { reason }) => {
                assert_eq!(reason, "systemd cursor missing required fields");
            }
            other => panic!("unexpected result: {other:?}"),
        }
    }

    #[test]
    fn cursor_rejects_invalid_systemd_segment_syntax() {
        match Cursor::parse("broken-segment") {
            Err(SdJournalError::InvalidQuery { reason }) => {
                assert_eq!(reason, "invalid systemd cursor segment");
            }
            other => panic!("unexpected result: {other:?}"),
        }
    }
}
