//! Crash recovery for WAL files.
//!
//! Provides `recover_file()` which scans a WAL file record-by-record,
//! verifying CRC32C and BLAKE3 checksums. On encountering corruption or
//! a partial record, it truncates the file to the last valid offset.
//!
//! Recovery never returns `Err` for data corruption — it logs and truncates.
//! Only I/O failures (disk error, permission denied) produce errors.

use crate::error::{QuarantineError, Result};
use crate::format::{
    compute_crc32c, FileHeader, HEADER_SIZE, MAX_RECORD_SIZE, RECORD_OVERHEAD, VERSION,
};
use byteorder::{LittleEndian, ReadBytesExt};
use std::fs::{File, OpenOptions};
use std::io::{BufReader, Read, Seek, SeekFrom};
use std::path::Path;
use std::time::{Duration, Instant};
use tracing::{info, instrument, warn};

/// Report from a recovery scan.
#[derive(Debug, Clone)]
pub struct RecoveryReport {
    /// Number of valid records found.
    pub valid_records: u64,
    /// Number of invalid/corrupt records encountered (always 0 or 1,
    /// since we stop at first corruption).
    pub invalid_records: u64,
    /// Bytes truncated from the end of the file.
    pub bytes_truncated: u64,
    /// Time spent during recovery.
    pub recovery_duration: Duration,
    /// Final valid offset (write position after recovery).
    pub final_offset: u64,
}

/// Recover a single WAL file, returning a report.
///
/// Algorithm:
/// 1. Read and validate FileHeader (must be v2)
/// 2. Sequential scan from HEADER_SIZE:
///    - Read payload_len. EOF -> clean end.
///    - Validate length (> 0, <= MAX_RECORD_SIZE). Invalid -> truncate.
///    - Read crc32c, blake3, payload. EOF -> truncate at scan position.
///    - Verify CRC32C. Mismatch -> truncate.
///    - Verify BLAKE3. Mismatch -> truncate.
///    - Advance scan position.
/// 3. If truncation needed: set_len + fsync.
/// 4. Return RecoveryReport.
#[instrument(skip_all, fields(path = %path.as_ref().display()))]
pub fn recover_file(path: impl AsRef<Path>) -> Result<RecoveryReport> {
    let path = path.as_ref();
    let start = Instant::now();

    let file = File::open(path).map_err(|e| QuarantineError::io(path, e))?;
    let file_len = file.metadata().map_err(|e| QuarantineError::io(path, e))?.len();

    // File too small for header
    if file_len < HEADER_SIZE as u64 {
        warn!(file_len, "WAL file smaller than header, truncating to 0");
        let wfile =
            OpenOptions::new().write(true).open(path).map_err(|e| QuarantineError::io(path, e))?;
        wfile.set_len(0).map_err(|e| QuarantineError::io(path, e))?;
        wfile.sync_all().map_err(|e| QuarantineError::io(path, e))?;
        return Ok(RecoveryReport {
            valid_records: 0,
            invalid_records: 0,
            bytes_truncated: file_len,
            recovery_duration: start.elapsed(),
            final_offset: 0,
        });
    }

    let mut reader = BufReader::new(file);

    // Validate header
    let header_result = FileHeader::read_from(&mut reader);
    if let Err(e) = header_result {
        warn!(error = %e, "WAL header invalid, cannot recover");
        return Err(e);
    }
    let header = header_result?;
    if header.version != VERSION {
        return Err(QuarantineError::CorruptRecord {
            offset: 0,
            reason: format!(
                "Unsupported WAL version {} (expected {}). Delete the WAL and re-ingest.",
                header.version, VERSION
            ),
        });
    }

    // Sequential scan
    let mut scan_offset = HEADER_SIZE as u64;
    let mut valid_records: u64 = 0;
    let mut needs_truncation = false;

    loop {
        if scan_offset >= file_len {
            break;
        }

        // Not enough room for even the fixed header portion of a record
        let remaining = file_len - scan_offset;
        if remaining < RECORD_OVERHEAD as u64 {
            warn!(
                offset = scan_offset,
                remaining_bytes = remaining,
                "Partial record header at end of file"
            );
            needs_truncation = true;
            break;
        }

        reader.seek(SeekFrom::Start(scan_offset)).map_err(|e| QuarantineError::io(path, e))?;

        // Read payload_len
        let payload_len = match reader.read_u32::<LittleEndian>() {
            Ok(len) => len,
            Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
            Err(e) => return Err(QuarantineError::io(path, e)),
        };

        // Validate length
        if payload_len == 0 || payload_len as usize > MAX_RECORD_SIZE {
            warn!(offset = scan_offset, payload_len, "Invalid record length, truncating");
            needs_truncation = true;
            break;
        }

        // Check if enough bytes remain for the full record
        let record_size = RECORD_OVERHEAD as u64 + payload_len as u64;
        if scan_offset + record_size > file_len {
            warn!(
                offset = scan_offset,
                expected_size = record_size,
                file_len,
                "Truncated record at end of file"
            );
            needs_truncation = true;
            break;
        }

        // Read CRC32C
        let stored_crc = match reader.read_u32::<LittleEndian>() {
            Ok(crc) => crc,
            Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
                needs_truncation = true;
                break;
            }
            Err(e) => return Err(QuarantineError::io(path, e)),
        };

        // Read BLAKE3
        let mut blake3_hash = [0u8; 32];
        if let Err(e) = reader.read_exact(&mut blake3_hash) {
            if e.kind() == std::io::ErrorKind::UnexpectedEof {
                needs_truncation = true;
                break;
            }
            return Err(QuarantineError::io(path, e));
        }

        // Read payload
        let mut payload = vec![0u8; payload_len as usize];
        if let Err(e) = reader.read_exact(&mut payload) {
            if e.kind() == std::io::ErrorKind::UnexpectedEof {
                needs_truncation = true;
                break;
            }
            return Err(QuarantineError::io(path, e));
        }

        // Verify CRC32C
        let len_bytes = payload_len.to_le_bytes();
        let computed_crc = compute_crc32c(&len_bytes, &blake3_hash, &payload);
        if stored_crc != computed_crc {
            warn!(
                offset = scan_offset,
                expected = stored_crc,
                actual = computed_crc,
                "CRC32C mismatch, truncating"
            );
            needs_truncation = true;
            break;
        }

        // Verify BLAKE3
        let computed_blake3: [u8; 32] = blake3::hash(&payload).into();
        if blake3_hash != computed_blake3 {
            warn!(offset = scan_offset, "BLAKE3 mismatch, truncating");
            needs_truncation = true;
            break;
        }

        // Record is valid
        scan_offset += record_size;
        valid_records += 1;
    }

    // Truncate if needed
    let bytes_truncated = if needs_truncation && scan_offset < file_len {
        let truncated = file_len - scan_offset;
        let wfile =
            OpenOptions::new().write(true).open(path).map_err(|e| QuarantineError::io(path, e))?;
        wfile.set_len(scan_offset).map_err(|e| QuarantineError::io(path, e))?;
        wfile.sync_all().map_err(|e| QuarantineError::io(path, e))?;
        if let Some(parent) = path.parent() {
            let _ = crate::durability::sync_directory(parent);
        }
        info!(truncated_bytes = truncated, final_offset = scan_offset, "Truncated corrupt tail");
        truncated
    } else {
        0
    };

    let report = RecoveryReport {
        valid_records,
        invalid_records: u64::from(needs_truncation),
        bytes_truncated,
        recovery_duration: start.elapsed(),
        final_offset: scan_offset,
    };

    info!(
        valid_records = report.valid_records,
        bytes_truncated = report.bytes_truncated,
        final_offset = report.final_offset,
        "Recovery complete"
    );

    Ok(report)
}

#[cfg(test)]
mod tests;
