//! `recover` command -- diagnose WAL state for crash recovery (--verify-only).

use serde::Serialize;

use crate::{CliError, json::render_json};

/// Run the `recover` diagnosis.
///
/// `verify_only` is a forward-compat stub: `recover` currently only inspects WAL
/// state (it never mutates the data dir), so the only accepted value today is
/// `true`. The flag reserves the surface for a future in-place repair/replay
/// mode (`verify_only = false`) without changing the CLI contract; passing
/// `false` now is rejected rather than silently no-op'd so the unimplemented
/// mode can't be mistaken for a successful recovery.
pub(crate) fn run(
    base: &std::path::Path,
    pretty: bool,
    verify_only: bool,
) -> Result<(String, i32), CliError> {
    if !verify_only {
        return Err(CliError::new(
            "only --verify-only mode is currently supported for recover",
        ));
    }

    let report = tidaldb::wal::diagnostics::diagnose_wal(base)
        .map_err(|e| CliError::new(format!("WAL diagnosis failed: {e}")))?;

    if pretty {
        Ok((format_pretty(&report), 0))
    } else {
        Ok((render_json(&RecoverOutput::from_report(&report), false)?, 0))
    }
}

fn format_pretty(report: &tidaldb::wal::diagnostics::WalDiagnosticReport) -> String {
    use std::fmt::Write;

    let mut out = String::new();

    out.push_str("WAL Diagnostic Report\n");
    out.push_str("=====================\n\n");

    out.push_str("Checkpoint:\n");
    let _ = writeln!(out, "  Sequence:  {}", report.checkpoint_seq);
    let _ = writeln!(out, "  Timestamp: {} ns", report.checkpoint_ts);
    out.push('\n');

    let _ = writeln!(out, "WAL Segments: {}", report.segment_count);
    #[allow(clippy::cast_precision_loss)]
    let mb = report.total_segment_bytes as f64 / (1024.0 * 1024.0);
    let _ = writeln!(
        out,
        "Total Segment Bytes: {} ({mb:.1} MB)",
        report.total_segment_bytes
    );
    out.push('\n');

    out.push_str("Events:\n");
    let _ = writeln!(out, "  Total in WAL:    {}", report.total_events);
    let _ = writeln!(out, "  To replay:       {}", report.replay_events);
    let _ = writeln!(out, "  Last WAL seq:    {}", report.last_wal_seq);
    let _ = writeln!(out, "  Inconsistencies: {}", report.inconsistency_count);
    out.push('\n');

    let _ = writeln!(
        out,
        "Estimated Recovery Time: {:.1}s",
        report.estimated_recovery_secs
    );

    // Session journal (`sessions.log`) is a separate append-only file from the
    // signal WAL segments; diagnose_wal inspects it independently. Surface its
    // health here so a single `recover --verify-only` glance covers both stores.
    out.push('\n');
    out.push_str("Session Journal:\n");
    let sj = &report.session_journal;
    let _ = writeln!(out, "  Present:         {}", sj.present);
    let _ = writeln!(out, "  File size:       {} bytes", sj.file_size);
    let _ = writeln!(out, "  Events:          {}", sj.event_count);
    let _ = writeln!(out, "  Corrupt records: {}", sj.corrupt_records);

    if !report.segments.is_empty() {
        out.push_str("\nSegment Inventory:\n");
        out.push_str("  first_seq   file_size  batches   events   corrupt\n");
        for seg in &report.segments {
            let _ = writeln!(
                out,
                "  {:>9}  {:>9}  {:>7}  {:>7}  {:>8}",
                seg.first_seq, seg.file_size, seg.batch_count, seg.event_count, seg.corrupt_batches,
            );
        }
    }

    out
}

/// Serializable view of a [`WalDiagnosticReport`] for the `recover` JSON path.
///
/// The engine's report type does not derive `Serialize` (it lives in the
/// `tidaldb` crate, outside this zone), so we project it into a local struct.
/// This mirrors every field of the report — including the `session_journal`
/// summary, which the previous hand-rolled builder silently dropped.
#[derive(Serialize)]
struct RecoverOutput {
    total_events: u64,
    replay_events: u64,
    last_wal_seq: u64,
    checkpoint_seq: u64,
    checkpoint_ts: u64,
    segment_count: usize,
    total_segment_bytes: u64,
    inconsistency_count: u64,
    estimated_recovery_secs: f64,
    segments: Vec<SegmentOutput>,
    session_journal: SessionJournalOutput,
}

#[derive(Serialize)]
struct SegmentOutput {
    first_seq: u64,
    file_size: u64,
    batch_count: u64,
    event_count: u64,
    corrupt_batches: u64,
}

#[derive(Serialize)]
struct SessionJournalOutput {
    present: bool,
    file_size: u64,
    event_count: u64,
    corrupt_records: u64,
}

impl RecoverOutput {
    fn from_report(report: &tidaldb::wal::diagnostics::WalDiagnosticReport) -> Self {
        let segments = report
            .segments
            .iter()
            .map(|s| SegmentOutput {
                first_seq: s.first_seq,
                file_size: s.file_size,
                batch_count: s.batch_count,
                event_count: s.event_count,
                corrupt_batches: s.corrupt_batches,
            })
            .collect();
        let sj = &report.session_journal;
        Self {
            total_events: report.total_events,
            replay_events: report.replay_events,
            last_wal_seq: report.last_wal_seq,
            checkpoint_seq: report.checkpoint_seq,
            checkpoint_ts: report.checkpoint_ts,
            segment_count: report.segment_count,
            total_segment_bytes: report.total_segment_bytes,
            inconsistency_count: report.inconsistency_count,
            estimated_recovery_secs: report.estimated_recovery_secs,
            segments,
            session_journal: SessionJournalOutput {
                present: sj.present,
                file_size: sj.file_size,
                event_count: sj.event_count,
                corrupt_records: sj.corrupt_records,
            },
        }
    }
}
