use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::path::{Path, PathBuf};

use rpki::cir::decode_cir;
use serde_json::json;
use sha2::{Digest, Sha256};

#[derive(Debug, Default, PartialEq, Eq)]
struct Args {
    ours_cir: Option<PathBuf>,
    peer_cir: Option<PathBuf>,
    cache_root: Option<PathBuf>,
    rpki_client_log: Option<PathBuf>,
    out_json: Option<PathBuf>,
    sample_limit: usize,
}

fn usage() -> &'static str {
    "Usage: cir_probe_rpki_client_cache --ours-cir <path> --rpki-client-cir <path> --cache-root <path> --out-json <path> [--rpki-client-log <path>] [--sample-limit <n>]"
}

fn main() {
    if let Err(err) = real_main() {
        eprintln!("{err}");
        std::process::exit(1);
    }
}

fn real_main() -> Result<(), String> {
    run(parse_args(&std::env::args().collect::<Vec<_>>())?)
}

fn parse_args(argv: &[String]) -> Result<Args, String> {
    let mut args = Args {
        sample_limit: 20,
        ..Args::default()
    };
    let mut index = 1usize;
    while index < argv.len() {
        match argv[index].as_str() {
            "--ours-cir" => {
                index += 1;
                args.ours_cir = Some(PathBuf::from(
                    argv.get(index).ok_or("--ours-cir requires a value")?,
                ));
            }
            "--rpki-client-cir" | "--peer-cir" => {
                index += 1;
                args.peer_cir = Some(PathBuf::from(
                    argv.get(index)
                        .ok_or("--rpki-client-cir requires a value")?,
                ));
            }
            "--cache-root" => {
                index += 1;
                args.cache_root = Some(PathBuf::from(
                    argv.get(index).ok_or("--cache-root requires a value")?,
                ));
            }
            "--rpki-client-log" => {
                index += 1;
                args.rpki_client_log = Some(PathBuf::from(
                    argv.get(index)
                        .ok_or("--rpki-client-log requires a value")?,
                ));
            }
            "--out-json" => {
                index += 1;
                args.out_json = Some(PathBuf::from(
                    argv.get(index).ok_or("--out-json requires a value")?,
                ));
            }
            "--sample-limit" => {
                index += 1;
                let value = argv.get(index).ok_or("--sample-limit requires a value")?;
                args.sample_limit = value
                    .parse::<usize>()
                    .map_err(|_| format!("invalid --sample-limit: {value}"))?;
            }
            "-h" | "--help" => return Err(usage().to_string()),
            other => return Err(format!("unknown argument: {other}\n{}", usage())),
        }
        index += 1;
    }
    if args.ours_cir.is_none() {
        return Err(format!("--ours-cir is required\n{}", usage()));
    }
    if args.peer_cir.is_none() {
        return Err(format!("--rpki-client-cir is required\n{}", usage()));
    }
    if args.cache_root.is_none() {
        return Err(format!("--cache-root is required\n{}", usage()));
    }
    if args.out_json.is_none() {
        return Err(format!("--out-json is required\n{}", usage()));
    }
    Ok(args)
}

fn run(args: Args) -> Result<(), String> {
    let ours_cir_path = args.ours_cir.as_ref().expect("validated");
    let peer_cir_path = args.peer_cir.as_ref().expect("validated");
    let cache_root = args.cache_root.as_ref().expect("validated");
    let out_json = args.out_json.as_ref().expect("validated");

    let ours = decode_cir(&read_file(ours_cir_path)?)
        .map_err(|e| format!("decode ours CIR failed: {e}"))?;
    let peer = decode_cir(&read_file(peer_cir_path)?)
        .map_err(|e| format!("decode rpki-client CIR failed: {e}"))?;
    let peer_objects = peer
        .validated_objects()
        .map(|item| item.rsync_uri.as_str())
        .collect::<BTreeSet<_>>();
    let only_in_ours = ours
        .validated_objects()
        .filter(|item| !peer_objects.contains(item.rsync_uri.as_str()))
        .map(|item| ProbeObject {
            uri: item.rsync_uri.clone(),
            sha256_hex: hex::encode(&item.sha256),
        })
        .collect::<Vec<_>>();

    let rrdp_dirs = list_rrdp_dirs(cache_root)?;
    let log_mentions = match args.rpki_client_log.as_ref() {
        Some(path) => build_log_mentions(path, &only_in_ours)?,
        None => LogMentions::default(),
    };

    let mut cache_hash_match_count = 0usize;
    let mut cache_hash_mismatch_count = 0usize;
    let mut cache_missing_count = 0usize;
    let mut valid_path_exists_count = 0usize;
    let mut rrdp_temp_path_exists_count = 0usize;
    let mut rsync_temp_path_exists_count = 0usize;
    let mut log_mention_count = 0usize;
    let mut samples = Vec::new();
    let mut missing_uris = Vec::new();
    let mut matched_uris = Vec::new();
    let mut mismatched_uris = Vec::new();
    let mut log_mentioned_uris = Vec::new();

    for object in &only_in_ours {
        let probe = probe_cache(cache_root, &rrdp_dirs, object)?;
        if probe.hash_match {
            cache_hash_match_count += 1;
            matched_uris.push(object.uri.as_str());
        } else if probe.exists {
            cache_hash_mismatch_count += 1;
            mismatched_uris.push(object.uri.as_str());
        } else {
            cache_missing_count += 1;
            missing_uris.push(object.uri.as_str());
        }
        if probe.valid_path_exists {
            valid_path_exists_count += 1;
        }
        if probe.rrdp_temp_path_exists {
            rrdp_temp_path_exists_count += 1;
        }
        if probe.rsync_temp_path_exists {
            rsync_temp_path_exists_count += 1;
        }
        let log_mention = log_mentions.mentioned.contains(object.uri.as_str());
        if log_mention {
            log_mention_count += 1;
            log_mentioned_uris.push(object.uri.as_str());
        }
        if samples.len() < args.sample_limit {
            samples.push(json!({
                "uri": object.uri,
                "sha256": object.sha256_hex,
                "cacheStatus": probe.status(),
                "cacheLocations": probe.locations,
                "logMentionedAsSuperfluousOrDeleted": log_mention,
            }));
        }
    }

    let summary = json!({
        "onlyInOursTotal": only_in_ours.len(),
        "cacheRoot": cache_root,
        "rpkiClientLog": args.rpki_client_log,
        "rrdpCacheDirs": rrdp_dirs.len(),
        "cacheProbe": {
            "hashMatchCount": cache_hash_match_count,
            "hashMismatchCount": cache_hash_mismatch_count,
            "missingCount": cache_missing_count,
            "validPathExistsCount": valid_path_exists_count,
            "rrdpTempPathExistsCount": rrdp_temp_path_exists_count,
            "rsyncTempPathExistsCount": rsync_temp_path_exists_count,
            "hashMatchByExtension": group_by_extension(matched_uris.iter().copied()),
            "missingByExtension": group_by_extension(missing_uris.iter().copied()),
            "hashMismatchByExtension": group_by_extension(mismatched_uris.iter().copied()),
            "hashMatchByHostTop": top_hosts(matched_uris.iter().copied(), args.sample_limit),
            "missingByHostTop": top_hosts(missing_uris.iter().copied(), args.sample_limit),
            "hashMismatchByHostTop": top_hosts(mismatched_uris.iter().copied(), args.sample_limit),
        },
        "logProbe": {
            "enabled": args.rpki_client_log.is_some(),
            "relevantLineCount": log_mentions.relevant_line_count,
            "mentionedCount": log_mention_count,
            "mentionedByExtension": group_by_extension(log_mentioned_uris.iter().copied()),
            "mentionedByHostTop": top_hosts(log_mentioned_uris.iter().copied(), args.sample_limit),
        },
        "samples": samples,
    });

    write_json(out_json, &summary)?;
    println!("{}", out_json.display());
    Ok(())
}

#[derive(Clone, Debug)]
struct ProbeObject {
    uri: String,
    sha256_hex: String,
}

#[derive(Default, Debug)]
struct CacheProbe {
    exists: bool,
    hash_match: bool,
    valid_path_exists: bool,
    rrdp_temp_path_exists: bool,
    rsync_temp_path_exists: bool,
    locations: Vec<serde_json::Value>,
}

impl CacheProbe {
    fn status(&self) -> &'static str {
        if self.hash_match {
            "hash_match"
        } else if self.exists {
            "hash_mismatch"
        } else {
            "missing"
        }
    }
}

fn probe_cache(
    cache_root: &Path,
    rrdp_dirs: &[PathBuf],
    object: &ProbeObject,
) -> Result<CacheProbe, String> {
    let Some(stripped) = strip_rsync_uri(&object.uri) else {
        return Ok(CacheProbe::default());
    };
    let mut probe = CacheProbe::default();
    let mut candidates = vec![
        ("valid", cache_root.join(stripped)),
        ("rsync_temp", cache_root.join(".rsync").join(stripped)),
    ];
    for dir in rrdp_dirs {
        candidates.push(("rrdp_temp", dir.join(stripped)));
    }

    for (kind, path) in candidates {
        if !path.is_file() {
            continue;
        }
        let sha256_hex = sha256_file_hex(&path)?;
        let hash_matches = sha256_hex == object.sha256_hex;
        probe.exists = true;
        probe.hash_match |= hash_matches;
        match kind {
            "valid" => probe.valid_path_exists = true,
            "rsync_temp" => probe.rsync_temp_path_exists = true,
            "rrdp_temp" => probe.rrdp_temp_path_exists = true,
            _ => {}
        }
        if probe.locations.len() < 4 {
            probe.locations.push(json!({
                "kind": kind,
                "path": path,
                "sha256": sha256_hex,
                "hashMatches": hash_matches,
            }));
        }
    }

    Ok(probe)
}

#[derive(Default, Debug)]
struct LogMentions {
    mentioned: BTreeSet<String>,
    relevant_line_count: usize,
}

fn build_log_mentions(path: &Path, objects: &[ProbeObject]) -> Result<LogMentions, String> {
    let content = std::fs::read_to_string(path)
        .map_err(|e| format!("read log failed: {}: {e}", path.display()))?;
    let mut by_file_name: HashMap<String, Vec<(&str, &str)>> = HashMap::new();
    for object in objects {
        let Some(stripped) = strip_rsync_uri(&object.uri) else {
            continue;
        };
        by_file_name
            .entry(file_name(stripped).to_string())
            .or_default()
            .push((object.uri.as_str(), stripped));
    }

    let mut mentioned = BTreeSet::new();
    let mut relevant_line_count = 0usize;
    for line in content.lines() {
        if !is_relevant_cleanup_line(line) {
            continue;
        }
        relevant_line_count += 1;
        let Some(name) = line.split('/').next_back().map(trim_log_token) else {
            continue;
        };
        let Some(candidates) = by_file_name.get(name) else {
            continue;
        };
        for (uri, stripped) in candidates {
            if line.contains(stripped) {
                mentioned.insert((*uri).to_string());
            }
        }
    }

    Ok(LogMentions {
        mentioned,
        relevant_line_count,
    })
}

fn is_relevant_cleanup_line(line: &str) -> bool {
    line.contains("superfluous")
        || line.contains("deleted ")
        || line.contains("deleted superfluous")
        || line.contains("bad message digest")
        || line.contains("referenced file supposed to be deleted")
}

fn trim_log_token(token: &str) -> &str {
    token.trim_matches(|ch: char| ch == ':' || ch == ',' || ch == ')' || ch == '(')
}

fn list_rrdp_dirs(cache_root: &Path) -> Result<Vec<PathBuf>, String> {
    let rrdp = cache_root.join(".rrdp");
    let mut dirs = Vec::new();
    let Ok(read_dir) = std::fs::read_dir(&rrdp) else {
        return Ok(dirs);
    };
    for entry in read_dir {
        let entry = entry.map_err(|e| format!("read .rrdp entry failed: {e}"))?;
        let path = entry.path();
        if path.is_dir() {
            dirs.push(path);
        }
    }
    dirs.sort();
    Ok(dirs)
}

fn sha256_file_hex(path: &Path) -> Result<String, String> {
    let bytes = std::fs::read(path)
        .map_err(|e| format!("read cache file failed: {}: {e}", path.display()))?;
    Ok(hex::encode(Sha256::digest(&bytes)))
}

fn read_file(path: &Path) -> Result<Vec<u8>, String> {
    std::fs::read(path).map_err(|e| format!("read file failed: {}: {e}", path.display()))
}

fn write_json(path: &Path, value: &serde_json::Value) -> Result<(), String> {
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent)
            .map_err(|e| format!("create parent dirs failed: {}: {e}", parent.display()))?;
    }
    std::fs::write(
        path,
        serde_json::to_vec_pretty(value).map_err(|e| e.to_string())?,
    )
    .map_err(|e| format!("write json failed: {}: {e}", path.display()))
}

fn strip_rsync_uri(uri: &str) -> Option<&str> {
    uri.strip_prefix("rsync://")
}

fn file_name(path: &str) -> &str {
    path.rsplit('/').next().unwrap_or(path)
}

fn group_by_extension<'a>(uris: impl IntoIterator<Item = &'a str>) -> BTreeMap<String, usize> {
    let mut counts = BTreeMap::new();
    for uri in uris {
        *counts.entry(uri_extension(uri)).or_insert(0) += 1;
    }
    counts
}

fn top_hosts<'a>(uris: impl IntoIterator<Item = &'a str>, limit: usize) -> Vec<serde_json::Value> {
    let mut counts = BTreeMap::new();
    for uri in uris {
        *counts.entry(uri_host(uri)).or_insert(0usize) += 1;
    }
    let mut rows = counts.into_iter().collect::<Vec<_>>();
    rows.sort_by(|(host_a, count_a), (host_b, count_b)| {
        count_b.cmp(count_a).then_with(|| host_a.cmp(host_b))
    });
    rows.into_iter()
        .take(limit)
        .map(|(host, count)| {
            json!({
                "host": host,
                "count": count,
            })
        })
        .collect()
}

fn uri_host(uri: &str) -> String {
    let without_scheme = uri.split_once("://").map(|(_, rest)| rest).unwrap_or(uri);
    without_scheme
        .split('/')
        .next()
        .filter(|host| !host.is_empty())
        .unwrap_or("<unknown>")
        .to_string()
}

fn uri_extension(uri: &str) -> String {
    let path = uri.split_once("://").map(|(_, rest)| rest).unwrap_or(uri);
    let path = path.split_once('/').map(|(_, path)| path).unwrap_or(path);
    let file = path.rsplit('/').next().unwrap_or(path);
    if let Some((_, ext)) = file.rsplit_once('.')
        && !ext.is_empty()
    {
        return format!(".{}", ext.to_ascii_lowercase());
    }
    "<none>".to_string()
}

#[cfg(test)]
mod tests {
    use super::*;
    use rpki::cir::{CanonicalInputRepresentation, CirObject, CirTrustAnchor, encode_cir};

    #[test]
    fn parse_args_accepts_required_flags() {
        let args = parse_args(&[
            "cir_probe_rpki_client_cache".to_string(),
            "--ours-cir".to_string(),
            "ours.cir".to_string(),
            "--rpki-client-cir".to_string(),
            "peer.cir".to_string(),
            "--cache-root".to_string(),
            "cache".to_string(),
            "--out-json".to_string(),
            "probe.json".to_string(),
            "--sample-limit".to_string(),
            "7".to_string(),
        ])
        .expect("parse args");
        assert_eq!(args.ours_cir.as_deref(), Some(Path::new("ours.cir")));
        assert_eq!(args.peer_cir.as_deref(), Some(Path::new("peer.cir")));
        assert_eq!(args.cache_root.as_deref(), Some(Path::new("cache")));
        assert_eq!(args.out_json.as_deref(), Some(Path::new("probe.json")));
        assert_eq!(args.sample_limit, 7);
    }

    #[test]
    fn parse_args_rejects_missing_invalid_and_unknown_flags() {
        let missing = parse_args(&[
            "cir_probe_rpki_client_cache".to_string(),
            "--ours-cir".to_string(),
            "ours.cir".to_string(),
            "--peer-cir".to_string(),
            "peer.cir".to_string(),
            "--cache-root".to_string(),
            "cache".to_string(),
        ])
        .unwrap_err();
        assert!(missing.contains("--out-json is required"), "{missing}");

        let invalid_limit = parse_args(&[
            "cir_probe_rpki_client_cache".to_string(),
            "--ours-cir".to_string(),
            "ours.cir".to_string(),
            "--peer-cir".to_string(),
            "peer.cir".to_string(),
            "--cache-root".to_string(),
            "cache".to_string(),
            "--out-json".to_string(),
            "probe.json".to_string(),
            "--sample-limit".to_string(),
            "NaN".to_string(),
        ])
        .unwrap_err();
        assert!(
            invalid_limit.contains("invalid --sample-limit"),
            "{invalid_limit}"
        );

        let unknown = parse_args(&[
            "cir_probe_rpki_client_cache".to_string(),
            "--unexpected".to_string(),
        ])
        .unwrap_err();
        assert!(unknown.contains("unknown argument"), "{unknown}");
    }

    #[test]
    fn run_probes_valid_temp_rrdp_missing_and_log_mentions() {
        let temp = tempfile::tempdir().expect("tempdir");
        let cache_root = temp.path().join("cache");
        let valid_bytes = b"valid object bytes";
        let rrdp_bytes = b"rrdp object bytes";
        let expected_mismatch_bytes = b"expected mismatch bytes";

        write_cache_file(
            &cache_root.join("rpki.example.test/repo/a.roa"),
            valid_bytes,
        );
        write_cache_file(
            &cache_root.join(".rsync/rpki.example.test/repo/b.mft"),
            b"different bytes",
        );
        write_cache_file(
            &cache_root.join(".rrdp/session-1/rpki.example.test/repo/c.cer"),
            rrdp_bytes,
        );

        let ours_cir = sample_cir(&[
            (
                "rsync://missing.example.test/repo/e.crl",
                b"missing bytes".as_slice(),
            ),
            (
                "rsync://rpki.example.test/repo/a.roa",
                valid_bytes.as_slice(),
            ),
            (
                "rsync://rpki.example.test/repo/b.mft",
                expected_mismatch_bytes.as_slice(),
            ),
            (
                "rsync://rpki.example.test/repo/c.cer",
                rrdp_bytes.as_slice(),
            ),
            (
                "rsync://shared.example.test/repo/d.roa",
                b"shared".as_slice(),
            ),
        ]);
        let peer_cir = sample_cir(&[(
            "rsync://shared.example.test/repo/d.roa",
            b"shared".as_slice(),
        )]);
        let ours = temp.path().join("ours.cir");
        let peer = temp.path().join("peer.cir");
        write_cir(&ours, &ours_cir);
        write_cir(&peer, &peer_cir);

        let log = temp.path().join("rpki-client.log");
        std::fs::write(
            &log,
            "rpki-client: deleted superfluous /cache/rpki.example.test/repo/b.mft\n",
        )
        .expect("write log");
        let out_json = temp.path().join("nested/probe.json");

        run(Args {
            ours_cir: Some(ours),
            peer_cir: Some(peer),
            cache_root: Some(cache_root),
            rpki_client_log: Some(log),
            out_json: Some(out_json.clone()),
            sample_limit: 10,
        })
        .expect("run");

        let summary: serde_json::Value =
            serde_json::from_slice(&std::fs::read(out_json).expect("read summary"))
                .expect("summary json");
        assert_eq!(summary["onlyInOursTotal"], 4);
        assert_eq!(summary["rrdpCacheDirs"], 1);
        assert_eq!(summary["cacheProbe"]["hashMatchCount"], 2);
        assert_eq!(summary["cacheProbe"]["hashMismatchCount"], 1);
        assert_eq!(summary["cacheProbe"]["missingCount"], 1);
        assert_eq!(summary["cacheProbe"]["validPathExistsCount"], 1);
        assert_eq!(summary["cacheProbe"]["rsyncTempPathExistsCount"], 1);
        assert_eq!(summary["cacheProbe"]["rrdpTempPathExistsCount"], 1);
        assert_eq!(summary["logProbe"]["enabled"], true);
        assert_eq!(summary["logProbe"]["relevantLineCount"], 1);
        assert_eq!(summary["logProbe"]["mentionedCount"], 1);
        assert_eq!(summary["samples"].as_array().unwrap().len(), 4);
    }

    #[test]
    fn run_reports_decode_failure_with_side_label() {
        let temp = tempfile::tempdir().expect("tempdir");
        let ours = temp.path().join("ours.cir");
        let peer = temp.path().join("peer.cir");
        std::fs::write(&ours, b"not a cir").expect("write invalid");
        write_cir(
            &peer,
            &sample_cir(&[(
                "rsync://shared.example.test/repo/d.roa",
                b"shared".as_slice(),
            )]),
        );
        let err = run(Args {
            ours_cir: Some(ours),
            peer_cir: Some(peer),
            cache_root: Some(temp.path().join("cache")),
            rpki_client_log: None,
            out_json: Some(temp.path().join("probe.json")),
            sample_limit: 20,
        })
        .unwrap_err();
        assert!(err.contains("decode ours CIR failed"), "{err}");
    }

    #[test]
    fn uri_helpers_extract_rsync_path_host_and_extension() {
        let uri = "rsync://rpki.example.test/repo/a/b.ROA";
        assert_eq!(strip_rsync_uri(uri), Some("rpki.example.test/repo/a/b.ROA"));
        assert_eq!(uri_host(uri), "rpki.example.test");
        assert_eq!(uri_extension(uri), ".roa");
        assert_eq!(file_name("rpki.example.test/repo/a/b.ROA"), "b.ROA");
    }

    #[test]
    fn log_mentions_match_cleanup_lines_by_uri_suffix() {
        let temp = tempfile::tempdir().expect("tempdir");
        let log = temp.path().join("run.log");
        std::fs::write(
            &log,
            "rpki-client: deleted superfluous .rrdp/abc/rpki.example.test/repo/a.roa\n",
        )
        .expect("write log");
        let objects = vec![ProbeObject {
            uri: "rsync://rpki.example.test/repo/a.roa".to_string(),
            sha256_hex: "00".repeat(32),
        }];
        let mentions = build_log_mentions(&log, &objects).expect("mentions");
        assert_eq!(mentions.relevant_line_count, 1);
        assert!(
            mentions
                .mentioned
                .contains("rsync://rpki.example.test/repo/a.roa")
        );
    }

    fn sample_cir(objects: &[(&str, &[u8])]) -> CanonicalInputRepresentation {
        let mut objects = objects
            .iter()
            .map(|(rsync_uri, bytes)| CirObject {
                rsync_uri: (*rsync_uri).to_string(),
                sha256: Sha256::digest(bytes).to_vec(),
            })
            .collect::<Vec<_>>();
        objects.sort_by(|left, right| left.rsync_uri.cmp(&right.rsync_uri));
        CanonicalInputRepresentation::new_v4(
            time::OffsetDateTime::UNIX_EPOCH,
            objects,
            Vec::new(),
            vec![sample_trust_anchor()],
            Vec::new(),
            Vec::new(),
        )
    }

    fn sample_trust_anchor() -> CirTrustAnchor {
        let ta_rsync_uri = "rsync://example.test/ta.cer";
        let ta_certificate_der = b"ta-der".to_vec();
        CirTrustAnchor {
            ta_rsync_uri: ta_rsync_uri.to_string(),
            tal_uri: "https://tal.example.test/apnic.tal".to_string(),
            tal_bytes: format!("{ta_rsync_uri}\n\nAQID\n").into_bytes(),
            ta_certificate_sha256: Sha256::digest(&ta_certificate_der).to_vec(),
            ta_certificate_der,
        }
    }

    fn write_cir(path: &Path, cir: &CanonicalInputRepresentation) {
        std::fs::write(path, encode_cir(cir).expect("encode cir")).expect("write cir");
    }

    fn write_cache_file(path: &Path, bytes: &[u8]) {
        std::fs::create_dir_all(path.parent().expect("parent")).expect("mkdir");
        std::fs::write(path, bytes).expect("write cache file");
    }
}
