use rpki::bundle::{
    BaseBundleStateMetadataV2, BundleManifestEntryV2, BundleManifestV2, DeltaSequenceMetadataV2,
    DeltaStepMetadataV2, RecordingHttpFetcher, RecordingRsyncFetcher, RirBundleMetadataV2,
    build_vap_compare_rows, build_vrp_compare_rows, sha256_hex, write_current_replay_state_locks,
    write_json, write_live_base_replay_bundle_inputs, write_live_delta_replay_step_inputs,
    write_vap_csv, write_vrp_csv,
};
use rpki::ccr::{
    CcrVerifySummary, build_ccr_from_run, decode_content_info, verify_content_info, write_ccr_file,
};
use rpki::fetch::http::{BlockingHttpFetcher, HttpFetcherConfig};
use rpki::fetch::rsync_system::{SystemRsyncConfig, SystemRsyncFetcher};
use rpki::policy::Policy;
use rpki::storage::RocksStore;
use rpki::sync::rrdp::Fetcher;
use rpki::validation::run_tree_from_tal::{
    run_tree_from_tal_and_ta_der_payload_delta_replay_step_serial_audit,
    run_tree_from_tal_and_ta_der_payload_replay_serial_audit,
    run_tree_from_tal_and_ta_der_serial_audit,
};
use rpki::validation::tree::TreeRunConfig;
use std::fs;
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
use time::format_description::well_known::Rfc3339;

#[derive(Debug, Default, PartialEq, Eq)]
struct Args {
    rir: Option<String>,
    out_dir: Option<PathBuf>,
    tal_path: Option<PathBuf>,
    ta_path: Option<PathBuf>,
    base_validation_time: Option<time::OffsetDateTime>,
    delta_count: usize,
    delta_interval_secs: u64,
    http_timeout_secs: u64,
    rsync_timeout_secs: u64,
    rsync_mirror_root: Option<PathBuf>,
    max_depth: Option<usize>,
    max_instances: Option<usize>,
    trust_anchor: Option<String>,
    keep_db: bool,
    capture_inputs_only: bool,
}

fn usage() -> &'static str {
    "Usage: replay_bundle_capture_sequence --rir <name> --out-dir <path> --tal-path <path> --ta-path <path> [--base-validation-time <rfc3339>] [--delta-count <n>] [--delta-interval-secs <n>] [--http-timeout-secs <n>] [--rsync-timeout-secs <n>] [--rsync-mirror-root <path>] [--max-depth <n>] [--max-instances <n>] [--trust-anchor <name>] [--keep-db] [--capture-inputs-only]"
}

fn parse_args(argv: &[String]) -> Result<Args, String> {
    let mut args = Args {
        delta_count: 5,
        delta_interval_secs: 600,
        http_timeout_secs: 20,
        rsync_timeout_secs: 60,
        ..Args::default()
    };
    let mut i = 1usize;
    while i < argv.len() {
        match argv[i].as_str() {
            "--help" | "-h" => return Err(usage().to_string()),
            "--rir" => {
                i += 1;
                args.rir = Some(argv.get(i).ok_or("--rir requires a value")?.clone());
            }
            "--out-dir" => {
                i += 1;
                args.out_dir = Some(PathBuf::from(argv.get(i).ok_or("--out-dir requires a value")?));
            }
            "--tal-path" => {
                i += 1;
                args.tal_path = Some(PathBuf::from(argv.get(i).ok_or("--tal-path requires a value")?));
            }
            "--ta-path" => {
                i += 1;
                args.ta_path = Some(PathBuf::from(argv.get(i).ok_or("--ta-path requires a value")?));
            }
            "--base-validation-time" => {
                i += 1;
                let value = argv.get(i).ok_or("--base-validation-time requires a value")?;
                args.base_validation_time = Some(
                    time::OffsetDateTime::parse(value, &Rfc3339)
                        .map_err(|e| format!("invalid --base-validation-time: {e}"))?,
                );
            }
            "--delta-count" => {
                i += 1;
                args.delta_count = argv
                    .get(i)
                    .ok_or("--delta-count requires a value")?
                    .parse()
                    .map_err(|e| format!("invalid --delta-count: {e}"))?;
            }
            "--delta-interval-secs" => {
                i += 1;
                args.delta_interval_secs = argv
                    .get(i)
                    .ok_or("--delta-interval-secs requires a value")?
                    .parse()
                    .map_err(|e| format!("invalid --delta-interval-secs: {e}"))?;
            }
            "--http-timeout-secs" => {
                i += 1;
                args.http_timeout_secs = argv
                    .get(i)
                    .ok_or("--http-timeout-secs requires a value")?
                    .parse()
                    .map_err(|e| format!("invalid --http-timeout-secs: {e}"))?;
            }
            "--rsync-timeout-secs" => {
                i += 1;
                args.rsync_timeout_secs = argv
                    .get(i)
                    .ok_or("--rsync-timeout-secs requires a value")?
                    .parse()
                    .map_err(|e| format!("invalid --rsync-timeout-secs: {e}"))?;
            }
            "--rsync-mirror-root" => {
                i += 1;
                args.rsync_mirror_root =
                    Some(PathBuf::from(argv.get(i).ok_or("--rsync-mirror-root requires a value")?));
            }
            "--max-depth" => {
                i += 1;
                args.max_depth = Some(
                    argv.get(i)
                        .ok_or("--max-depth requires a value")?
                        .parse()
                        .map_err(|e| format!("invalid --max-depth: {e}"))?,
                );
            }
            "--max-instances" => {
                i += 1;
                args.max_instances = Some(
                    argv.get(i)
                        .ok_or("--max-instances requires a value")?
                        .parse()
                        .map_err(|e| format!("invalid --max-instances: {e}"))?,
                );
            }
            "--trust-anchor" => {
                i += 1;
                args.trust_anchor = Some(argv.get(i).ok_or("--trust-anchor requires a value")?.clone());
            }
            "--keep-db" => args.keep_db = true,
            "--capture-inputs-only" => args.capture_inputs_only = true,
            other => return Err(format!("unknown argument: {other}\n{}", usage())),
        }
        i += 1;
    }

    if args.rir.is_none() {
        return Err(format!("--rir is required\n{}", usage()));
    }
    if args.out_dir.is_none() {
        return Err(format!("--out-dir is required\n{}", usage()));
    }
    if args.tal_path.is_none() {
        return Err(format!("--tal-path is required\n{}", usage()));
    }
    if args.ta_path.is_none() {
        return Err(format!("--ta-path is required\n{}", usage()));
    }
    Ok(args)
}

fn write_v2_top_readme(path: &Path, rir: &str, delta_count: usize, delta_interval_secs: u64) -> Result<(), String> {
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)
            .map_err(|e| format!("create parent failed: {}: {e}", parent.display()))?;
    }
    fs::write(
        path,
        format!(
            "# Ours Multi-Delta Replay Bundle\n\n- RIR: `{rir}`\n- Schema: `20260401-v2`\n- Configured delta steps: `{delta_count}`\n- Configured interval seconds: `{delta_interval_secs}`\n"
        ),
    )
    .map_err(|e| format!("write readme failed: {}: {e}", path.display()))
}

fn write_v2_rir_readme(
    path: &Path,
    rir: &str,
    base_validation_time: &str,
    delta_count: usize,
    delta_interval_secs: u64,
) -> Result<(), String> {
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)
            .map_err(|e| format!("create parent failed: {}: {e}", parent.display()))?;
    }
    fs::write(
        path,
        format!(
            "# {rir} multi-delta live replay bundle\n\n- `base-locks.json.validationTime` = `{base_validation_time}`\n- `delta-steps/` contains `{delta_count}` ordered target steps\n- configured interval seconds = `{delta_interval_secs}`\n"
        ),
    )
    .map_err(|e| format!("write rir readme failed: {}: {e}", path.display()))
}

fn ensure_recorded_target_snapshots_for_locks(
    store: &RocksStore,
    previous_locks_path: &Path,
    http: &RecordingHttpFetcher<BlockingHttpFetcher>,
) -> Result<(), String> {
    let previous_locks: serde_json::Value = serde_json::from_slice(
        &fs::read(previous_locks_path)
            .map_err(|e| format!("read previous locks failed: {}: {e}", previous_locks_path.display()))?,
    )
    .map_err(|e| format!("parse previous locks failed: {}: {e}", previous_locks_path.display()))?;
    let previous_rrdp = previous_locks
        .get("rrdp")
        .and_then(|v| v.as_object())
        .cloned()
        .unwrap_or_default();

    for (notify_uri, base_lock) in previous_rrdp {
        let Some(base_transport) = base_lock.get("transport").and_then(|v| v.as_str()) else {
            continue;
        };
        if base_transport != "rrdp" {
            continue;
        }
        let Some(base_session) = base_lock.get("session").and_then(|v| v.as_str()) else {
            continue;
        };
        let Some(base_serial) = base_lock.get("serial").and_then(|v| v.as_u64()) else {
            continue;
        };
        let Some(record) = store
            .get_rrdp_source_record(&notify_uri)
            .map_err(|e| format!("read rrdp source record failed for {notify_uri}: {e}"))?
        else {
            continue;
        };
        let Some(target_session) = record.last_session_id.as_deref() else {
            continue;
        };
        let Some(target_serial) = record.last_serial else {
            continue;
        };
        if target_session != base_session || target_serial <= base_serial {
            continue;
        }
        let Some(snapshot_uri) = record.last_snapshot_uri.as_deref() else {
            continue;
        };
        if step_http_has_snapshot(http, snapshot_uri) {
            continue;
        }
        if let Err(err) = http.fetch(snapshot_uri) {
            eprintln!(
                "[sequence] warning: fetch target snapshot failed notify_uri={} snapshot_uri={} err={}",
                notify_uri,
                snapshot_uri,
                err
            );
        }
    }
    Ok(())
}

fn step_http_has_snapshot(
    http: &RecordingHttpFetcher<BlockingHttpFetcher>,
    snapshot_uri: &str,
) -> bool {
    http.snapshot_responses().contains_key(snapshot_uri)
}

fn run(args: Args) -> Result<PathBuf, String> {
    let rir = args.rir.as_ref().unwrap();
    let rir_normalized = rir.to_ascii_lowercase();
    let trust_anchor = args
        .trust_anchor
        .clone()
        .unwrap_or_else(|| rir_normalized.clone());
    let out_root = args.out_dir.as_ref().unwrap();
    if out_root.exists() {
        fs::remove_dir_all(out_root)
            .map_err(|e| format!("remove old out dir failed: {}: {e}", out_root.display()))?;
    }
    let rir_dir = out_root.join(&rir_normalized);
    let delta_steps_root = rir_dir.join("delta-steps");
    fs::create_dir_all(&delta_steps_root)
        .map_err(|e| format!("create delta steps dir failed: {}: {e}", delta_steps_root.display()))?;

    let tal_bytes = fs::read(args.tal_path.as_ref().unwrap())
        .map_err(|e| format!("read tal failed: {e}"))?;
    let ta_bytes = fs::read(args.ta_path.as_ref().unwrap())
        .map_err(|e| format!("read ta failed: {e}"))?;
    fs::write(rir_dir.join("tal.tal"), &tal_bytes).map_err(|e| format!("write tal failed: {e}"))?;
    fs::write(rir_dir.join("ta.cer"), &ta_bytes).map_err(|e| format!("write ta failed: {e}"))?;

    let base_validation_time = args.base_validation_time.unwrap_or_else(time::OffsetDateTime::now_utc);

    let work_db_dir = out_root.join(".tmp").join(format!("{rir}-sequence-work-db"));
    let base_self_replay_dir = out_root.join(".tmp").join(format!("{rir}-sequence-base-self-replay-db"));
    let _ = fs::remove_dir_all(&work_db_dir);
    let _ = fs::remove_dir_all(&base_self_replay_dir);
    if let Some(parent) = work_db_dir.parent() {
        fs::create_dir_all(parent)
            .map_err(|e| format!("create tmp dir failed: {}: {e}", parent.display()))?;
    }

    let store = RocksStore::open(&work_db_dir).map_err(|e| format!("open work db failed: {e}"))?;
    let base_http = RecordingHttpFetcher::new(
        BlockingHttpFetcher::new(HttpFetcherConfig {
            timeout: Duration::from_secs(args.http_timeout_secs),
            ..HttpFetcherConfig::default()
        })
        .map_err(|e| format!("create base http fetcher failed: {e}"))?,
    );
    let base_rsync_inner = SystemRsyncFetcher::new(SystemRsyncConfig {
        timeout: Duration::from_secs(args.rsync_timeout_secs),
        mirror_root: args.rsync_mirror_root.clone(),
        ..SystemRsyncConfig::default()
    });
    let base_rsync = if args.capture_inputs_only {
        RecordingRsyncFetcher::new_without_objects(base_rsync_inner)
    } else {
        RecordingRsyncFetcher::new(base_rsync_inner)
    };
    eprintln!("[sequence] base live run start rir={rir_normalized}");
    let started = Instant::now();
    let base_out = run_tree_from_tal_and_ta_der_serial_audit(
        &store,
        &Policy::default(),
        &tal_bytes,
        &ta_bytes,
        None,
        &base_http,
        &base_rsync,
        base_validation_time,
        &TreeRunConfig {
            max_depth: args.max_depth,
            max_instances: args.max_instances,
        },
    )
    .map_err(|e| format!("live base run failed: {e}"))?;
    let base_duration = started.elapsed();
    eprintln!(
        "[sequence] base live run done rir={} duration_s={:.3}",
        rir_normalized,
        base_duration.as_secs_f64()
    );

    eprintln!("[sequence] base input materialization start rir={rir_normalized}");
    let base_capture = write_live_base_replay_bundle_inputs(
        &rir_dir,
        &rir_normalized,
        base_validation_time,
        &base_out.publication_points,
        &store,
        &base_http.snapshot_responses(),
        &base_rsync.snapshot_fetches(),
    )?;
    eprintln!(
        "[sequence] base input materialization done rir={} rrdp_repos={} rsync_modules={}",
        rir_normalized,
        base_capture.rrdp_repo_count,
        base_capture.rsync_module_count
    );
    let base_ccr_path = rir_dir.join("base.ccr");
    let base_vrps_path = rir_dir.join("base-vrps.csv");
    let base_vaps_path = rir_dir.join("base-vaps.csv");
    let (base_ccr_sha256, base_vrp_rows, base_vap_rows, base_verify, base_self_replay_ok) =
        if args.capture_inputs_only {
            eprintln!("[sequence] base output generation skipped rir={rir_normalized}");
            (
                String::new(),
                std::collections::BTreeSet::<rpki::bundle::VrpCompareRow>::new(),
                std::collections::BTreeSet::<rpki::bundle::VapCompareRow>::new(),
                CcrVerifySummary {
                    content_type_oid: String::new(),
                    version: 0,
                    produced_at_rfc3339_utc: String::new(),
                    state_hashes_ok: false,
                    manifest_instances: 0,
                    roa_payload_sets: 0,
                    roa_vrp_count: 0,
                    aspa_payload_sets: 0,
                    trust_anchor_ski_count: 0,
                    router_key_sets: 0,
                    router_key_count: 0,
                },
                false,
            )
        } else {
            eprintln!("[sequence] base CCR/self-replay start rir={rir_normalized}");
            let base_ccr = build_ccr_from_run(
                &store,
                &[base_out.discovery.trust_anchor.clone()],
                &base_out.tree.vrps,
                &base_out.tree.aspas,
                &base_out.tree.router_keys,
                base_validation_time,
            )
            .map_err(|e| format!("build base ccr failed: {e}"))?;
            write_ccr_file(&base_ccr_path, &base_ccr)
                .map_err(|e| format!("write base ccr failed: {e}"))?;
            let base_ccr_bytes = fs::read(&base_ccr_path)
                .map_err(|e| format!("read base ccr failed: {}: {e}", base_ccr_path.display()))?;
            let base_decoded = decode_content_info(&base_ccr_bytes)
                .map_err(|e| format!("decode base ccr failed: {e}"))?;
            let base_verify =
                verify_content_info(&base_decoded).map_err(|e| format!("verify base ccr failed: {e}"))?;
            let base_vrp_rows = build_vrp_compare_rows(&base_out.tree.vrps, &trust_anchor);
            let base_vap_rows = build_vap_compare_rows(&base_out.tree.aspas, &trust_anchor);
            let (base_ccr_vrps, base_ccr_vaps) =
                rpki::bundle::decode_ccr_compare_views(&base_decoded, &trust_anchor)?;
            if base_vrp_rows != base_ccr_vrps {
                return Err("base-vrps compare view does not match base.ccr".to_string());
            }
            if base_vap_rows != base_ccr_vaps {
                return Err("base-vaps compare view does not match base.ccr".to_string());
            }
            write_vrp_csv(&base_vrps_path, &base_vrp_rows)?;
            write_vap_csv(&base_vaps_path, &base_vap_rows)?;
            let base_replay_store = RocksStore::open(&base_self_replay_dir)
                .map_err(|e| format!("open base self replay db failed: {e}"))?;
            let base_replay_out = run_tree_from_tal_and_ta_der_payload_replay_serial_audit(
                &base_replay_store,
                &Policy::default(),
                &tal_bytes,
                &ta_bytes,
                None,
                &rir_dir.join("base-payload-archive"),
                &rir_dir.join("base-locks.json"),
                base_validation_time,
                &TreeRunConfig {
                    max_depth: args.max_depth,
                    max_instances: args.max_instances,
                },
            )
            .map_err(|e| format!("base self replay failed: {e}"))?;
            if build_vrp_compare_rows(&base_replay_out.tree.vrps, &trust_anchor) != base_vrp_rows {
                return Err("base self replay VRP compare view mismatch".to_string());
            }
            if build_vap_compare_rows(&base_replay_out.tree.aspas, &trust_anchor) != base_vap_rows {
                return Err("base self replay VAP compare view mismatch".to_string());
            }
            let output = (
                sha256_hex(&base_ccr_bytes),
                base_vrp_rows,
                base_vap_rows,
                base_verify,
                true,
            );
            eprintln!("[sequence] base CCR/self-replay done rir={rir_normalized}");
            output
        };
    fs::create_dir_all(rir_dir.join("timings"))
        .map_err(|e| format!("create timings dir failed: {e}"))?;
    write_json(
        &rir_dir.join("timings").join("base-produce.json"),
        &serde_json::json!({
            "mode": "base",
            "validationTime": base_validation_time.format(&Rfc3339).map_err(|e| format!("format base validation time failed: {e}"))?,
            "durationSeconds": base_duration.as_secs_f64(),
        }),
    )?;

    let mut steps_json = Vec::new();
    let mut delta_steps = Vec::new();
    let mut previous_locks_path = rir_dir.join("base-locks.json");
    let mut previous_ref = "base".to_string();
    let sequence_self_replay_dir = out_root.join(".tmp").join(format!("{rir}-sequence-self-replay-db"));
    let _ = fs::remove_dir_all(&sequence_self_replay_dir);
    let sequence_replay_store = if args.capture_inputs_only {
        None
    } else {
        let store = RocksStore::open(&sequence_self_replay_dir)
            .map_err(|e| format!("open sequence self replay db failed: {e}"))?;
        let _base_replay = run_tree_from_tal_and_ta_der_payload_replay_serial_audit(
            &store,
            &Policy::default(),
            &tal_bytes,
            &ta_bytes,
            None,
            &rir_dir.join("base-payload-archive"),
            &rir_dir.join("base-locks.json"),
            base_validation_time,
            &TreeRunConfig {
                max_depth: args.max_depth,
                max_instances: args.max_instances,
            },
        )
        .map_err(|e| format!("sequence base self replay failed: {e}"))?;
        Some(store)
    };
    let mut all_steps_self_replay_ok = true;

    for step_index in 1..=args.delta_count {
        if step_index > 1 && args.delta_interval_secs > 0 {
            std::thread::sleep(Duration::from_secs(args.delta_interval_secs));
        }
        let step_id = format!("step-{step_index:03}");
        let step_dir = delta_steps_root.join(&step_id);
        fs::create_dir_all(&step_dir)
            .map_err(|e| format!("create step dir failed: {}: {e}", step_dir.display()))?;
        let step_validation_time = time::OffsetDateTime::now_utc();
        eprintln!("[sequence] step live run start rir={} step={}", rir_normalized, step_id);
        let step_http = RecordingHttpFetcher::new(
            BlockingHttpFetcher::new(HttpFetcherConfig {
                timeout: Duration::from_secs(args.http_timeout_secs),
                ..HttpFetcherConfig::default()
            })
            .map_err(|e| format!("create step http fetcher failed: {e}"))?,
        );
        let step_rsync_inner = SystemRsyncFetcher::new(SystemRsyncConfig {
            timeout: Duration::from_secs(args.rsync_timeout_secs),
            mirror_root: args.rsync_mirror_root.clone(),
            ..SystemRsyncConfig::default()
        });
        let step_rsync = if args.capture_inputs_only {
            RecordingRsyncFetcher::new_without_objects(step_rsync_inner)
        } else {
            RecordingRsyncFetcher::new(step_rsync_inner)
        };
        let started = Instant::now();
        let step_out = run_tree_from_tal_and_ta_der_serial_audit(
            &store,
            &Policy::default(),
            &tal_bytes,
            &ta_bytes,
            None,
            &step_http,
            &step_rsync,
            step_validation_time,
            &TreeRunConfig {
                max_depth: args.max_depth,
                max_instances: args.max_instances,
            },
        )
        .map_err(|e| format!("live delta step {step_id} failed: {e}"))?;
        let step_duration = started.elapsed();
        eprintln!(
            "[sequence] step live run done rir={} step={} duration_s={:.3}",
            rir_normalized,
            step_id,
            step_duration.as_secs_f64()
        );
        ensure_recorded_target_snapshots_for_locks(&store, &previous_locks_path, &step_http)?;

        eprintln!("[sequence] step output generation phase start rir={} step={}", rir_normalized, step_id);
        let delta_ccr_path = step_dir.join("delta.ccr");
        let delta_vrps_path = step_dir.join("record-delta.csv");
        let delta_vaps_path = step_dir.join("record-delta-vaps.csv");
        let (delta_ccr_sha256, delta_vrp_rows, delta_vap_rows, delta_verify, step_self_replay_ok) =
            if args.capture_inputs_only {
                eprintln!(
                    "[sequence] step CCR/self-replay skipped rir={} step={}",
                    rir_normalized,
                    step_id
                );
                (
                    String::new(),
                    std::collections::BTreeSet::<rpki::bundle::VrpCompareRow>::new(),
                    std::collections::BTreeSet::<rpki::bundle::VapCompareRow>::new(),
                    CcrVerifySummary {
                        content_type_oid: String::new(),
                        version: 0,
                        produced_at_rfc3339_utc: String::new(),
                        state_hashes_ok: false,
                        manifest_instances: 0,
                        roa_payload_sets: 0,
                        roa_vrp_count: 0,
                        aspa_payload_sets: 0,
                        trust_anchor_ski_count: 0,
                        router_key_sets: 0,
                        router_key_count: 0,
                    },
                    false,
                )
            } else {
                eprintln!("[sequence] step CCR/self-replay start rir={} step={}", rir_normalized, step_id);
                let delta_ccr = build_ccr_from_run(
                    &store,
                    &[step_out.discovery.trust_anchor.clone()],
                    &step_out.tree.vrps,
                    &step_out.tree.aspas,
                    &step_out.tree.router_keys,
                    step_validation_time,
                )
                .map_err(|e| format!("build step ccr failed for {step_id}: {e}"))?;
                write_ccr_file(&delta_ccr_path, &delta_ccr)
                    .map_err(|e| format!("write step ccr failed for {step_id}: {e}"))?;
                let delta_ccr_bytes = fs::read(&delta_ccr_path).map_err(|e| {
                    format!("read step ccr failed: {}: {e}", delta_ccr_path.display())
                })?;
                let delta_decoded = decode_content_info(&delta_ccr_bytes)
                    .map_err(|e| format!("decode step ccr failed for {step_id}: {e}"))?;
                let delta_verify = verify_content_info(&delta_decoded)
                    .map_err(|e| format!("verify step ccr failed for {step_id}: {e}"))?;
                let delta_vrp_rows = build_vrp_compare_rows(&step_out.tree.vrps, &trust_anchor);
                let delta_vap_rows = build_vap_compare_rows(&step_out.tree.aspas, &trust_anchor);
                let (ccr_vrps, ccr_vaps) =
                    rpki::bundle::decode_ccr_compare_views(&delta_decoded, &trust_anchor)?;
                if delta_vrp_rows != ccr_vrps {
                    return Err(format!("{step_id} VRP compare view does not match delta.ccr"));
                }
                if delta_vap_rows != ccr_vaps {
                    return Err(format!("{step_id} VAP compare view does not match delta.ccr"));
                }
                write_vrp_csv(&delta_vrps_path, &delta_vrp_rows)?;
                write_vap_csv(&delta_vaps_path, &delta_vap_rows)?;
                let step_replay_out = run_tree_from_tal_and_ta_der_payload_delta_replay_step_serial_audit(
                    sequence_replay_store.as_ref().expect("sequence replay store"),
                    &Policy::default(),
                    &tal_bytes,
                    &ta_bytes,
                    None,
                    &step_dir.join("payload-delta-archive"),
                    &previous_locks_path,
                    &step_dir.join("locks-delta.json"),
                    step_validation_time,
                    &TreeRunConfig {
                        max_depth: args.max_depth,
                        max_instances: args.max_instances,
                    },
                )
                .map_err(|e| format!("sequence self replay failed for {step_id}: {e}"))?;
                let step_self_replay_ok =
                    build_vrp_compare_rows(&step_replay_out.tree.vrps, &trust_anchor) == delta_vrp_rows
                        && build_vap_compare_rows(&step_replay_out.tree.aspas, &trust_anchor)
                            == delta_vap_rows;
                let output = (
                    sha256_hex(&delta_ccr_bytes),
                    delta_vrp_rows,
                    delta_vap_rows,
                    delta_verify,
                    step_self_replay_ok,
                );
                eprintln!("[sequence] step CCR/self-replay done rir={} step={}", rir_normalized, step_id);
                output
            };

        eprintln!("[sequence] step input materialization start rir={} step={}", rir_normalized, step_id);
        let delta_capture = write_live_delta_replay_step_inputs(
            &step_dir,
            &rir_normalized,
            &previous_locks_path,
            step_validation_time,
            &step_out.publication_points,
            &store,
            &step_http.snapshot_responses(),
            &step_rsync.snapshot_fetches(),
        )?;
        let target_lock_capture_id = format!("{rir_normalized}-target-{step_id}");
        write_current_replay_state_locks(
            &step_dir.join("target-locks.json"),
            &target_lock_capture_id,
            step_validation_time,
            &step_out.publication_points,
            &store,
        )?;
        eprintln!(
            "[sequence] step input materialization done rir={} step={} rrdp_repos={} rsync_modules={}",
            rir_normalized,
            step_id,
            delta_capture.rrdp_repo_count,
            delta_capture.rsync_module_count
        );
        write_json(
            &step_dir.join("verification.json"),
            &serde_json::json!({
                "index": step_index,
                "id": step_id,
                "validationTime": step_validation_time.format(&Rfc3339).map_err(|e| format!("format validation time failed: {e}"))?,
                "capture": {
                    "captureId": delta_capture.capture_id,
                    "rrdpRepoCount": delta_capture.rrdp_repo_count,
                    "rsyncModuleCount": delta_capture.rsync_module_count,
                },
                "ccr": {
                    "path": "delta.ccr",
                "sha256": delta_ccr_sha256.clone(),
                    "stateHashesOk": delta_verify.state_hashes_ok,
                    "manifestInstances": delta_verify.manifest_instances,
                    "roaVrpCount": delta_verify.roa_vrp_count,
                    "aspaPayloadSets": delta_verify.aspa_payload_sets,
                    "routerKeyCount": delta_verify.router_key_count,
                },
                "compareViews": {
                    "vrpCount": delta_vrp_rows.len(),
                    "vapCount": delta_vap_rows.len(),
                },
                "selfReplayOk": serde_json::Value::Null,
                "timings": {
                    "durationSeconds": step_duration.as_secs_f64(),
                }
            }),
        )?;
        fs::create_dir_all(step_dir.join("timings"))
            .map_err(|e| format!("create step timings dir failed: {e}"))?;
        write_json(
            &step_dir.join("timings").join("delta-produce.json"),
            &serde_json::json!({
                "mode": "delta",
                "stepIndex": step_index,
                "validationTime": step_validation_time.format(&Rfc3339).map_err(|e| format!("format validation time failed: {e}"))?,
                "durationSeconds": step_duration.as_secs_f64(),
            }),
        )?;

        delta_steps.push(DeltaStepMetadataV2 {
            index: step_index,
            id: step_id.clone(),
            relative_path: format!("delta-steps/{step_id}"),
            base_ref: previous_ref.clone(),
            validation_time: step_validation_time
                .format(&Rfc3339)
                .map_err(|e| format!("format validation time failed: {e}"))?,
            delta_ccr_sha256: delta_ccr_sha256.clone(),
            vrp_count: delta_vrp_rows.len(),
            vap_count: delta_vap_rows.len(),
            relative_archive_path: format!("delta-steps/{step_id}/payload-delta-archive"),
            relative_transition_locks_path: format!("delta-steps/{step_id}/locks-delta.json"),
            relative_target_locks_path: format!("delta-steps/{step_id}/target-locks.json"),
            relative_ccr_path: format!("delta-steps/{step_id}/delta.ccr"),
            relative_vrps_path: format!("delta-steps/{step_id}/record-delta.csv"),
            relative_vaps_path: format!("delta-steps/{step_id}/record-delta-vaps.csv"),
            has_aspa: !delta_vap_rows.is_empty(),
            has_router_key: delta_verify.router_key_count > 0,
        });
        all_steps_self_replay_ok &= step_self_replay_ok;

        steps_json.push(serde_json::json!({
            "index": step_index,
            "id": step_id,
            "validationTime": step_validation_time.format(&Rfc3339).map_err(|e| format!("format validation time failed: {e}"))?,
            "capture": {
                "captureId": delta_capture.capture_id,
                "rrdpRepoCount": delta_capture.rrdp_repo_count,
                "rsyncModuleCount": delta_capture.rsync_module_count,
            },
            "ccr": {
                "path": format!("delta-steps/{step_id}/delta.ccr"),
                "sha256": delta_ccr_sha256,
                "stateHashesOk": delta_verify.state_hashes_ok,
                "manifestInstances": delta_verify.manifest_instances,
                "roaVrpCount": delta_verify.roa_vrp_count,
                "aspaPayloadSets": delta_verify.aspa_payload_sets,
                "routerKeyCount": delta_verify.router_key_count,
            },
            "compareViews": {
                    "vrpCount": delta_vrp_rows.len(),
                    "vapCount": delta_vap_rows.len(),
                },
                "selfReplayOk": if args.capture_inputs_only { serde_json::Value::Null } else { serde_json::Value::Bool(step_self_replay_ok) }
            }));

        previous_locks_path = step_dir.join("target-locks.json");
        previous_ref = step_id;
    }

    let metadata = RirBundleMetadataV2 {
        schema_version: "20260401-v2".to_string(),
        bundle_producer: "ours".to_string(),
        rir: rir_normalized.clone(),
        tal_sha256: sha256_hex(&tal_bytes),
        ta_cert_sha256: sha256_hex(&ta_bytes),
        has_any_aspa: !base_vap_rows.is_empty() || delta_steps.iter().any(|step| step.has_aspa),
        has_any_router_key: base_verify.router_key_count > 0
            || delta_steps.iter().any(|step| step.has_router_key),
        base: BaseBundleStateMetadataV2 {
            validation_time: base_validation_time
                .format(&Rfc3339)
                .map_err(|e| format!("format base validation time failed: {e}"))?,
            ccr_sha256: base_ccr_sha256.clone(),
            vrp_count: base_vrp_rows.len(),
            vap_count: base_vap_rows.len(),
            relative_archive_path: "base-payload-archive".to_string(),
            relative_locks_path: "base-locks.json".to_string(),
            relative_ccr_path: "base.ccr".to_string(),
            relative_vrps_path: "base-vrps.csv".to_string(),
            relative_vaps_path: "base-vaps.csv".to_string(),
        },
        delta_sequence: DeltaSequenceMetadataV2 {
            configured_delta_count: args.delta_count,
            configured_interval_seconds: args.delta_interval_secs,
            steps: delta_steps.clone(),
        },
    };
    write_json(&rir_dir.join("bundle.json"), &metadata)?;
    write_json(
        &rir_dir.join("verification.json"),
        &serde_json::json!({
            "base": {
                "validationTime": metadata.base.validation_time,
                "ccr": {
                    "path": "base.ccr",
                    "sha256": metadata.base.ccr_sha256,
                    "stateHashesOk": base_verify.state_hashes_ok,
                    "manifestInstances": base_verify.manifest_instances,
                    "roaVrpCount": base_verify.roa_vrp_count,
                    "aspaPayloadSets": base_verify.aspa_payload_sets,
                    "routerKeyCount": base_verify.router_key_count,
                },
                "compareViews": {
                    "baseVrpCount": metadata.base.vrp_count,
                    "baseVapCount": metadata.base.vap_count,
                },
                "capture": {
                    "captureId": base_capture.capture_id,
                    "rrdpRepoCount": base_capture.rrdp_repo_count,
                    "rsyncModuleCount": base_capture.rsync_module_count,
                    "selfReplayOk": if args.capture_inputs_only { serde_json::Value::Null } else { serde_json::Value::Bool(base_self_replay_ok) },
                }
            },
            "steps": steps_json,
            "summary": {
                "baseSelfReplayOk": if args.capture_inputs_only { serde_json::Value::Null } else { serde_json::Value::Bool(base_self_replay_ok) },
                "stepCount": args.delta_count,
                "allStepsSelfReplayOk": if args.capture_inputs_only { serde_json::Value::Null } else { serde_json::Value::Bool(all_steps_self_replay_ok) },
            }
        }),
    )?;
    write_v2_top_readme(&out_root.join("README.md"), &rir_normalized, args.delta_count, args.delta_interval_secs)?;
    write_v2_rir_readme(
        &rir_dir.join("README.md"),
        &rir_normalized,
        &metadata.base.validation_time,
        args.delta_count,
        args.delta_interval_secs,
    )?;

    let bundle_manifest = BundleManifestV2 {
        schema_version: "20260401-v2".to_string(),
        bundle_producer: "ours".to_string(),
        recorded_at_rfc3339_utc: time::OffsetDateTime::now_utc()
            .format(&Rfc3339)
            .map_err(|e| format!("format recorded_at failed: {e}"))?,
        rirs: vec![rir_normalized.clone()],
        per_rir_bundles: vec![BundleManifestEntryV2 {
            rir: rir_normalized.clone(),
            relative_path: rir_normalized,
            base_validation_time: metadata.base.validation_time.clone(),
            step_count: metadata.delta_sequence.steps.len(),
            first_delta_validation_time: metadata
                .delta_sequence
                .steps
                .first()
                .map(|step| step.validation_time.clone()),
            last_delta_validation_time: metadata
                .delta_sequence
                .steps
                .last()
                .map(|step| step.validation_time.clone()),
            has_aspa: metadata.has_any_aspa,
        }],
    };
    write_json(&out_root.join("bundle-manifest.json"), &bundle_manifest)?;

    if !args.keep_db {
        drop(sequence_replay_store);
        drop(store);
        let _ = fs::remove_dir_all(&work_db_dir);
        let _ = fs::remove_dir_all(&base_self_replay_dir);
        let _ = fs::remove_dir_all(&sequence_self_replay_dir);
        let tmp_dir = out_root.join(".tmp");
        if tmp_dir.is_dir() {
            let is_empty = fs::read_dir(&tmp_dir)
                .map_err(|e| format!("read tmp dir failed: {}: {e}", tmp_dir.display()))?
                .next()
                .is_none();
            if is_empty {
                let _ = fs::remove_dir(&tmp_dir);
            }
        }
    }

    Ok(out_root.clone())
}

fn main() -> Result<(), String> {
    let args = parse_args(&std::env::args().collect::<Vec<_>>())?;
    let out = run(args)?;
    println!("{}", out.display());
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_args_defaults_delta_sequence_parameters() {
        let argv = vec![
            "replay_bundle_capture_sequence".to_string(),
            "--rir".to_string(),
            "apnic".to_string(),
            "--out-dir".to_string(),
            "out".to_string(),
            "--tal-path".to_string(),
            "tal".to_string(),
            "--ta-path".to_string(),
            "ta".to_string(),
        ];
        let args = parse_args(&argv).expect("parse");
        assert_eq!(args.delta_count, 5);
        assert_eq!(args.delta_interval_secs, 600);
        assert!(!args.keep_db);
    }

    #[test]
    fn parse_args_accepts_overrides_and_keep_db() {
        let argv = vec![
            "replay_bundle_capture_sequence".to_string(),
            "--rir".to_string(),
            "apnic".to_string(),
            "--out-dir".to_string(),
            "out".to_string(),
            "--tal-path".to_string(),
            "tal".to_string(),
            "--ta-path".to_string(),
            "ta".to_string(),
            "--delta-count".to_string(),
            "2".to_string(),
            "--delta-interval-secs".to_string(),
            "0".to_string(),
            "--keep-db".to_string(),
        ];
        let args = parse_args(&argv).expect("parse");
        assert_eq!(args.delta_count, 2);
        assert_eq!(args.delta_interval_secs, 0);
        assert!(args.keep_db);
    }
}
