use std::path::PathBuf;

use rocksdb::{DB, IteratorMode, Options};
use rpki::storage::{
    CF_RRDP_SOURCE, CF_RRDP_SOURCE_MEMBER, CF_RRDP_URI_OWNER, RrdpSourceMemberRecord,
    RrdpSourceRecord, RrdpUriOwnerRecord, column_family_descriptors,
};

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum DumpView {
    Source,
    Members,
    Owners,
    All,
}

impl DumpView {
    fn parse(value: &str) -> Result<Self, String> {
        match value {
            "source" => Ok(Self::Source),
            "members" => Ok(Self::Members),
            "owners" => Ok(Self::Owners),
            "all" => Ok(Self::All),
            other => Err(format!(
                "invalid --view: {other} (expected one of: source, members, owners, all)"
            )),
        }
    }
}

#[derive(Debug)]
struct DumpArgs {
    db_path: PathBuf,
    view: DumpView,
}

fn usage() -> String {
    let bin = "rrdp_state_dump";
    format!(
        "\
Usage:
  {bin} --db <path> [--view <source|members|owners|all>]

Options:
  --db <path>     RocksDB directory
  --view <name>   Dump one RRDP view; default is all
  --help          Show this help
"
    )
}

fn parse_args(argv: &[String]) -> Result<DumpArgs, String> {
    if argv.iter().any(|a| a == "--help" || a == "-h") {
        return Err(usage());
    }

    let mut db_path: Option<PathBuf> = None;
    let mut view = DumpView::All;
    let mut i = 1usize;
    while i < argv.len() {
        match argv[i].as_str() {
            "--db" => {
                i += 1;
                let v = argv.get(i).ok_or("--db requires a value")?;
                db_path = Some(PathBuf::from(v));
            }
            "--view" => {
                i += 1;
                let v = argv.get(i).ok_or("--view requires a value")?;
                view = DumpView::parse(v)?;
            }
            other => return Err(format!("unknown argument: {other}\n\n{}", usage())),
        }
        i += 1;
    }

    Ok(DumpArgs {
        db_path: db_path.ok_or_else(|| format!("--db is required\n\n{}", usage()))?,
        view,
    })
}

fn open_db(path: &std::path::Path) -> Result<DB, Box<dyn std::error::Error>> {
    let mut opts = Options::default();
    opts.create_if_missing(false);
    opts.create_missing_column_families(false);
    Ok(DB::open_cf_descriptors(
        &opts,
        path,
        column_family_descriptors(),
    )?)
}

fn collect_source_records(db: &DB) -> Result<Vec<RrdpSourceRecord>, Box<dyn std::error::Error>> {
    let cf = db
        .cf_handle(CF_RRDP_SOURCE)
        .ok_or("missing column family: rrdp_source")?;
    let mut out = Vec::new();
    for res in db.iterator_cf(cf, IteratorMode::Start) {
        let (_k, v) = res?;
        let record: RrdpSourceRecord = serde_cbor::from_slice(&v)?;
        record.validate_internal()?;
        out.push(record);
    }
    out.sort_by(|a, b| a.notify_uri.cmp(&b.notify_uri));
    Ok(out)
}

fn collect_source_member_records(
    db: &DB,
) -> Result<Vec<RrdpSourceMemberRecord>, Box<dyn std::error::Error>> {
    let cf = db
        .cf_handle(CF_RRDP_SOURCE_MEMBER)
        .ok_or("missing column family: rrdp_source_member")?;
    let mut out = Vec::new();
    for res in db.iterator_cf(cf, IteratorMode::Start) {
        let (_k, v) = res?;
        let record: RrdpSourceMemberRecord = serde_cbor::from_slice(&v)?;
        record.validate_internal()?;
        out.push(record);
    }
    out.sort_by(|a, b| {
        a.notify_uri
            .cmp(&b.notify_uri)
            .then(a.rsync_uri.cmp(&b.rsync_uri))
    });
    Ok(out)
}

fn collect_uri_owner_records(
    db: &DB,
) -> Result<Vec<RrdpUriOwnerRecord>, Box<dyn std::error::Error>> {
    let cf = db
        .cf_handle(CF_RRDP_URI_OWNER)
        .ok_or("missing column family: rrdp_uri_owner")?;
    let mut out = Vec::new();
    for res in db.iterator_cf(cf, IteratorMode::Start) {
        let (_k, v) = res?;
        let record: RrdpUriOwnerRecord = serde_cbor::from_slice(&v)?;
        record.validate_internal()?;
        out.push(record);
    }
    out.sort_by(|a, b| a.rsync_uri.cmp(&b.rsync_uri));
    Ok(out)
}

fn print_source_records(entries: &[RrdpSourceRecord]) {
    println!("[source]");
    println!("notify_uri\tlast_serial\tlast_session_id\tsync_state\tlast_snapshot_uri\tlast_error");
    for record in entries {
        println!(
            "{}\t{}\t{}\t{:?}\t{}\t{}",
            record.notify_uri,
            record
                .last_serial
                .map(|v| v.to_string())
                .unwrap_or_else(|| "-".to_string()),
            record.last_session_id.as_deref().unwrap_or("-"),
            record.sync_state,
            record.last_snapshot_uri.as_deref().unwrap_or("-"),
            record.last_error.as_deref().unwrap_or("-"),
        );
    }
}

fn print_source_member_records(entries: &[RrdpSourceMemberRecord]) {
    println!("[members]");
    println!("notify_uri\trsync_uri\tpresent\thash\tsession_id\tserial");
    for record in entries {
        println!(
            "{}\t{}\t{}\t{}\t{}\t{}",
            record.notify_uri,
            record.rsync_uri,
            record.present,
            record.current_hash.as_deref().unwrap_or("-"),
            record.last_confirmed_session_id,
            record.last_confirmed_serial,
        );
    }
}

fn print_uri_owner_records(entries: &[RrdpUriOwnerRecord]) {
    println!("[owners]");
    println!("rsync_uri\tnotify_uri\towner_state\thash\tsession_id\tserial");
    for record in entries {
        println!(
            "{}\t{}\t{:?}\t{}\t{}\t{}",
            record.rsync_uri,
            record.notify_uri,
            record.owner_state,
            record.current_hash.as_deref().unwrap_or("-"),
            record.last_confirmed_session_id,
            record.last_confirmed_serial,
        );
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let argv: Vec<String> = std::env::args().collect();
    let args = parse_args(&argv).map_err(|e| -> Box<dyn std::error::Error> { e.into() })?;
    let db = open_db(&args.db_path)?;

    match args.view {
        DumpView::Source => print_source_records(&collect_source_records(&db)?),
        DumpView::Members => print_source_member_records(&collect_source_member_records(&db)?),
        DumpView::Owners => print_uri_owner_records(&collect_uri_owner_records(&db)?),
        DumpView::All => {
            print_source_records(&collect_source_records(&db)?);
            print_source_member_records(&collect_source_member_records(&db)?);
            print_uri_owner_records(&collect_uri_owner_records(&db)?);
        }
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use rpki::storage::{PackTime, RocksStore, RrdpSourceSyncState, RrdpUriOwnerState};

    #[test]
    fn parse_args_accepts_view_and_db() {
        let args = parse_args(&[
            "rrdp_state_dump".to_string(),
            "--db".to_string(),
            "db".to_string(),
            "--view".to_string(),
            "owners".to_string(),
        ])
        .expect("parse args");
        assert_eq!(args.db_path, PathBuf::from("db"));
        assert_eq!(args.view, DumpView::Owners);
    }

    #[test]
    fn parse_args_rejects_invalid_view() {
        let err = parse_args(&[
            "rrdp_state_dump".to_string(),
            "--db".to_string(),
            "db".to_string(),
            "--view".to_string(),
            "nope".to_string(),
        ])
        .unwrap_err();
        assert!(err.contains("invalid --view"), "{err}");
    }

    #[test]
    fn collect_rrdp_views_reads_current_records() {
        let dir = tempfile::tempdir().expect("tempdir");
        let store = RocksStore::open(dir.path()).expect("open store");
        store
            .put_rrdp_source_record(&RrdpSourceRecord {
                notify_uri: "https://example.test/notify.xml".to_string(),
                last_session_id: Some("session-1".to_string()),
                last_serial: Some(42),
                first_seen_at: PackTime::from_utc_offset_datetime(time::OffsetDateTime::now_utc()),
                last_seen_at: PackTime::from_utc_offset_datetime(time::OffsetDateTime::now_utc()),
                last_sync_at: None,
                sync_state: RrdpSourceSyncState::DeltaReady,
                last_snapshot_uri: Some("https://example.test/snapshot.xml".to_string()),
                last_snapshot_hash: None,
                last_error: None,
            })
            .expect("put source record");
        store
            .put_rrdp_source_member_record(&RrdpSourceMemberRecord {
                notify_uri: "https://example.test/notify.xml".to_string(),
                rsync_uri: "rsync://example.test/repo/a.roa".to_string(),
                current_hash: Some("11".repeat(32)),
                object_type: Some("roa".to_string()),
                present: true,
                last_confirmed_session_id: "session-1".to_string(),
                last_confirmed_serial: 42,
                last_changed_at: PackTime::from_utc_offset_datetime(time::OffsetDateTime::now_utc()),
            })
            .expect("put member record");
        store
            .put_rrdp_uri_owner_record(&RrdpUriOwnerRecord {
                rsync_uri: "rsync://example.test/repo/a.roa".to_string(),
                notify_uri: "https://example.test/notify.xml".to_string(),
                current_hash: Some("11".repeat(32)),
                last_confirmed_session_id: "session-1".to_string(),
                last_confirmed_serial: 42,
                last_changed_at: PackTime::from_utc_offset_datetime(time::OffsetDateTime::now_utc()),
                owner_state: RrdpUriOwnerState::Active,
            })
            .expect("put owner record");
        drop(store);

        let db = open_db(dir.path()).expect("open db");
        let sources = collect_source_records(&db).expect("source dump");
        let members = collect_source_member_records(&db).expect("members dump");
        let owners = collect_uri_owner_records(&db).expect("owners dump");

        assert_eq!(sources.len(), 1);
        assert_eq!(sources[0].sync_state, RrdpSourceSyncState::DeltaReady);
        assert_eq!(members.len(), 1);
        assert_eq!(members[0].rsync_uri, "rsync://example.test/repo/a.roa");
        assert_eq!(owners.len(), 1);
        assert_eq!(owners[0].owner_state, RrdpUriOwnerState::Active);
    }
}
