//! WAL diagnostic inspection for crash recovery analysis.
//!
//! Provides dry-run WAL inspection that reads WAL state without writing
//! any changes. Used by `tidalctl recover --verify-only` to report
//! WAL health after a crash.

use std::path::Path;

use super::checkpoint::CheckpointManager;
use super::error::WalError;
use super::format::{self, HEADER_SIZE, MAGIC};
use super::segment;

/// Diagnostic report from a dry-run WAL inspection.
#[derive(Debug)]
pub struct WalDiagnosticReport {
    /// Total number of valid signal events found in WAL segments.
    pub total_events: u64,
    /// Number of events that would be replayed (after checkpoint filtering).
    pub replay_events: u64,
    /// Last WAL sequence number seen in any segment (one past last event).
    pub last_wal_seq: u64,
    /// Checkpoint sequence number (from `checkpoint.meta`), or 0 if none.
    pub checkpoint_seq: u64,
    /// Checkpoint timestamp (nanos), or 0 if none.
    pub checkpoint_ts: u64,
    /// Number of WAL segment files.
    pub segment_count: usize,
    /// Total bytes across all segment files.
    pub total_segment_bytes: u64,
    /// Number of corrupted or truncated batches encountered.
    pub inconsistency_count: u64,
    /// Per-segment summary.
    pub segments: Vec<SegmentSummary>,
    /// Estimated recovery time in seconds (based on event count heuristic).
    pub estimated_recovery_secs: f64,
}

/// Summary of a single WAL segment file.
#[derive(Debug)]
pub struct SegmentSummary {
    /// First sequence number in this segment.
    pub first_seq: u64,
    /// File size in bytes.
    pub file_size: u64,
    /// Number of valid batches.
    pub batch_count: u64,
    /// Number of valid events.
    pub event_count: u64,
    /// Number of corrupted batches.
    pub corrupt_batches: u64,
}

/// Perform a dry-run WAL inspection and produce a diagnostic report.
///
/// Reads WAL segments and the checkpoint file but writes nothing.
/// Safe to run against a live database directory (read-only).
///
/// # Errors
///
/// Returns `WalError::Io` on filesystem failure.
#[allow(clippy::too_many_lines)]
pub fn diagnose_wal(data_dir: &Path) -> Result<WalDiagnosticReport, WalError> {
    let wal_dir = data_dir.join("wal");

    // If the wal directory doesn't exist, return an empty report.
    if !wal_dir.exists() {
        return Ok(WalDiagnosticReport {
            total_events: 0,
            replay_events: 0,
            last_wal_seq: 0,
            checkpoint_seq: 0,
            checkpoint_ts: 0,
            segment_count: 0,
            total_segment_bytes: 0,
            inconsistency_count: 0,
            segments: Vec::new(),
            estimated_recovery_secs: 0.0,
        });
    }

    // Read checkpoint (read-only).
    let checkpoint = CheckpointManager::read(&wal_dir)?;
    let (checkpoint_seq, checkpoint_ts) = checkpoint.unwrap_or((0, 0));

    // List and scan segments (read-only).
    let segment_list = segment::list_segments(&wal_dir)?;
    let segment_count = segment_list.len();
    let mut total_events = 0u64;
    let mut replay_events = 0u64;
    let mut last_wal_seq = 0u64;
    let mut inconsistency_count = 0u64;
    let mut total_segment_bytes = 0u64;
    let mut segments = Vec::with_capacity(segment_count);

    for (seg_first_seq, seg_path) in &segment_list {
        let file_size = std::fs::metadata(seg_path).map(|m| m.len()).unwrap_or(0);
        total_segment_bytes += file_size;

        let data = std::fs::read(seg_path)?;
        let mut offset = 0usize;
        let mut batch_count = 0u64;
        let mut event_count = 0u64;
        let mut corrupt_batches = 0u64;
        let mut seg_first_seq_seen: Option<u64> = None;

        while offset < data.len() {
            // Need at least HEADER_SIZE bytes for a valid batch header.
            if data.len() - offset < HEADER_SIZE {
                corrupt_batches += 1;
                inconsistency_count += 1;
                break;
            }
            // Validate magic bytes before attempting decode.
            if data[offset..offset + 4] != MAGIC {
                corrupt_batches += 1;
                inconsistency_count += 1;
                break;
            }

            // Read payload_len from the header to compute batch bounds.
            let payload_len =
                u32::from_le_bytes(data[offset + 24..offset + 28].try_into().unwrap_or([0; 4]))
                    as usize;

            let batch_end = offset + HEADER_SIZE + payload_len;
            if batch_end > data.len() {
                // Truncated batch (torn write).
                corrupt_batches += 1;
                inconsistency_count += 1;
                break;
            }

            if let Ok((header, events)) = format::decode_batch(&data[offset..batch_end]) {
                batch_count += 1;
                let n = events.len() as u64;
                event_count += n;
                total_events += n;

                if seg_first_seq_seen.is_none() {
                    seg_first_seq_seen = Some(header.first_seq);
                }

                for (i, _) in events.iter().enumerate() {
                    let event_seq = header.first_seq + i as u64;
                    // Events at or after the checkpoint must be replayed.
                    if event_seq >= checkpoint_seq {
                        replay_events += 1;
                    }
                    let candidate = event_seq + 1;
                    if candidate > last_wal_seq {
                        last_wal_seq = candidate;
                    }
                }

                offset = batch_end;
            } else {
                corrupt_batches += 1;
                inconsistency_count += 1;
                break;
            }
        }

        // Use the first_seq from the filename (segment list), falling back
        // to what we actually saw in the data.
        let reported_first_seq = seg_first_seq_seen.unwrap_or(*seg_first_seq);

        segments.push(SegmentSummary {
            first_seq: reported_first_seq,
            file_size,
            batch_count,
            event_count,
            corrupt_batches,
        });
    }

    // Estimate recovery time.
    // Heuristic: ~100K events/sec replay throughput based on benchmarks.
    // Add 5 seconds for checkpoint restore overhead.
    #[allow(clippy::cast_precision_loss)]
    let replay_secs = replay_events as f64 / 100_000.0;
    let estimated_recovery_secs = replay_secs + 5.0;

    Ok(WalDiagnosticReport {
        total_events,
        replay_events,
        last_wal_seq,
        checkpoint_seq,
        checkpoint_ts,
        segment_count,
        total_segment_bytes,
        inconsistency_count,
        segments,
        estimated_recovery_secs,
    })
}

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
    use super::*;
    use crate::replication::ShardId;
    use crate::wal::checkpoint::CheckpointManager;
    use crate::wal::format::{EventRecord, encode_batch};
    use crate::wal::segment::segment_filename;

    fn sample_event(id: u64) -> EventRecord {
        EventRecord {
            entity_id: id,
            signal_type: 1,
            weight: 1.0,
            timestamp_nanos: id * 1_000_000_000,
        }
    }

    #[test]
    fn diagnose_empty_dir() {
        let dir = tempfile::tempdir().unwrap();
        std::fs::create_dir_all(dir.path().join("wal")).unwrap();

        let report = diagnose_wal(dir.path()).unwrap();
        assert_eq!(report.total_events, 0);
        assert_eq!(report.segment_count, 0);
        assert_eq!(report.checkpoint_seq, 0);
        assert_eq!(report.inconsistency_count, 0);
    }

    #[test]
    fn diagnose_missing_wal_dir() {
        let dir = tempfile::tempdir().unwrap();
        // No wal subdirectory -- should return empty report, not error.
        let report = diagnose_wal(dir.path()).unwrap();
        assert_eq!(report.total_events, 0);
        assert_eq!(report.segment_count, 0);
    }

    #[test]
    fn diagnose_single_segment() {
        let dir = tempfile::tempdir().unwrap();
        let wal_dir = dir.path().join("wal");
        std::fs::create_dir_all(&wal_dir).unwrap();

        let events = vec![sample_event(1), sample_event(2), sample_event(3)];
        let batch = encode_batch(&events, 1, 1000).unwrap();
        let seg_name = segment_filename(ShardId::SINGLE, 1);
        std::fs::write(wal_dir.join(seg_name), &batch).unwrap();

        let report = diagnose_wal(dir.path()).unwrap();
        assert_eq!(report.total_events, 3);
        assert_eq!(report.replay_events, 3); // no checkpoint, all replayed
        assert_eq!(report.segment_count, 1);
        assert_eq!(report.inconsistency_count, 0);
        assert_eq!(report.segments[0].event_count, 3);
        assert_eq!(report.segments[0].batch_count, 1);
    }

    #[test]
    fn diagnose_with_checkpoint() {
        let dir = tempfile::tempdir().unwrap();
        let wal_dir = dir.path().join("wal");
        std::fs::create_dir_all(&wal_dir).unwrap();

        // Write checkpoint at seq=2.
        CheckpointManager::write(&wal_dir, 2, 2000).unwrap();

        // Write events at seq 1, 2, 3 (batch with first_seq=1, 3 events).
        let events = vec![sample_event(1), sample_event(2), sample_event(3)];
        let batch = encode_batch(&events, 1, 1000).unwrap();
        std::fs::write(wal_dir.join(segment_filename(ShardId::SINGLE, 1)), &batch).unwrap();

        let report = diagnose_wal(dir.path()).unwrap();
        assert_eq!(report.total_events, 3);
        // replay_events: event_seq >= checkpoint_seq=2: events at seq 2 and 3 = 2 events
        assert_eq!(report.replay_events, 2);
        assert_eq!(report.checkpoint_seq, 2);
        assert_eq!(report.checkpoint_ts, 2000);
    }

    #[test]
    fn diagnose_with_corruption() {
        let dir = tempfile::tempdir().unwrap();
        let wal_dir = dir.path().join("wal");
        std::fs::create_dir_all(&wal_dir).unwrap();

        let events = vec![sample_event(1)];
        let mut batch = encode_batch(&events, 1, 1000).unwrap();
        // Append garbage to simulate a torn write.
        batch.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);

        std::fs::write(wal_dir.join(segment_filename(ShardId::SINGLE, 1)), &batch).unwrap();

        let report = diagnose_wal(dir.path()).unwrap();
        assert_eq!(report.total_events, 1); // one valid event before corruption
        assert_eq!(report.inconsistency_count, 1); // one corrupt tail
        assert_eq!(report.segments[0].corrupt_batches, 1);
    }

    #[test]
    fn diagnose_multiple_segments() {
        let dir = tempfile::tempdir().unwrap();
        let wal_dir = dir.path().join("wal");
        std::fs::create_dir_all(&wal_dir).unwrap();

        // Segment 1: events at seq 1..3
        let events1 = vec![sample_event(1), sample_event(2), sample_event(3)];
        let batch1 = encode_batch(&events1, 1, 1000).unwrap();
        std::fs::write(wal_dir.join(segment_filename(ShardId::SINGLE, 1)), &batch1).unwrap();

        // Segment 2: events at seq 4..5
        let events2 = vec![sample_event(4), sample_event(5)];
        let batch2 = encode_batch(&events2, 4, 2000).unwrap();
        std::fs::write(wal_dir.join(segment_filename(ShardId::SINGLE, 4)), &batch2).unwrap();

        let report = diagnose_wal(dir.path()).unwrap();
        assert_eq!(report.total_events, 5);
        assert_eq!(report.replay_events, 5);
        assert_eq!(report.segment_count, 2);
        assert_eq!(report.last_wal_seq, 6); // one past last event (seq 5)
        assert_eq!(report.inconsistency_count, 0);
        assert_eq!(report.segments.len(), 2);
    }

    #[test]
    fn diagnose_estimated_recovery_time() {
        let dir = tempfile::tempdir().unwrap();
        let wal_dir = dir.path().join("wal");
        std::fs::create_dir_all(&wal_dir).unwrap();

        let events = vec![sample_event(1)];
        let batch = encode_batch(&events, 1, 1000).unwrap();
        std::fs::write(wal_dir.join(segment_filename(ShardId::SINGLE, 1)), &batch).unwrap();

        let report = diagnose_wal(dir.path()).unwrap();
        // 1 event / 100_000 + 5.0 = ~5.00001
        assert!(report.estimated_recovery_secs >= 5.0);
        assert!(report.estimated_recovery_secs < 6.0);
    }

    #[test]
    fn diagnose_empty_wal_recovery_time_is_base() {
        let dir = tempfile::tempdir().unwrap();
        // No wal directory at all.
        let report = diagnose_wal(dir.path()).unwrap();
        assert_eq!(report.estimated_recovery_secs, 0.0);
    }
}
