//! Replicated log for Raft consensus.

use super::state::Command;
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};

/// A single entry in the replicated log.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct LogEntry {
    /// Term when entry was received by leader.
    pub term: u64,
    /// Position in the log (1-indexed).
    pub index: u64,
    /// Command to execute.
    pub command: Command,
    /// Timestamp when entry was created.
    pub timestamp: u64,
}

impl LogEntry {
    /// Creates a new log entry.
    pub fn new(term: u64, index: u64, command: Command) -> Self {
        let timestamp = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap()
            .as_millis() as u64;

        Self {
            term,
            index,
            command,
            timestamp,
        }
    }

    /// Serializes to bytes.
    pub fn to_bytes(&self) -> Vec<u8> {
        bincode::serialize(self).unwrap_or_default()
    }

    /// Deserializes from bytes.
    pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
        bincode::deserialize(bytes).ok()
    }
}

/// Replicated log storing all commands.
pub struct ReplicatedLog {
    /// Log entries.
    entries: RwLock<Vec<LogEntry>>,
    /// Index of first entry in the log (for log compaction).
    start_index: RwLock<u64>,
    /// Term of last included entry (for snapshots).
    snapshot_term: RwLock<u64>,
}

impl ReplicatedLog {
    /// Creates a new empty log.
    pub fn new() -> Self {
        Self {
            entries: RwLock::new(Vec::new()),
            start_index: RwLock::new(1), // 1-indexed
            snapshot_term: RwLock::new(0),
        }
    }

    /// Returns the index of the last entry.
    pub fn last_index(&self) -> u64 {
        let entries = self.entries.read();
        let start = *self.start_index.read();
        if entries.is_empty() {
            start.saturating_sub(1)
        } else {
            start + entries.len() as u64 - 1
        }
    }

    /// Returns the term of the last entry.
    pub fn last_term(&self) -> u64 {
        let entries = self.entries.read();
        if entries.is_empty() {
            *self.snapshot_term.read()
        } else {
            entries.last().map(|e| e.term).unwrap_or(0)
        }
    }

    /// Returns the term at a given index.
    pub fn term_at(&self, index: u64) -> Option<u64> {
        let start = *self.start_index.read();
        if index < start {
            return if index == start - 1 {
                Some(*self.snapshot_term.read())
            } else {
                None
            };
        }

        let entries = self.entries.read();
        let offset = (index - start) as usize;
        entries.get(offset).map(|e| e.term)
    }

    /// Gets an entry by index.
    pub fn get(&self, index: u64) -> Option<LogEntry> {
        let start = *self.start_index.read();
        if index < start {
            return None;
        }

        let entries = self.entries.read();
        let offset = (index - start) as usize;
        entries.get(offset).cloned()
    }

    /// Gets entries from start_index to end_index (inclusive).
    pub fn get_range(&self, start_idx: u64, end_idx: u64) -> Vec<LogEntry> {
        let start = *self.start_index.read();
        if start_idx > end_idx || start_idx < start {
            return Vec::new();
        }

        let entries = self.entries.read();
        let start_offset = (start_idx - start) as usize;
        let end_offset = (end_idx - start + 1) as usize;

        entries
            .get(start_offset..end_offset.min(entries.len()))
            .map(|s| s.to_vec())
            .unwrap_or_default()
    }

    /// Gets all entries from a given index.
    pub fn entries_from(&self, from_index: u64) -> Vec<LogEntry> {
        let start = *self.start_index.read();
        if from_index < start {
            return self.entries.read().clone();
        }

        let entries = self.entries.read();
        let offset = (from_index - start) as usize;
        entries.get(offset..).map(|s| s.to_vec()).unwrap_or_default()
    }

    /// Appends an entry to the log.
    pub fn append(&self, entry: LogEntry) -> u64 {
        let mut entries = self.entries.write();
        let index = entry.index;
        entries.push(entry);
        index
    }

    /// Appends multiple entries, potentially overwriting conflicting entries.
    pub fn append_entries(&self, prev_index: u64, prev_term: u64, new_entries: Vec<LogEntry>) -> bool {
        // Check that prev entry matches
        if prev_index > 0 {
            if let Some(prev_entry_term) = self.term_at(prev_index) {
                if prev_entry_term != prev_term {
                    // Conflict - need to truncate
                    return false;
                }
            } else if prev_index >= *self.start_index.read() {
                // Missing entry
                return false;
            }
        }

        let mut entries = self.entries.write();
        let start = *self.start_index.read();

        for entry in new_entries {
            let offset = (entry.index - start) as usize;

            if offset < entries.len() {
                // Check for conflict
                if entries[offset].term != entry.term {
                    // Delete this and all following entries
                    entries.truncate(offset);
                    entries.push(entry);
                }
                // Otherwise entry already exists with same term, skip
            } else {
                entries.push(entry);
            }
        }

        true
    }

    /// Truncates the log after the given index.
    pub fn truncate_after(&self, index: u64) {
        let start = *self.start_index.read();
        if index < start {
            return;
        }

        let mut entries = self.entries.write();
        let offset = (index - start + 1) as usize;
        entries.truncate(offset);
    }

    /// Returns the number of entries.
    pub fn len(&self) -> usize {
        self.entries.read().len()
    }

    /// Returns true if the log is empty.
    pub fn is_empty(&self) -> bool {
        self.entries.read().is_empty()
    }

    /// Compacts the log up to (and including) the given index.
    pub fn compact(&self, up_to_index: u64, up_to_term: u64) {
        let mut entries = self.entries.write();
        let mut start = self.start_index.write();
        let mut snapshot_term = self.snapshot_term.write();

        if up_to_index < *start {
            return;
        }

        let remove_count = (up_to_index - *start + 1) as usize;
        if remove_count >= entries.len() {
            entries.clear();
        } else {
            entries.drain(0..remove_count);
        }

        *start = up_to_index + 1;
        *snapshot_term = up_to_term;
    }

    /// Checks if this log is at least as up-to-date as the candidate's log.
    /// Used during leader election.
    pub fn is_up_to_date(&self, candidate_last_index: u64, candidate_last_term: u64) -> bool {
        let last_term = self.last_term();
        let last_index = self.last_index();

        // Compare terms first, then indices
        if last_term != candidate_last_term {
            last_term <= candidate_last_term
        } else {
            last_index <= candidate_last_index
        }
    }

    /// Creates entries for replication starting from a given index.
    pub fn entries_for_replication(&self, from_index: u64, max_entries: usize) -> (u64, u64, Vec<LogEntry>) {
        let prev_index = from_index.saturating_sub(1);
        let prev_term = self.term_at(prev_index).unwrap_or(0);

        let entries = self.entries_from(from_index);
        let limited = if entries.len() > max_entries {
            entries[..max_entries].to_vec()
        } else {
            entries
        };

        (prev_index, prev_term, limited)
    }
}

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

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

    #[test]
    fn test_log_basic() {
        let log = ReplicatedLog::new();

        assert_eq!(log.last_index(), 0);
        assert_eq!(log.last_term(), 0);
        assert!(log.is_empty());

        let entry = LogEntry::new(1, 1, Command::Noop);
        log.append(entry);

        assert_eq!(log.last_index(), 1);
        assert_eq!(log.last_term(), 1);
        assert!(!log.is_empty());
    }

    #[test]
    fn test_log_append() {
        let log = ReplicatedLog::new();

        for i in 1..=5 {
            log.append(LogEntry::new(1, i, Command::Noop));
        }

        assert_eq!(log.len(), 5);
        assert_eq!(log.last_index(), 5);

        let entry = log.get(3).unwrap();
        assert_eq!(entry.index, 3);
    }

    #[test]
    fn test_log_range() {
        let log = ReplicatedLog::new();

        for i in 1..=10 {
            log.append(LogEntry::new(1, i, Command::Noop));
        }

        let range = log.get_range(3, 7);
        assert_eq!(range.len(), 5);
        assert_eq!(range[0].index, 3);
        assert_eq!(range[4].index, 7);
    }

    #[test]
    fn test_log_compact() {
        let log = ReplicatedLog::new();

        for i in 1..=10 {
            log.append(LogEntry::new(1, i, Command::Noop));
        }

        log.compact(5, 1);

        assert_eq!(log.len(), 5);
        assert!(log.get(5).is_none());
        assert!(log.get(6).is_some());
        assert_eq!(log.last_index(), 10);
    }

    #[test]
    fn test_log_up_to_date() {
        let log = ReplicatedLog::new();

        log.append(LogEntry::new(1, 1, Command::Noop));
        log.append(LogEntry::new(2, 2, Command::Noop));

        // Same log is up to date
        assert!(log.is_up_to_date(2, 2));

        // Higher term is more up to date
        assert!(log.is_up_to_date(1, 3));

        // Same term, higher index is more up to date
        assert!(log.is_up_to_date(3, 2));

        // Lower term is not up to date
        assert!(!log.is_up_to_date(10, 1));
    }

    #[test]
    fn test_append_entries() {
        let log = ReplicatedLog::new();

        // Initial entries
        log.append(LogEntry::new(1, 1, Command::Noop));
        log.append(LogEntry::new(1, 2, Command::Noop));

        // Append more entries
        let new_entries = vec![
            LogEntry::new(1, 3, Command::Noop),
            LogEntry::new(2, 4, Command::Noop),
        ];

        let success = log.append_entries(2, 1, new_entries);
        assert!(success);
        assert_eq!(log.len(), 4);
        assert_eq!(log.term_at(4), Some(2));
    }

    #[test]
    fn test_entries_for_replication() {
        let log = ReplicatedLog::new();

        for i in 1..=5 {
            log.append(LogEntry::new(1, i, Command::Noop));
        }

        let (prev_idx, prev_term, entries) = log.entries_for_replication(3, 10);
        assert_eq!(prev_idx, 2);
        assert_eq!(prev_term, 1);
        assert_eq!(entries.len(), 3);
    }
}
