use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};
use std::thread;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};

use crate::ccr::{CcrStateDigestComparison, compare_state_digests, decode_state_digest_summary};
use serde::Serialize;
use serde_json::{Value, json};

const RPS: [&str; 3] = ["ours-rp", "routinator", "rpki-client"];
const CCR_STATES: [&str; 5] = ["mfts", "vrps", "vaps", "tas", "rks"];

#[derive(Clone, Debug, PartialEq, Eq)]
struct Args {
    ours_run_root: PathBuf,
    peer_root: PathBuf,
    listen: String,
    poll_secs: u64,
    instance: String,
    once: bool,
    out_metrics: Option<PathBuf>,
    out_status: Option<PathBuf>,
}

fn usage() -> &'static str {
    "Usage: rpki_inter_rp_metrics --ours-run-root <path> --peer-root <path> [--listen <addr:port>] [--poll-secs <n>] [--instance <name>] [--once] [--out-metrics <path>] [--out-status <path>]"
}

pub fn main_entry() -> Result<(), String> {
    real_main()
}

fn real_main() -> Result<(), String> {
    let args = parse_args(&std::env::args().collect::<Vec<_>>())?;
    if args.once {
        let snapshot = scan(&args)?;
        let metrics = render_metrics(&snapshot);
        let status = render_status_json(&snapshot)?;
        if let Some(path) = args.out_metrics.as_ref() {
            write_file(path, metrics.as_bytes())?;
        } else {
            print!("{metrics}");
        }
        if let Some(path) = args.out_status.as_ref() {
            write_file(path, status.as_bytes())?;
        }
        return Ok(());
    }

    let shared = Arc::new(RwLock::new(scan(&args)?));
    let scanner = Arc::clone(&shared);
    let scan_args = args.clone();
    thread::spawn(move || {
        let poll_secs = scan_args.poll_secs.max(1);
        loop {
            thread::sleep(Duration::from_secs(poll_secs));
            match scan(&scan_args) {
                Ok(next) => *scanner.write().expect("metrics lock poisoned") = next,
                Err(err) => {
                    let mut previous = scanner.write().expect("metrics lock poisoned");
                    previous.service.last_reload_success = false;
                    previous.service.last_scan_timestamp_seconds = unix_now_seconds();
                    previous
                        .service
                        .parse_errors
                        .push(format!("scan failed: {err}"));
                }
            }
        }
    });

    serve_http(&args.listen, shared)
}

fn parse_args(argv: &[String]) -> Result<Args, String> {
    let mut ours_run_root = None;
    let mut peer_root = None;
    let mut listen = "127.0.0.1:9557".to_string();
    let mut poll_secs = 30u64;
    let mut instance = "inter-rp".to_string();
    let mut once = false;
    let mut out_metrics = None;
    let mut out_status = None;
    let mut index = 1usize;
    while index < argv.len() {
        match argv[index].as_str() {
            "--ours-run-root" => {
                index += 1;
                ours_run_root = Some(PathBuf::from(value_at(argv, index, "--ours-run-root")?));
            }
            "--peer-root" => {
                index += 1;
                peer_root = Some(PathBuf::from(value_at(argv, index, "--peer-root")?));
            }
            "--listen" => {
                index += 1;
                listen = value_at(argv, index, "--listen")?.to_string();
            }
            "--poll-secs" => {
                index += 1;
                let value = value_at(argv, index, "--poll-secs")?;
                poll_secs = value
                    .parse::<u64>()
                    .map_err(|_| format!("invalid --poll-secs: {value}"))?;
            }
            "--instance" => {
                index += 1;
                instance = value_at(argv, index, "--instance")?.to_string();
            }
            "--once" => once = true,
            "--out-metrics" => {
                index += 1;
                out_metrics = Some(PathBuf::from(value_at(argv, index, "--out-metrics")?));
            }
            "--out-status" => {
                index += 1;
                out_status = Some(PathBuf::from(value_at(argv, index, "--out-status")?));
            }
            "-h" | "--help" => return Err(usage().to_string()),
            other => return Err(format!("unknown argument: {other}\n{}", usage())),
        }
        index += 1;
    }
    Ok(Args {
        ours_run_root: ours_run_root
            .ok_or_else(|| format!("--ours-run-root is required\n{}", usage()))?,
        peer_root: peer_root.ok_or_else(|| format!("--peer-root is required\n{}", usage()))?,
        listen,
        poll_secs,
        instance,
        once,
        out_metrics,
        out_status,
    })
}

fn value_at<'a>(argv: &'a [String], index: usize, flag: &str) -> Result<&'a str, String> {
    argv.get(index)
        .map(|s| s.as_str())
        .ok_or_else(|| format!("{flag} requires a value"))
}

#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct Snapshot {
    instance: String,
    service: ServiceMetrics,
    sync: SyncMetrics,
    samples: BTreeMap<String, RpSample>,
    ccr_compare: Option<CcrCompareMetrics>,
}

#[derive(Clone, Debug, Default, Serialize)]
#[serde(rename_all = "camelCase")]
struct ServiceMetrics {
    last_scan_timestamp_seconds: f64,
    last_scan_duration_seconds: f64,
    last_reload_success: bool,
    parse_errors: Vec<String>,
    ours_run_root: String,
    peer_root: String,
}

#[derive(Clone, Debug, Default, Serialize)]
#[serde(rename_all = "camelCase")]
struct SyncMetrics {
    present: bool,
    success: bool,
    last_sync_timestamp_seconds: Option<f64>,
    age_seconds: Option<f64>,
    remote_host: Option<String>,
    message: Option<String>,
}

#[derive(Clone, Debug, Default, Serialize)]
#[serde(rename_all = "camelCase")]
struct RpSample {
    rp: String,
    present: bool,
    source_dir: Option<String>,
    run_id: Option<String>,
    run_seq: Option<u64>,
    success: bool,
    wall_seconds: Option<f64>,
    finish_timestamp_seconds: Option<f64>,
    artifact_age_seconds: Option<f64>,
    max_rss_bytes: BTreeMap<String, u64>,
    vrps: Option<u64>,
    vaps: Option<u64>,
    ccr_path: Option<String>,
    ccr: Option<CcrSampleMetrics>,
    parse_errors: Vec<String>,
}

#[derive(Clone, Debug, Default, Serialize)]
#[serde(rename_all = "camelCase")]
struct CcrSampleMetrics {
    version: u32,
    hash_alg_oid: String,
    state_present: BTreeMap<String, bool>,
    state_hashes: BTreeMap<String, String>,
}

#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct CcrCompareMetrics {
    available: bool,
    match_all: bool,
    states: BTreeMap<String, CcrStateMetrics>,
    error: Option<String>,
}

#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct CcrStateMetrics {
    left_present: bool,
    right_present: bool,
    matches: bool,
    left_hash_hex: Option<String>,
    right_hash_hex: Option<String>,
}

fn scan(args: &Args) -> Result<Snapshot, String> {
    let start = Instant::now();
    let now = unix_now_seconds();
    let mut parse_errors = Vec::new();
    let mut samples = BTreeMap::new();

    let ours = scan_ours(&args.ours_run_root, now);
    parse_errors.extend(
        ours.parse_errors
            .iter()
            .map(|err| format!("ours-rp: {err}")),
    );
    samples.insert("ours-rp".to_string(), ours);

    for rp in ["routinator", "rpki-client"] {
        let peer = scan_peer(&args.peer_root, rp, now);
        parse_errors.extend(peer.parse_errors.iter().map(|err| format!("{rp}: {err}")));
        samples.insert(rp.to_string(), peer);
    }

    let sync = scan_sync_status(&args.peer_root, now);
    if let Some(message) = sync.message.as_ref().filter(|_| !sync.success) {
        parse_errors.push(format!("sync: {message}"));
    }

    let ccr_compare = build_ccr_compare(samples.get("ours-rp"), samples.get("rpki-client"));
    if let Some(compare) = ccr_compare
        .as_ref()
        .and_then(|compare| compare.error.as_ref())
    {
        parse_errors.push(format!("ccr compare: {compare}"));
    }

    Ok(Snapshot {
        instance: args.instance.clone(),
        service: ServiceMetrics {
            last_scan_timestamp_seconds: now,
            last_scan_duration_seconds: start.elapsed().as_secs_f64(),
            last_reload_success: parse_errors.is_empty(),
            parse_errors,
            ours_run_root: args.ours_run_root.display().to_string(),
            peer_root: args.peer_root.display().to_string(),
        },
        sync,
        samples,
        ccr_compare,
    })
}

fn scan_ours(run_root: &Path, now: f64) -> RpSample {
    let mut sample = RpSample {
        rp: "ours-rp".to_string(),
        ..RpSample::default()
    };
    let runs_root = if run_root.join("runs").is_dir() {
        run_root.join("runs")
    } else {
        run_root.to_path_buf()
    };
    let Some(run_dir) = latest_run_with_summary(&runs_root) else {
        sample.parse_errors.push(format!(
            "no run-summary.json found under {}",
            runs_root.display()
        ));
        return sample;
    };
    sample.present = true;
    sample.source_dir = Some(run_dir.display().to_string());

    let summary_path = run_dir.join("run-summary.json");
    let value = match read_json(&summary_path) {
        Ok(value) => value,
        Err(err) => {
            sample.parse_errors.push(err);
            return sample;
        }
    };
    sample.run_id = json_str(&value, &["runId"])
        .map(str::to_string)
        .or_else(|| {
            run_dir
                .file_name()
                .and_then(|v| v.to_str())
                .map(str::to_string)
        });
    sample.run_seq = json_u64(&value, &["runSeq"]).or_else(|| run_index_from_path(&run_dir));
    let status = json_str(&value, &["status"]).unwrap_or("unknown");
    sample.success = status == "success" && json_i64(&value, &["exitCode"]).unwrap_or(0) == 0;
    sample.wall_seconds = json_f64(&value, &["wallMs"]).map(|v| v / 1000.0);
    sample.finish_timestamp_seconds =
        json_str(&value, &["finishedAtRfc3339Utc"]).and_then(parse_rfc3339_to_unix);
    sample.artifact_age_seconds = sample
        .finish_timestamp_seconds
        .map(|finished| (now - finished).max(0.0));
    if let Some(max_rss_kb) = json_u64(&value, &["processMetrics", "maxRssKb"]) {
        sample
            .max_rss_bytes
            .insert("parent".to_string(), max_rss_kb.saturating_mul(1024));
        sample.max_rss_bytes.insert(
            "aggregate_peak".to_string(),
            max_rss_kb.saturating_mul(1024),
        );
    }
    sample.vrps =
        count_vrp_csv_unique_keys_opt(&run_dir.join("vrps.csv"), &mut sample.parse_errors)
            .or_else(|| json_u64(&value, &["reportCounts", "vrps"]));
    sample.vaps =
        count_vap_csv_unique_keys_opt(&run_dir.join("vaps.csv"), &mut sample.parse_errors)
            .or_else(|| json_u64(&value, &["reportCounts", "aspas"]));

    let ccr_path = run_dir.join("result.ccr");
    load_ccr_sample(&ccr_path, &mut sample);
    sample
}

fn scan_peer(peer_root: &Path, rp: &str, now: f64) -> RpSample {
    let mut sample = RpSample {
        rp: rp.to_string(),
        ..RpSample::default()
    };
    let rp_root = peer_root.join(rp);
    let latest = rp_root.join("latest");
    if !latest.exists() {
        sample
            .parse_errors
            .push(format!("missing latest directory: {}", latest.display()));
        return sample;
    }
    sample.present = true;
    sample.source_dir = Some(latest.display().to_string());

    let meta_path = ["run-meta.json", "meta.json"]
        .into_iter()
        .map(|name| latest.join(name))
        .find(|path| path.exists());
    let mut meta = Value::Null;
    if let Some(path) = meta_path.as_ref() {
        match read_json(path) {
            Ok(value) => meta = value,
            Err(err) => sample.parse_errors.push(err),
        }
    } else {
        sample
            .parse_errors
            .push(format!("missing run-meta.json under {}", latest.display()));
    }

    sample.run_id = json_str(&meta, &["runId"])
        .map(str::to_string)
        .or_else(|| json_str(&meta, &["run_id"]).map(str::to_string));
    sample.run_seq = json_u64(&meta, &["runSeq"]).or_else(|| json_u64(&meta, &["run_seq"]));
    sample.success = json_bool(&meta, &["success"]).unwrap_or(false);
    sample.wall_seconds = json_f64(&meta, &["wallSeconds"])
        .or_else(|| json_f64(&meta, &["wall_seconds"]))
        .or_else(|| json_f64(&meta, &["wallMs"]).map(|v| v / 1000.0))
        .or_else(|| json_f64(&meta, &["wall_ms"]).map(|v| v / 1000.0));
    sample.finish_timestamp_seconds = json_str(&meta, &["finishedAtRfc3339Utc"])
        .or_else(|| json_str(&meta, &["finished_at_rfc3339_utc"]))
        .and_then(parse_rfc3339_to_unix);
    sample.artifact_age_seconds = sample
        .finish_timestamp_seconds
        .map(|finished| (now - finished).max(0.0));
    read_rss_metric(&meta, &mut sample);

    let vrps_path = artifact_path(&latest, &meta, "vrpsCsv", "vrps.csv");
    let vaps_path = artifact_path(&latest, &meta, "vapsCsv", "vaps.csv");
    let ccr_path = artifact_path(&latest, &meta, "ccr", "result.ccr");
    load_ccr_sample(&ccr_path, &mut sample);

    sample.vrps = count_vrp_csv_unique_keys_opt(&vrps_path, &mut sample.parse_errors)
        .or_else(|| json_u64(&meta, &["counts", "vrps"]));
    sample.vaps = count_vap_csv_unique_keys_opt(&vaps_path, &mut sample.parse_errors)
        .or_else(|| json_u64(&meta, &["counts", "vaps"]))
        .or_else(|| json_u64(&meta, &["counts", "aspas"]));

    sample
}

fn read_rss_metric(meta: &Value, sample: &mut RpSample) {
    let fields = [
        ("parent", &["maxRssKb", "parent"][..]),
        ("child_max", &["maxRssKb", "childMax"][..]),
        ("aggregate_peak", &["maxRssKb", "aggregatePeak"][..]),
    ];
    for (name, path) in fields {
        if let Some(value) = json_u64(meta, path) {
            sample
                .max_rss_bytes
                .insert(name.to_string(), value.saturating_mul(1024));
        }
    }
    if sample.max_rss_bytes.is_empty() {
        if let Some(value) = json_u64(meta, &["maxRssKb"]) {
            sample
                .max_rss_bytes
                .insert("parent".to_string(), value.saturating_mul(1024));
            sample
                .max_rss_bytes
                .insert("aggregate_peak".to_string(), value.saturating_mul(1024));
        }
    }
}

fn artifact_path(run_dir: &Path, meta: &Value, key: &str, default_name: &str) -> PathBuf {
    json_str(meta, &["artifacts", key])
        .map(PathBuf::from)
        .filter(|path| !path.as_os_str().is_empty())
        .map(|path| {
            if path.is_absolute() {
                path
            } else {
                run_dir.join(path)
            }
        })
        .unwrap_or_else(|| run_dir.join(default_name))
}

fn load_ccr_sample(ccr_path: &Path, sample: &mut RpSample) {
    if !ccr_path.exists() {
        return;
    }
    sample.ccr_path = Some(ccr_path.display().to_string());
    let der = match fs::read(ccr_path) {
        Ok(der) => der,
        Err(err) => {
            sample
                .parse_errors
                .push(format!("read CCR failed: {}: {err}", ccr_path.display()));
            return;
        }
    };
    match decode_state_digest_summary(&der) {
        Ok(summary) => sample.ccr = Some(ccr_summary_to_metrics(summary)),
        Err(err) => sample.parse_errors.push(format!(
            "decode CCR digest failed: {}: {err}",
            ccr_path.display()
        )),
    }
}

fn ccr_summary_to_metrics(summary: crate::ccr::CcrStateDigestSummary) -> CcrSampleMetrics {
    let mut state_present = BTreeMap::new();
    let mut state_hashes = BTreeMap::new();
    for (name, hash) in [
        ("mfts", summary.mfts),
        ("vrps", summary.vrps),
        ("vaps", summary.vaps),
        ("tas", summary.tas),
        ("rks", summary.rks),
    ] {
        state_present.insert(name.to_string(), hash.is_some());
        if let Some(hash) = hash {
            state_hashes.insert(name.to_string(), hex::encode(hash));
        }
    }
    CcrSampleMetrics {
        version: summary.version,
        hash_alg_oid: summary.hash_alg_oid,
        state_present,
        state_hashes,
    }
}

fn scan_sync_status(peer_root: &Path, now: f64) -> SyncMetrics {
    let path = peer_root.join("sync-status.json");
    if !path.exists() {
        return SyncMetrics {
            present: false,
            success: false,
            message: Some(format!("missing {}", path.display())),
            ..SyncMetrics::default()
        };
    }
    let value = match read_json(&path) {
        Ok(value) => value,
        Err(err) => {
            return SyncMetrics {
                present: true,
                success: false,
                message: Some(err),
                ..SyncMetrics::default()
            };
        }
    };
    let timestamp = json_str(&value, &["lastSyncAtRfc3339Utc"])
        .or_else(|| json_str(&value, &["lastSyncTimestampRfc3339Utc"]))
        .or_else(|| json_str(&value, &["updatedAtRfc3339Utc"]))
        .and_then(parse_rfc3339_to_unix);
    SyncMetrics {
        present: true,
        success: json_bool(&value, &["success"])
            .or_else(|| json_bool(&value, &["lastSyncSuccess"]))
            .unwrap_or(false),
        last_sync_timestamp_seconds: timestamp,
        age_seconds: timestamp.map(|ts| (now - ts).max(0.0)),
        remote_host: json_str(&value, &["remoteHost"]).map(str::to_string),
        message: json_str(&value, &["message"])
            .or_else(|| json_str(&value, &["error"]))
            .map(str::to_string),
    }
}

fn build_ccr_compare(
    ours: Option<&RpSample>,
    rpki_client: Option<&RpSample>,
) -> Option<CcrCompareMetrics> {
    let left = ours?.ccr_path.as_ref()?;
    let right = rpki_client?.ccr_path.as_ref()?;
    let left = match fs::read(left) {
        Ok(value) => value,
        Err(err) => {
            return Some(CcrCompareMetrics {
                available: false,
                match_all: false,
                states: BTreeMap::new(),
                error: Some(format!("read ours CCR failed: {err}")),
            });
        }
    };
    let right = match fs::read(right) {
        Ok(value) => value,
        Err(err) => {
            return Some(CcrCompareMetrics {
                available: false,
                match_all: false,
                states: BTreeMap::new(),
                error: Some(format!("read rpki-client CCR failed: {err}")),
            });
        }
    };
    match compare_state_digests(&left, &right) {
        Ok(comparison) => Some(compare_to_metrics(comparison)),
        Err(err) => Some(CcrCompareMetrics {
            available: false,
            match_all: false,
            states: BTreeMap::new(),
            error: Some(err.to_string()),
        }),
    }
}

fn compare_to_metrics(comparison: CcrStateDigestComparison) -> CcrCompareMetrics {
    let states = comparison
        .states
        .iter()
        .map(|state| {
            (
                state.name.to_string(),
                CcrStateMetrics {
                    left_present: state.ours_present,
                    right_present: state.peer_present,
                    matches: state.matches,
                    left_hash_hex: state.ours_hash_hex.clone(),
                    right_hash_hex: state.peer_hash_hex.clone(),
                },
            )
        })
        .collect();
    CcrCompareMetrics {
        available: true,
        match_all: comparison.matches(),
        states,
        error: None,
    }
}

fn latest_run_with_summary(runs_root: &Path) -> Option<PathBuf> {
    let mut candidates = fs::read_dir(runs_root)
        .ok()?
        .filter_map(Result::ok)
        .map(|entry| entry.path())
        .filter(|path| path.is_dir() && path.join("run-summary.json").exists())
        .filter_map(|path| run_index_from_path(&path).map(|index| (index, path)))
        .collect::<Vec<_>>();
    candidates.sort_by_key(|(index, _)| *index);
    candidates.pop().map(|(_, path)| path)
}

fn count_vrp_csv_unique_keys_opt(path: &Path, errors: &mut Vec<String>) -> Option<u64> {
    if !path.exists() {
        return None;
    }
    match count_vrp_csv_unique_keys(path) {
        Ok(count) => Some(count),
        Err(err) => {
            errors.push(err);
            None
        }
    }
}

fn count_vap_csv_unique_keys_opt(path: &Path, errors: &mut Vec<String>) -> Option<u64> {
    if !path.exists() {
        return None;
    }
    match count_vap_csv_unique_keys(path) {
        Ok(count) => Some(count),
        Err(err) => {
            errors.push(err);
            None
        }
    }
}

fn count_vrp_csv_unique_keys(path: &Path) -> Result<u64, String> {
    let content = fs::read_to_string(path)
        .map_err(|e| format!("read VRP CSV failed: {}: {e}", path.display()))?;
    let mut unique = BTreeSet::new();
    for (index, line) in data_csv_lines(&content).enumerate() {
        if index == 0 {
            continue;
        }
        let columns = split_csv_simple(line);
        if columns.len() < 3 {
            return Err(format!(
                "invalid VRP CSV row in {}: expected at least 3 columns, got {}",
                path.display(),
                columns.len()
            ));
        }
        unique.insert((
            columns[0].to_string(),
            columns[1].to_string(),
            columns[2].to_string(),
        ));
    }
    Ok(unique.len() as u64)
}

fn count_vap_csv_unique_keys(path: &Path) -> Result<u64, String> {
    let content = fs::read_to_string(path)
        .map_err(|e| format!("read VAP CSV failed: {}: {e}", path.display()))?;
    let mut unique = BTreeSet::new();
    for (index, line) in data_csv_lines(&content).enumerate() {
        if index == 0 {
            continue;
        }
        let columns = split_csv_simple(line);
        if columns.len() < 2 {
            return Err(format!(
                "invalid VAP CSV row in {}: expected at least 2 columns, got {}",
                path.display(),
                columns.len()
            ));
        }
        unique.insert((columns[0].to_string(), columns[1].to_string()));
    }
    Ok(unique.len() as u64)
}

#[cfg(test)]
fn count_csv_rows(path: &Path) -> Result<u64, String> {
    let content = fs::read_to_string(path)
        .map_err(|e| format!("read CSV failed: {}: {e}", path.display()))?;
    let count = data_csv_lines(&content).skip(1).count();
    Ok(count as u64)
}

fn data_csv_lines(content: &str) -> impl Iterator<Item = &str> {
    content
        .lines()
        .map(str::trim)
        .filter(|line| !line.is_empty())
        .filter(|line| !line.starts_with('#'))
}

fn split_csv_simple(line: &str) -> Vec<&str> {
    line.split(',').map(str::trim).collect()
}

fn render_metrics(snapshot: &Snapshot) -> String {
    let mut out = String::new();
    let mut writer = PromWriter::new(&mut out);
    let instance = snapshot.instance.as_str();
    writer.gauge(
        "inter_rp_service_up",
        "Inter-RP metrics service is up",
        &[label("instance", instance)],
        1.0,
    );
    writer.gauge(
        "inter_rp_service_last_scan_timestamp_seconds",
        "Last inter-RP scan timestamp",
        &[label("instance", instance)],
        snapshot.service.last_scan_timestamp_seconds,
    );
    writer.gauge(
        "inter_rp_service_last_scan_duration_seconds",
        "Last inter-RP scan duration",
        &[label("instance", instance)],
        snapshot.service.last_scan_duration_seconds,
    );
    writer.gauge(
        "inter_rp_service_last_reload_success",
        "Last inter-RP reload success",
        &[label("instance", instance)],
        bool_value(snapshot.service.last_reload_success),
    );
    writer.gauge(
        "inter_rp_parse_errors",
        "Current inter-RP parse error count",
        &[label("instance", instance)],
        snapshot.service.parse_errors.len() as f64,
    );
    writer.gauge(
        "inter_rp_sync_last_success",
        "Last remote200 artifact sync success",
        &[label("instance", instance)],
        bool_value(snapshot.sync.success),
    );
    if let Some(ts) = snapshot.sync.last_sync_timestamp_seconds {
        writer.gauge(
            "inter_rp_sync_last_timestamp_seconds",
            "Last remote200 artifact sync timestamp",
            &[label("instance", instance)],
            ts,
        );
    }
    if let Some(age) = snapshot.sync.age_seconds {
        writer.gauge(
            "inter_rp_sync_age_seconds",
            "Age of latest remote200 artifact sync",
            &[label("instance", instance)],
            age,
        );
    }

    for rp in RPS {
        let sample = snapshot.samples.get(rp);
        render_sample_metrics(&mut writer, instance, rp, sample);
    }
    render_count_diffs(&mut writer, instance, &snapshot.samples);
    render_ccr_compare_metrics(&mut writer, instance, snapshot.ccr_compare.as_ref());
    out
}

fn render_sample_metrics(
    writer: &mut PromWriter<'_>,
    instance: &str,
    rp: &str,
    sample: Option<&RpSample>,
) {
    let present = sample.map(|sample| sample.present).unwrap_or(false);
    let success = sample.map(|sample| sample.success).unwrap_or(false);
    writer.gauge(
        "inter_rp_run_present",
        "Whether latest RP run artifact is present",
        &[label("instance", instance), label("rp", rp)],
        bool_value(present),
    );
    writer.gauge(
        "inter_rp_run_success",
        "Whether latest RP run succeeded",
        &[label("instance", instance), label("rp", rp)],
        bool_value(success),
    );
    if let Some(sample) = sample {
        if let Some(seq) = sample.run_seq {
            writer.gauge(
                "inter_rp_run_seq",
                "Latest RP run sequence",
                &[label("instance", instance), label("rp", rp)],
                seq as f64,
            );
        }
        if let Some(wall) = sample.wall_seconds {
            writer.gauge(
                "inter_rp_run_wall_seconds",
                "Latest RP run wall clock seconds",
                &[label("instance", instance), label("rp", rp)],
                wall,
            );
        }
        if let Some(finish) = sample.finish_timestamp_seconds {
            writer.gauge(
                "inter_rp_run_finish_timestamp_seconds",
                "Latest RP run finish timestamp",
                &[label("instance", instance), label("rp", rp)],
                finish,
            );
        }
        if let Some(age) = sample.artifact_age_seconds {
            writer.gauge(
                "inter_rp_artifact_age_seconds",
                "Latest RP artifact age seconds",
                &[label("instance", instance), label("rp", rp)],
                age,
            );
        }
        for (kind, bytes) in &sample.max_rss_bytes {
            writer.gauge(
                "inter_rp_run_max_rss_bytes",
                "Latest RP run max RSS bytes",
                &[
                    label("instance", instance),
                    label("rp", rp),
                    label("kind", kind),
                ],
                *bytes as f64,
            );
        }
        if let Some(vrps) = sample.vrps {
            writer.gauge(
                "inter_rp_vrps",
                "Latest RP VRP count",
                &[label("instance", instance), label("rp", rp)],
                vrps as f64,
            );
        }
        if let Some(vaps) = sample.vaps {
            writer.gauge(
                "inter_rp_vaps",
                "Latest RP VAP/ASPA count",
                &[label("instance", instance), label("rp", rp)],
                vaps as f64,
            );
        }
        writer.gauge(
            "inter_rp_sample_parse_errors",
            "Current parse error count for an RP sample",
            &[label("instance", instance), label("rp", rp)],
            sample.parse_errors.len() as f64,
        );
        if let Some(ccr) = sample.ccr.as_ref() {
            writer.gauge(
                "inter_rp_ccr_version",
                "CCR version",
                &[label("instance", instance), label("rp", rp)],
                ccr.version as f64,
            );
            for state in CCR_STATES {
                writer.gauge(
                    "inter_rp_ccr_digest_present",
                    "CCR state digest presence",
                    &[
                        label("instance", instance),
                        label("rp", rp),
                        label("state", state),
                    ],
                    bool_value(ccr.state_present.get(state).copied().unwrap_or(false)),
                );
            }
        }
    }
}

fn render_count_diffs(
    writer: &mut PromWriter<'_>,
    instance: &str,
    samples: &BTreeMap<String, RpSample>,
) {
    for (left_index, left) in RPS.iter().enumerate() {
        for right in RPS.iter().skip(left_index + 1) {
            let left_sample = samples.get(*left);
            let right_sample = samples.get(*right);
            if let (Some(left_vrps), Some(right_vrps)) = (
                left_sample.and_then(|sample| sample.vrps),
                right_sample.and_then(|sample| sample.vrps),
            ) {
                writer.gauge(
                    "inter_rp_vrps_diff",
                    "Latest VRP count difference between two RPs; value is left minus right",
                    &[
                        label("instance", instance),
                        label("left", left),
                        label("right", right),
                    ],
                    left_vrps as f64 - right_vrps as f64,
                );
            }
            if let (Some(left_vaps), Some(right_vaps)) = (
                left_sample.and_then(|sample| sample.vaps),
                right_sample.and_then(|sample| sample.vaps),
            ) {
                writer.gauge(
                    "inter_rp_vaps_diff",
                    "Latest VAP/ASPA count difference between two RPs; value is left minus right",
                    &[
                        label("instance", instance),
                        label("left", left),
                        label("right", right),
                    ],
                    left_vaps as f64 - right_vaps as f64,
                );
            }
        }
    }
}

fn render_ccr_compare_metrics(
    writer: &mut PromWriter<'_>,
    instance: &str,
    compare: Option<&CcrCompareMetrics>,
) {
    let available = compare.map(|compare| compare.available).unwrap_or(false);
    let match_all = compare.map(|compare| compare.match_all).unwrap_or(false);
    writer.gauge(
        "inter_rp_ccr_digest_compare_available",
        "Whether ours-rp and rpki-client CCR digest comparison is available",
        &[
            label("instance", instance),
            label("left", "ours-rp"),
            label("right", "rpki-client"),
        ],
        bool_value(available),
    );
    writer.gauge(
        "inter_rp_ccr_digest_match",
        "Whether CCR state digest matches between ours-rp and rpki-client",
        &[
            label("instance", instance),
            label("left", "ours-rp"),
            label("right", "rpki-client"),
            label("state", "overall"),
        ],
        bool_value(match_all),
    );
    if let Some(compare) = compare {
        for state in CCR_STATES {
            let state_metrics = compare.states.get(state);
            writer.gauge(
                "inter_rp_ccr_digest_match",
                "Whether CCR state digest matches between ours-rp and rpki-client",
                &[
                    label("instance", instance),
                    label("left", "ours-rp"),
                    label("right", "rpki-client"),
                    label("state", state),
                ],
                bool_value(state_metrics.map(|state| state.matches).unwrap_or(false)),
            );
        }
    }
}

fn render_status_json(snapshot: &Snapshot) -> Result<String, String> {
    serde_json::to_string_pretty(&json!({
        "schemaVersion": 1,
        "generatedBy": "rpki_inter_rp_metrics",
        "instance": snapshot.instance,
        "service": snapshot.service,
        "sync": snapshot.sync,
        "samples": snapshot.samples,
        "ccrCompare": snapshot.ccr_compare,
    }))
    .map_err(|e| e.to_string())
}

#[derive(Clone, Debug)]
struct Label<'a> {
    key: &'a str,
    value: &'a str,
}

fn label<'a>(key: &'a str, value: &'a str) -> Label<'a> {
    Label { key, value }
}

struct PromWriter<'a> {
    out: &'a mut String,
    emitted_headers: BTreeSet<String>,
}

impl<'a> PromWriter<'a> {
    fn new(out: &'a mut String) -> Self {
        Self {
            out,
            emitted_headers: BTreeSet::new(),
        }
    }

    fn gauge(&mut self, name: &str, help: &str, labels: &[Label<'_>], value: f64) {
        self.header(name, help, "gauge");
        self.out.push_str(name);
        write_labels(self.out, labels);
        self.out.push(' ');
        self.out.push_str(&format_prom_value(value));
        self.out.push('\n');
    }

    fn header(&mut self, name: &str, help: &str, metric_type: &str) {
        if self.emitted_headers.insert(name.to_string()) {
            self.out.push_str("# HELP ");
            self.out.push_str(name);
            self.out.push(' ');
            self.out.push_str(&escape_help(help));
            self.out.push('\n');
            self.out.push_str("# TYPE ");
            self.out.push_str(name);
            self.out.push(' ');
            self.out.push_str(metric_type);
            self.out.push('\n');
        }
    }
}

fn write_labels(out: &mut String, labels: &[Label<'_>]) {
    if labels.is_empty() {
        return;
    }
    out.push('{');
    for (index, label) in labels.iter().enumerate() {
        if index > 0 {
            out.push(',');
        }
        out.push_str(label.key);
        out.push_str("=\"");
        out.push_str(&escape_label(label.value));
        out.push('"');
    }
    out.push('}');
}

fn serve_http(listen: &str, shared: Arc<RwLock<Snapshot>>) -> Result<(), String> {
    let listener = TcpListener::bind(listen).map_err(|e| format!("bind failed: {listen}: {e}"))?;
    for stream in listener.incoming() {
        match stream {
            Ok(mut stream) => {
                let snapshot = shared.read().expect("metrics lock poisoned").clone();
                if let Err(err) = handle_http_stream(&mut stream, &snapshot) {
                    eprintln!("http request failed: {err}");
                }
            }
            Err(err) => eprintln!("accept failed: {err}"),
        }
    }
    Ok(())
}

fn handle_http_stream(stream: &mut TcpStream, snapshot: &Snapshot) -> Result<(), String> {
    let mut buf = [0u8; 4096];
    let len = stream.read(&mut buf).map_err(|e| e.to_string())?;
    let req = String::from_utf8_lossy(&buf[..len]);
    let path = req
        .lines()
        .next()
        .and_then(|line| line.split_whitespace().nth(1))
        .unwrap_or("/");
    match path {
        "/metrics" => write_http_response(
            stream,
            "200 OK",
            "text/plain; version=0.0.4",
            &render_metrics(snapshot),
        ),
        "/status" => write_http_response(
            stream,
            "200 OK",
            "application/json",
            &render_status_json(snapshot)?,
        ),
        "/healthz" => write_http_response(stream, "200 OK", "text/plain", "ok\n"),
        _ => write_http_response(stream, "404 Not Found", "text/plain", "not found\n"),
    }
}

fn write_http_response(
    stream: &mut TcpStream,
    status: &str,
    content_type: &str,
    body: &str,
) -> Result<(), String> {
    let header = format!(
        "HTTP/1.1 {status}\r\nContent-Type: {content_type}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
        body.len()
    );
    stream
        .write_all(header.as_bytes())
        .and_then(|_| stream.write_all(body.as_bytes()))
        .map_err(|e| e.to_string())
}

fn read_json(path: &Path) -> Result<Value, String> {
    let bytes = fs::read(path).map_err(|e| format!("read json failed: {}: {e}", path.display()))?;
    serde_json::from_slice(&bytes)
        .map_err(|e| format!("parse json failed: {}: {e}", path.display()))
}

fn write_file(path: &Path, bytes: &[u8]) -> Result<(), String> {
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)
            .map_err(|e| format!("create parent dirs failed: {}: {e}", parent.display()))?;
    }
    fs::write(path, bytes).map_err(|e| format!("write file failed: {}: {e}", path.display()))
}

fn run_index_from_path(path: &Path) -> Option<u64> {
    path.file_name()
        .and_then(|name| name.to_str())
        .and_then(|name| name.strip_prefix("run_"))
        .and_then(|value| value.parse::<u64>().ok())
}

fn json_str<'a>(value: &'a Value, path: &[&str]) -> Option<&'a str> {
    let mut current = value;
    for key in path {
        current = current.get(*key)?;
    }
    current.as_str()
}

fn json_u64(value: &Value, path: &[&str]) -> Option<u64> {
    let mut current = value;
    for key in path {
        current = current.get(*key)?;
    }
    current.as_u64()
}

fn json_i64(value: &Value, path: &[&str]) -> Option<i64> {
    let mut current = value;
    for key in path {
        current = current.get(*key)?;
    }
    current.as_i64()
}

fn json_f64(value: &Value, path: &[&str]) -> Option<f64> {
    let mut current = value;
    for key in path {
        current = current.get(*key)?;
    }
    current
        .as_f64()
        .or_else(|| current.as_i64().map(|value| value as f64))
        .or_else(|| current.as_u64().map(|value| value as f64))
}

fn json_bool(value: &Value, path: &[&str]) -> Option<bool> {
    let mut current = value;
    for key in path {
        current = current.get(*key)?;
    }
    current.as_bool()
}

fn parse_rfc3339_to_unix(value: &str) -> Option<f64> {
    time::OffsetDateTime::parse(value, &time::format_description::well_known::Rfc3339)
        .ok()
        .map(|dt| dt.unix_timestamp() as f64)
}

fn unix_now_seconds() -> f64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|d| d.as_secs_f64())
        .unwrap_or(0.0)
}

fn bool_value(value: bool) -> f64 {
    if value { 1.0 } else { 0.0 }
}

fn format_prom_value(value: f64) -> String {
    if value.is_infinite() && value.is_sign_positive() {
        "+Inf".to_string()
    } else if value.fract() == 0.0 {
        format!("{value:.0}")
    } else {
        format!("{value:.6}")
            .trim_end_matches('0')
            .trim_end_matches('.')
            .to_string()
    }
}

fn escape_label(value: &str) -> String {
    value
        .replace('\\', "\\\\")
        .replace('\n', "\\n")
        .replace('"', "\\\"")
}

fn escape_help(value: &str) -> String {
    value.replace('\\', "\\\\").replace('\n', "\\n")
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ccr::{
        CcrContentInfo, CcrDigestAlgorithm, RpkiCanonicalCacheRepresentation,
        build_aspa_payload_state, build_roa_payload_state, encode_content_info,
    };
    use crate::data_model::roa::{IpPrefix, RoaAfi};
    use crate::validation::objects::{AspaAttestation, Vrp};
    use tempfile::TempDir;

    #[test]
    fn parse_args_accepts_required_paths() {
        let args = parse_args(&[
            "rpki_inter_rp_metrics".to_string(),
            "--ours-run-root".to_string(),
            "ours".to_string(),
            "--peer-root".to_string(),
            "peers".to_string(),
            "--once".to_string(),
        ])
        .expect("parse");
        assert_eq!(args.ours_run_root, PathBuf::from("ours"));
        assert_eq!(args.peer_root, PathBuf::from("peers"));
        assert!(args.once);
    }

    #[test]
    fn count_csv_rows_ignores_header_comments_and_blanks() {
        let temp = TempDir::new().expect("temp");
        let path = temp.path().join("vrps.csv");
        fs::write(
            &path,
            "# generated\nASN,IP Prefix,Max Length,Trust Anchor\nAS1,192.0.2.0/24,24,ta\n\n",
        )
        .expect("write");
        assert_eq!(count_csv_rows(&path).expect("count"), 1);
    }

    #[test]
    fn count_vrp_csv_uses_unique_vrp_keys() {
        let temp = TempDir::new().expect("temp");
        let path = temp.path().join("vrps.csv");
        fs::write(
            &path,
            "ASN,IP Prefix,Max Length,Trust Anchor,Expires\n\
             AS1,192.0.2.0/24,24,ta-a,100\n\
             AS1,192.0.2.0/24,24,ta-a,100\n\
             AS1,192.0.2.0/24,24,ta-b,200\n\
             AS2,198.51.100.0/24,24,ta-a,100\n",
        )
        .expect("write");
        assert_eq!(count_csv_rows(&path).expect("raw rows"), 4);
        assert_eq!(count_vrp_csv_unique_keys(&path).expect("unique vrps"), 2);
    }

    #[test]
    fn count_vap_csv_uses_unique_vap_keys() {
        let temp = TempDir::new().expect("temp");
        let path = temp.path().join("vaps.csv");
        fs::write(
            &path,
            "Customer ASN,Providers,Trust Anchor\n\
             AS64496,AS64497;AS64498,arin\n\
             AS64496,AS64497;AS64498,ripe\n\
             AS64496,AS64497,arin\n",
        )
        .expect("write");
        assert_eq!(count_csv_rows(&path).expect("raw rows"), 3);
        assert_eq!(count_vap_csv_unique_keys(&path).expect("unique vaps"), 2);
    }

    #[test]
    fn scan_fixture_renders_core_metrics_and_ccr_match() {
        let temp = TempDir::new().expect("temp");
        let ours_run = temp.path().join("ours/runs/run_0001");
        fs::create_dir_all(&ours_run).expect("mkdir");
        fs::write(
            ours_run.join("run-summary.json"),
            r#"{
              "runId":"run_0001",
              "runSeq":1,
              "status":"success",
              "exitCode":0,
              "wallMs":10000,
              "finishedAtRfc3339Utc":"2026-06-09T00:00:00Z",
              "processMetrics":{"maxRssKb":1000},
              "reportCounts":{"vrps":1,"aspas":1,"publicationPoints":1}
            }"#,
        )
        .expect("write summary");
        let ccr = sample_ccr(64496);
        fs::write(ours_run.join("result.ccr"), &ccr).expect("write ccr");

        let peer_root = temp.path().join("peers");
        let routinator = peer_root.join("routinator/latest");
        fs::create_dir_all(&routinator).expect("mkdir");
        fs::write(
            routinator.join("run-meta.json"),
            r#"{"runId":"run_0001","runSeq":1,"success":true,"wallMs":9000,"finishedAtRfc3339Utc":"2026-06-09T00:00:01Z","maxRssKb":{"aggregatePeak":900}}"#,
        )
        .expect("write meta");
        fs::write(
            routinator.join("vrps.csv"),
            "ASN,IP Prefix,Max Length,Trust Anchor\nAS64496,192.0.2.0/24,24,ta\n",
        )
        .expect("write vrps");
        fs::write(
            routinator.join("vaps.csv"),
            "Customer ASN,Providers,Trust Anchor\nAS64496,AS64497,ta\n",
        )
        .expect("write vaps");

        let rpki_client = peer_root.join("rpki-client/latest");
        fs::create_dir_all(&rpki_client).expect("mkdir");
        fs::write(
            rpki_client.join("run-meta.json"),
            r#"{"runId":"run_0001","runSeq":1,"success":true,"wallMs":8000,"finishedAtRfc3339Utc":"2026-06-09T00:00:02Z","maxRssKb":{"aggregatePeak":800},"artifacts":{"ccr":"result.ccr"},"counts":{"vrps":1,"vaps":1}}"#,
        )
        .expect("write meta");
        fs::write(rpki_client.join("result.ccr"), &ccr).expect("write ccr");
        fs::write(
            peer_root.join("sync-status.json"),
            r#"{"success":true,"lastSyncAtRfc3339Utc":"2026-06-09T00:00:03Z","remoteHost":"remote200"}"#,
        )
        .expect("write sync");

        let args = Args {
            ours_run_root: temp.path().join("ours"),
            peer_root,
            listen: "127.0.0.1:0".to_string(),
            poll_secs: 1,
            instance: "test".to_string(),
            once: true,
            out_metrics: None,
            out_status: None,
        };
        let snapshot = scan(&args).expect("scan");
        let metrics = render_metrics(&snapshot);
        assert!(metrics.contains("inter_rp_vrps{instance=\"test\",rp=\"ours-rp\"} 1"));
        assert!(metrics.contains("inter_rp_vaps{instance=\"test\",rp=\"rpki-client\"} 1"));
        assert!(metrics.contains(
            "inter_rp_ccr_digest_match{instance=\"test\",left=\"ours-rp\",right=\"rpki-client\",state=\"overall\"} 1"
        ));
        assert!(metrics.contains(
            "inter_rp_vrps_diff{instance=\"test\",left=\"routinator\",right=\"rpki-client\"} 0"
        ));
    }

    fn sample_ccr(customer_asn: u32) -> Vec<u8> {
        let vrps = build_roa_payload_state(&[Vrp {
            asn: customer_asn,
            prefix: IpPrefix {
                afi: RoaAfi::Ipv4,
                prefix_len: 24,
                addr: [192, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
            },
            max_length: 24,
        }])
        .expect("build vrps");
        let vaps = build_aspa_payload_state(&[AspaAttestation {
            customer_as_id: customer_asn,
            provider_as_ids: vec![64497],
        }])
        .expect("build vaps");
        let content = CcrContentInfo::new(RpkiCanonicalCacheRepresentation {
            version: 0,
            hash_alg: CcrDigestAlgorithm::Sha256,
            produced_at: time::OffsetDateTime::UNIX_EPOCH,
            mfts: None,
            vrps: Some(vrps),
            vaps: Some(vaps),
            tas: None,
            rks: None,
        });
        encode_content_info(&content).expect("encode")
    }
}
