//! tidalctl -- command-line inspector for embedded tidalDB instances.
//!
//! Reports WAL state, checkpoint status, and directory layout for a
//! tidalDB data directory without opening or locking any files.
//!
//! Usage:
//!   tidalctl <command> --path <dir> [--pretty]
//!
//! Commands:
//!   status       Report WAL state, checkpoint, and directory layout
//!   paths        Report resolved directory paths and existence
//!   recover      Diagnose WAL state for crash recovery (--verify-only)
//!   diagnostics  Print health summary (human-readable or JSON)

#![forbid(unsafe_code)]

use std::path::PathBuf;
use std::process;

fn main() {
    let args: Vec<String> = std::env::args().collect();

    match run(&args) {
        Ok((output, code)) => {
            println!("{output}");
            if code != 0 {
                process::exit(code);
            }
        }
        Err(e) => {
            eprintln!("{}", e.to_json());
            process::exit(1);
        }
    }
}

// ---------------------------------------------------------------------------
// CLI parsing
// ---------------------------------------------------------------------------

struct CliArgs {
    command: Command,
    path: PathBuf,
    pretty: bool,
    verify_only: bool,
}

enum Command {
    Status,
    Paths,
    Recover,
    Diagnostics,
}

struct CliError {
    message: String,
}

impl CliError {
    fn new(message: impl Into<String>) -> Self {
        Self {
            message: message.into(),
        }
    }

    fn to_json(&self) -> String {
        // Hand-rolled JSON to avoid serde in the error path.
        let escaped = self.message.replace('\\', "\\\\").replace('"', "\\\"");
        format!(r#"{{"error":"{escaped}"}}"#)
    }
}

fn parse_args(args: &[String]) -> Result<CliArgs, CliError> {
    if args.len() < 2 {
        return Err(CliError::new(usage()));
    }

    let command = match args[1].as_str() {
        "status" => Command::Status,
        "paths" => Command::Paths,
        "recover" => Command::Recover,
        "diagnostics" => Command::Diagnostics,
        "--help" | "-h" | "help" => return Err(CliError::new(usage())),
        other => {
            return Err(CliError::new(format!(
                "unknown command: '{other}'\n{}",
                usage()
            )));
        }
    };

    let mut path: Option<PathBuf> = None;
    let mut pretty = false;
    let mut verify_only = false;
    let mut i = 2;

    while i < args.len() {
        match args[i].as_str() {
            "--path" => {
                i += 1;
                if i >= args.len() {
                    return Err(CliError::new("--path requires a value"));
                }
                path = Some(PathBuf::from(&args[i]));
            }
            "--pretty" => {
                pretty = true;
            }
            "--verify-only" => {
                verify_only = true;
            }
            other => {
                return Err(CliError::new(format!("unknown flag: '{other}'")));
            }
        }
        i += 1;
    }

    let path = path.ok_or_else(|| CliError::new("--path is required"))?;

    Ok(CliArgs {
        command,
        path,
        pretty,
        verify_only,
    })
}

fn usage() -> String {
    "Usage: tidalctl <command> --path <dir> [--pretty]\n\n\
     Commands:\n  \
       status       Report WAL state, checkpoint, and directory layout\n  \
       paths        Report resolved directory paths and existence\n  \
       recover      Diagnose WAL state for crash recovery (--verify-only)\n  \
       diagnostics  Print health summary (human-readable or JSON)"
        .to_string()
}

// ---------------------------------------------------------------------------
// Command dispatch
// ---------------------------------------------------------------------------

fn run(args: &[String]) -> Result<(String, i32), CliError> {
    let cli = parse_args(args)?;

    match cli.command {
        Command::Status => cmd_status(&cli.path, cli.pretty),
        Command::Paths => cmd_paths(&cli.path, cli.pretty),
        Command::Recover => cmd_recover(&cli.path, cli.pretty, cli.verify_only),
        Command::Diagnostics => cmd_diagnostics(&cli.path, cli.pretty),
    }
}

// ---------------------------------------------------------------------------
// status command
// ---------------------------------------------------------------------------

fn cmd_status(base: &std::path::Path, pretty: bool) -> Result<(String, i32), CliError> {
    let paths = tidaldb::Paths::new(base);
    let wal_dir = paths.wal_dir();

    let version = env!("CARGO_PKG_VERSION");
    let build_hash = tidaldb::BUILD_HASH;

    // Determine WAL state
    let wal_result = gather_wal_state(&wal_dir);

    let (status, wal_json) = match wal_result {
        Ok(wal) if wal.segments > 0 => ("ok", wal.to_json()),
        Ok(wal) => ("empty", wal.to_json()),
        Err(e) => ("error", format!(r#"{{"error":"{}"}}"#, json_escape(&e))),
    };

    let dirs_json = dirs_json(&paths);

    let json = format!(
        r#"{{"version":"{version}","build_hash":"{build_hash}","status":"{status}","wal":{wal_json},"dirs":{dirs_json}}}"#
    );

    let output = format_json(&json, pretty)?;
    Ok((output, 0))
}

struct WalState {
    segments: usize,
    first_seq: u64,
    last_segment_seq: u64,
    checkpoint_seq: u64,
    checkpoint_ts: u64,
    wal_dir_bytes: u64,
}

impl WalState {
    fn to_json(&self) -> String {
        format!(
            r#"{{"segments":{},"first_seq":{},"last_segment_seq":{},"checkpoint_seq":{},"checkpoint_ts":{},"wal_dir_bytes":{}}}"#,
            self.segments,
            self.first_seq,
            self.last_segment_seq,
            self.checkpoint_seq,
            self.checkpoint_ts,
            self.wal_dir_bytes,
        )
    }
}

fn gather_wal_state(wal_dir: &std::path::Path) -> Result<WalState, String> {
    if !wal_dir.exists() {
        return Ok(WalState {
            segments: 0,
            first_seq: 0,
            last_segment_seq: 0,
            checkpoint_seq: 0,
            checkpoint_ts: 0,
            wal_dir_bytes: 0,
        });
    }

    let segments = tidaldb::wal::segment::list_segments(wal_dir).map_err(|e| format!("{e}"))?;

    let first_seq = segments.first().map_or(0, |(seq, _)| *seq);
    let last_segment_seq = segments.last().map_or(0, |(seq, _)| *seq);

    let (checkpoint_seq, checkpoint_ts) =
        match tidaldb::wal::checkpoint::CheckpointManager::read(wal_dir) {
            Ok(Some((seq, ts))) => (seq, ts),
            Ok(None) => (0, 0),
            Err(e) => return Err(format!("checkpoint read error: {e}")),
        };

    let wal_dir_bytes = dir_size(wal_dir).map_err(|e| format!("{e}"))?;

    Ok(WalState {
        segments: segments.len(),
        first_seq,
        last_segment_seq,
        checkpoint_seq,
        checkpoint_ts,
        wal_dir_bytes,
    })
}

fn dir_size(dir: &std::path::Path) -> Result<u64, std::io::Error> {
    let mut total = 0u64;
    for entry in std::fs::read_dir(dir)? {
        let entry = entry?;
        let meta = entry.metadata()?;
        if meta.is_file() {
            total += meta.len();
        }
    }
    Ok(total)
}

// ---------------------------------------------------------------------------
// paths command
// ---------------------------------------------------------------------------

fn cmd_paths(base: &std::path::Path, pretty: bool) -> Result<(String, i32), CliError> {
    let paths = tidaldb::Paths::new(base);

    let base_str = json_escape(&paths.base().display().to_string());
    let wal_str = json_escape(&paths.wal_dir().display().to_string());
    let items_str = json_escape(&paths.items_dir().display().to_string());
    let users_str = json_escape(&paths.users_dir().display().to_string());
    let creators_str = json_escape(&paths.creators_dir().display().to_string());
    let cache_str = json_escape(&paths.cache_dir().display().to_string());

    let json = format!(
        r#"{{"base":"{base_str}","wal":"{wal_str}","items":"{items_str}","users":"{users_str}","creators":"{creators_str}","cache":"{cache_str}","exists":{{"base":{},"wal":{},"items":{},"users":{},"creators":{},"cache":{}}}}}"#,
        paths.base().exists(),
        paths.wal_dir().exists(),
        paths.items_dir().exists(),
        paths.users_dir().exists(),
        paths.creators_dir().exists(),
        paths.cache_dir().exists(),
    );

    let output = format_json(&json, pretty)?;
    Ok((output, 0))
}

// ---------------------------------------------------------------------------
// recover command
// ---------------------------------------------------------------------------

fn cmd_recover(
    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_recover_pretty(&report), 0))
    } else {
        Ok((format_recover_json(&report), 0))
    }
}

fn format_recover_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
    );

    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
}

fn format_recover_json(report: &tidaldb::wal::diagnostics::WalDiagnosticReport) -> String {
    // Build segments array.
    let seg_entries: Vec<String> = report
        .segments
        .iter()
        .map(|s| {
            format!(
                r#"{{"first_seq":{},"file_size":{},"batch_count":{},"event_count":{},"corrupt_batches":{}}}"#,
                s.first_seq, s.file_size, s.batch_count, s.event_count, s.corrupt_batches,
            )
        })
        .collect();
    let segments_json = format!("[{}]", seg_entries.join(","));

    format!(
        r#"{{"total_events":{},"replay_events":{},"last_wal_seq":{},"checkpoint_seq":{},"checkpoint_ts":{},"segment_count":{},"total_segment_bytes":{},"inconsistency_count":{},"estimated_recovery_secs":{:.1},"segments":{segments_json}}}"#,
        report.total_events,
        report.replay_events,
        report.last_wal_seq,
        report.checkpoint_seq,
        report.checkpoint_ts,
        report.segment_count,
        report.total_segment_bytes,
        report.inconsistency_count,
        report.estimated_recovery_secs,
    )
}

// ---------------------------------------------------------------------------
// diagnostics command
// ---------------------------------------------------------------------------

fn cmd_diagnostics(base: &std::path::Path, pretty: bool) -> Result<(String, i32), CliError> {
    // Exit code 1 if data dir doesn't exist.
    if !base.exists() {
        return Err(CliError::new("data directory does not exist"));
    }

    let version = env!("CARGO_PKG_VERSION");
    let build_hash = tidaldb::BUILD_HASH;

    // WAL stats.
    let paths = tidaldb::Paths::new(base);
    let wal_dir = paths.wal_dir();
    let mut exit_code = 0;

    let wal = gather_wal_state(&wal_dir).unwrap_or_else(|_| {
        exit_code = 2;
        WalState {
            segments: 0,
            first_seq: 0,
            last_segment_seq: 0,
            checkpoint_seq: 0,
            checkpoint_ts: 0,
            wal_dir_bytes: 0,
        }
    });

    // Compute checkpoint age in seconds.
    let checkpoint_age_secs: Option<u64> = if wal.checkpoint_ts > 0 {
        let now_ns = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_nanos() as u64;
        Some(now_ns.saturating_sub(wal.checkpoint_ts) / 1_000_000_000)
    } else {
        None
    };

    // Signal entry estimate: use last_segment_seq as a rough proxy.
    let signal_estimated_entries: u64 = wal.last_segment_seq;

    // Tantivy text index stats.
    let text_index_dir = base.join("text_index");
    let (tantivy_segments, tantivy_indexed_docs) =
        tidaldb::text::read_stats_from_dir(&text_index_dir).unwrap_or((0, 0));

    // Creator text index stats.
    let creator_text_index_dir = base.join("creator_text_index");
    let (creator_tantivy_segments, creator_tantivy_docs) =
        tidaldb::text::read_stats_from_dir(&creator_text_index_dir).unwrap_or((0, 0));

    // USearch: in-memory only (no disk directory), report 0.
    let usearch_directory_bytes: u64 = 0;

    let checkpoint_age = checkpoint_age_secs.unwrap_or(0);

    if pretty {
        let output = format_diagnostics_pretty(
            version,
            build_hash,
            base,
            &wal,
            checkpoint_age_secs,
            signal_estimated_entries,
            tantivy_segments,
            tantivy_indexed_docs,
            creator_tantivy_segments,
            creator_tantivy_docs,
        );
        Ok((output, exit_code))
    } else {
        let json = format!(
            r#"{{"version":"{version}","build_hash":"{build_hash}","wal_segments":{segs},"wal_total_bytes":{wal_bytes},"wal_lag_bytes":{lag},"checkpoint_age_seconds":{age},"checkpoint_wal_sequence":{cp_seq},"signal_estimated_entries":{sig_est},"tantivy_segments":{t_segs},"tantivy_indexed_docs":{t_docs},"creator_tantivy_segments":{ct_segs},"creator_tantivy_docs":{ct_docs},"usearch_directory_bytes":{u_bytes},"sessions_active":0,"sessions_closed_total":0,"sessions_auto_closed_total":0,"degradation_level":0,"collection_count":0,"cohort_count":0}}"#,
            segs = wal.segments,
            wal_bytes = wal.wal_dir_bytes,
            lag = wal.wal_dir_bytes,
            age = checkpoint_age,
            cp_seq = wal.checkpoint_seq,
            sig_est = signal_estimated_entries,
            t_segs = tantivy_segments,
            t_docs = tantivy_indexed_docs,
            ct_segs = creator_tantivy_segments,
            ct_docs = creator_tantivy_docs,
            u_bytes = usearch_directory_bytes,
        );

        let formatted = format_json(&json, false)?;
        Ok((formatted, exit_code))
    }
}

#[allow(clippy::too_many_arguments)]
fn format_diagnostics_pretty(
    version: &str,
    build_hash: &str,
    base: &std::path::Path,
    wal: &WalState,
    checkpoint_age_secs: Option<u64>,
    signal_estimated_entries: u64,
    tantivy_segments: usize,
    tantivy_indexed_docs: u64,
    creator_tantivy_segments: usize,
    creator_tantivy_docs: u64,
) -> String {
    use std::fmt::Write;

    let mut out = String::new();

    out.push_str("tidalDB Diagnostics\n");
    out.push_str("===================\n");
    let _ = writeln!(out, "Version:        {version} (build: {build_hash})");
    let _ = writeln!(out, "Data dir:       {}", base.display());
    out.push_str("Storage mode:   durable\n");
    out.push('\n');

    out.push_str("WAL\n");
    out.push_str("---\n");
    let _ = writeln!(out, "Segments:       {}", wal.segments);
    #[allow(clippy::cast_precision_loss)]
    let wal_mb = wal.wal_dir_bytes as f64 / 1_048_576.0;
    let _ = writeln!(out, "Total size:     {wal_mb:.1} MB");
    let _ = writeln!(out, "First seq:      {}", wal.first_seq);
    let _ = writeln!(out, "Last seq:       {}", wal.last_segment_seq);
    out.push('\n');

    out.push_str("Checkpoint\n");
    out.push_str("----------\n");
    if let Some(age) = checkpoint_age_secs {
        // Format checkpoint timestamp as seconds since Unix epoch.
        let ts_secs = wal.checkpoint_ts / 1_000_000_000;
        let _ = writeln!(out, "Last checkpoint: {ts_secs}s Unix ({age}s ago)");
    } else {
        out.push_str("Last checkpoint: none\n");
    }
    let _ = writeln!(out, "WAL sequence:    {}", wal.checkpoint_seq);
    out.push('\n');

    out.push_str("Signal Ledger\n");
    out.push_str("-------------\n");
    let _ = writeln!(out, "Estimated entries: ~{signal_estimated_entries}");
    out.push('\n');

    out.push_str("Text Index (Tantivy)\n");
    out.push_str("--------------------\n");
    let _ = writeln!(out, "Segments:       {tantivy_segments}");
    let _ = writeln!(out, "Indexed docs:   {tantivy_indexed_docs}");
    out.push('\n');

    out.push_str("Creator Text Index (Tantivy)\n");
    out.push_str("----------------------------\n");
    let _ = writeln!(out, "Segments:       {creator_tantivy_segments}");
    let _ = writeln!(out, "Indexed docs:   {creator_tantivy_docs}");
    out.push('\n');

    out.push_str("Vector Index (USearch)\n");
    out.push_str("---------------------\n");
    out.push_str("Directory size: not available\n");
    out.push('\n');

    out.push_str("Sessions\n");
    out.push_str("--------\n");
    out.push_str("Active:         0\n");
    out.push_str("Closed (total): 0\n");
    out.push_str("Auto-closed:    0\n");
    out.push('\n');

    out.push_str("Degradation\n");
    out.push_str("-----------\n");
    out.push_str("Level:          0 (healthy)\n");
    out.push('\n');

    out.push_str("Collections:    0\n");
    out.push_str("Cohorts:        0\n");

    out
}

// ---------------------------------------------------------------------------
// JSON helpers
// ---------------------------------------------------------------------------

fn dirs_json(paths: &tidaldb::Paths) -> String {
    let base_str = json_escape(&paths.base().display().to_string());
    let wal_str = json_escape(&paths.wal_dir().display().to_string());
    let items_str = json_escape(&paths.items_dir().display().to_string());
    let users_str = json_escape(&paths.users_dir().display().to_string());
    let creators_str = json_escape(&paths.creators_dir().display().to_string());
    let cache_str = json_escape(&paths.cache_dir().display().to_string());

    format!(
        r#"{{"base":"{base_str}","wal":"{wal_str}","items":"{items_str}","users":"{users_str}","creators":"{creators_str}","cache":"{cache_str}"}}"#
    )
}

fn json_escape(s: &str) -> String {
    s.replace('\\', "\\\\").replace('"', "\\\"")
}

/// Format a compact JSON string. When `pretty` is true, use `serde_json`
/// to re-parse and pretty-print.
fn format_json(json: &str, pretty: bool) -> Result<String, CliError> {
    if pretty {
        let value: serde_json::Value = serde_json::from_str(json)
            .map_err(|e| CliError::new(format!("internal json error: {e}")))?;
        serde_json::to_string_pretty(&value)
            .map_err(|e| CliError::new(format!("internal json error: {e}")))
    } else {
        Ok(json.to_string())
    }
}
