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

use rpki::blob_store::ExternalRepoBytesDb;
use rpki::ccr::decode_ccr_compare_views;
use rpki::ccr::decode_content_info;
use rpki::cir::decode_cir;
use rpki::data_model::roa::RoaObject;

const USAGE: &str = "Usage: cir_drop_report --cir <path> --ccr <path> --report-json <path> --repo-bytes-db <path> --json-out <path> --md-out <path>";

#[derive(serde::Serialize)]
struct DroppedObjectRecord {
    uri: String,
    sha256: String,
    kind: String,
    reason_code: String,
    reason_text: Option<String>,
    publication_point: Option<String>,
    manifest_uri: Option<String>,
    derived_vrp_count: usize,
}

fn classify_reason(detail: Option<&str>, result: &str) -> String {
    let text = detail.unwrap_or("").to_ascii_lowercase();
    if text.contains("fetch") {
        "fetch_failed".to_string()
    } else if text.contains("manifest") {
        "manifest_invalid".to_string()
    } else if text.contains("crl") {
        "crl_invalid".to_string()
    } else if text.contains("policy") {
        "policy_rejected".to_string()
    } else if text.contains("parse") {
        "object_parse_failed".to_string()
    } else if text.contains("signature") || text.contains("cms") {
        "cms_signature_invalid".to_string()
    } else if text.contains("resource") {
        "resource_invalid".to_string()
    } else if text.contains("expired") || text.contains("not yet valid") {
        "expired_or_not_yet_valid".to_string()
    } else if result == "skipped" {
        "skipped".to_string()
    } else if result == "error" {
        "error".to_string()
    } else {
        "other".to_string()
    }
}

fn parse_args(
    argv: &[String],
) -> Result<(PathBuf, PathBuf, PathBuf, PathBuf, PathBuf, PathBuf), String> {
    let mut cir = None;
    let mut ccr = None;
    let mut report = None;
    let mut repo_bytes_db = None;
    let mut json_out = None;
    let mut md_out = None;
    let mut i = 1usize;
    while i < argv.len() {
        match argv[i].as_str() {
            "--cir" => {
                i += 1;
                cir = Some(PathBuf::from(argv.get(i).ok_or("--cir requires a value")?));
            }
            "--ccr" => {
                i += 1;
                ccr = Some(PathBuf::from(argv.get(i).ok_or("--ccr requires a value")?));
            }
            "--report-json" => {
                i += 1;
                report = Some(PathBuf::from(
                    argv.get(i).ok_or("--report-json requires a value")?,
                ));
            }
            "--repo-bytes-db" => {
                i += 1;
                repo_bytes_db = Some(PathBuf::from(
                    argv.get(i).ok_or("--repo-bytes-db requires a value")?,
                ));
            }
            "--json-out" => {
                i += 1;
                json_out = Some(PathBuf::from(
                    argv.get(i).ok_or("--json-out requires a value")?,
                ));
            }
            "--md-out" => {
                i += 1;
                md_out = Some(PathBuf::from(
                    argv.get(i).ok_or("--md-out requires a value")?,
                ));
            }
            "-h" | "--help" => return Err(USAGE.to_string()),
            other => return Err(format!("unknown argument: {other}\n\n{USAGE}")),
        }
        i += 1;
    }
    Ok((
        cir.ok_or_else(|| format!("--cir is required\n\n{USAGE}"))?,
        ccr.ok_or_else(|| format!("--ccr is required\n\n{USAGE}"))?,
        report.ok_or_else(|| format!("--report-json is required\n\n{USAGE}"))?,
        repo_bytes_db.ok_or_else(|| format!("--repo-bytes-db is required\n\n{USAGE}"))?,
        json_out.ok_or_else(|| format!("--json-out is required\n\n{USAGE}"))?,
        md_out.ok_or_else(|| format!("--md-out is required\n\n{USAGE}"))?,
    ))
}

fn main() -> Result<(), String> {
    let argv: Vec<String> = std::env::args().collect();
    let (cir_path, ccr_path, report_path, repo_bytes_db, json_out, md_out) = parse_args(&argv)?;

    let cir = decode_cir(&std::fs::read(&cir_path).map_err(|e| format!("read cir failed: {e}"))?)
        .map_err(|e| format!("decode cir failed: {e}"))?;
    let ccr = decode_content_info(
        &std::fs::read(&ccr_path).map_err(|e| format!("read ccr failed: {e}"))?,
    )
    .map_err(|e| format!("decode ccr failed: {e}"))?;
    let (vrps, vaps) =
        decode_ccr_compare_views(&ccr).map_err(|e| format!("decode compare views failed: {e}"))?;
    let report: serde_json::Value = serde_json::from_slice(
        &std::fs::read(&report_path).map_err(|e| format!("read report failed: {e}"))?,
    )
    .map_err(|e| format!("parse report failed: {e}"))?;
    let repo_bytes = ExternalRepoBytesDb::open(&repo_bytes_db)
        .map_err(|e| format!("open repo bytes db failed: {e}"))?;

    let mut object_hash_by_uri = BTreeMap::new();
    for object in cir.validated_objects() {
        object_hash_by_uri.insert(object.rsync_uri.clone(), hex::encode(&object.sha256));
    }

    let publication_points = report["publication_points"]
        .as_array()
        .ok_or("report.publication_points must be an array")?;

    let mut dropped_objects = Vec::new();
    let mut dropped_vrp_rows = BTreeSet::new();
    let mut dropped_by_kind: BTreeMap<String, usize> = BTreeMap::new();
    let mut dropped_by_reason: BTreeMap<String, usize> = BTreeMap::new();
    let mut unknown_roa_objects = 0usize;

    for pp in publication_points {
        let publication_point = pp["publication_point_rsync_uri"]
            .as_str()
            .map(str::to_string);
        let manifest_uri = pp["manifest_rsync_uri"].as_str().map(str::to_string);
        for obj in pp["objects"].as_array().into_iter().flatten() {
            let result = obj["result"].as_str().unwrap_or("unknown");
            if result == "ok" {
                continue;
            }
            let uri = obj["rsync_uri"].as_str().unwrap_or("").to_string();
            let hash = obj["sha256_hex"]
                .as_str()
                .map(str::to_string)
                .or_else(|| object_hash_by_uri.get(&uri).cloned())
                .unwrap_or_default();
            let kind = obj["kind"].as_str().unwrap_or("other").to_string();
            let detail = obj["detail"].as_str().map(str::to_string);
            let reason_code = classify_reason(detail.as_deref(), result);
            *dropped_by_kind.entry(kind.clone()).or_insert(0) += 1;
            *dropped_by_reason.entry(reason_code.clone()).or_insert(0) += 1;

            let mut derived_vrp_count = 0usize;
            if kind == "roa" && !hash.is_empty() {
                let bytes_opt = repo_bytes.get_blob_bytes(&hash).ok().flatten();
                match bytes_opt {
                    Some(bytes) => {
                        if let Ok(roa) = RoaObject::decode_der(&bytes) {
                            for family in roa.roa.ip_addr_blocks {
                                for addr in family.addresses {
                                    let prefix = match addr.prefix.afi {
                                        rpki::data_model::roa::RoaAfi::Ipv4 => format!(
                                            "{}.{}.{}.{}/{}",
                                            addr.prefix.addr[0],
                                            addr.prefix.addr[1],
                                            addr.prefix.addr[2],
                                            addr.prefix.addr[3],
                                            addr.prefix.prefix_len
                                        ),
                                        rpki::data_model::roa::RoaAfi::Ipv6 => {
                                            let bytes: [u8; 16] = addr.prefix.addr;
                                            format!(
                                                "{}/{}",
                                                std::net::Ipv6Addr::from(bytes),
                                                addr.prefix.prefix_len
                                            )
                                        }
                                    };
                                    let max_len = addr.max_length.unwrap_or(addr.prefix.prefix_len);
                                    dropped_vrp_rows.insert((roa.roa.as_id, prefix, max_len));
                                    derived_vrp_count += 1;
                                }
                            }
                        } else {
                            unknown_roa_objects += 1;
                        }
                    }
                    None => unknown_roa_objects += 1,
                }
            }

            dropped_objects.push(DroppedObjectRecord {
                uri,
                sha256: hash,
                kind,
                reason_code,
                reason_text: detail,
                publication_point: publication_point.clone(),
                manifest_uri: manifest_uri.clone(),
                derived_vrp_count,
            });
        }
    }

    let output = serde_json::json!({
        "summary": {
            "finalVrpCount": vrps.len(),
            "finalVapCount": vaps.len(),
            "droppedVrpCount": dropped_vrp_rows.len(),
            "droppedObjectCount": dropped_objects.len(),
            "droppedByKind": dropped_by_kind,
            "droppedByReason": dropped_by_reason,
            "unknownDroppedRoaObjects": unknown_roa_objects,
        },
        "objects": dropped_objects,
    });

    if let Some(parent) = json_out.parent() {
        std::fs::create_dir_all(parent).map_err(|e| format!("create json parent failed: {e}"))?;
    }
    std::fs::write(&json_out, serde_json::to_vec_pretty(&output).unwrap())
        .map_err(|e| format!("write json failed: {e}"))?;

    let mut md = String::new();
    md.push_str("# CIR Drop Report\n\n");
    md.push_str(&format!("- `final_vrp_count`: `{}`\n", vrps.len()));
    md.push_str(&format!("- `final_vap_count`: `{}`\n", vaps.len()));
    md.push_str(&format!(
        "- `dropped_vrp_count`: `{}`\n",
        output["summary"]["droppedVrpCount"]
    ));
    md.push_str(&format!(
        "- `dropped_object_count`: `{}`\n",
        output["summary"]["droppedObjectCount"]
    ));
    md.push_str(&format!(
        "- `unknown_dropped_roa_objects`: `{}`\n\n",
        output["summary"]["unknownDroppedRoaObjects"]
    ));
    md.push_str("## Dropped By Kind\n\n");
    for (kind, count) in output["summary"]["droppedByKind"]
        .as_object()
        .into_iter()
        .flatten()
    {
        md.push_str(&format!("- `{kind}`: `{}`\n", count.as_u64().unwrap_or(0)));
    }
    md.push_str("\n## Dropped By Reason\n\n");
    for (reason, count) in output["summary"]["droppedByReason"]
        .as_object()
        .into_iter()
        .flatten()
    {
        md.push_str(&format!(
            "- `{reason}`: `{}`\n",
            count.as_u64().unwrap_or(0)
        ));
    }
    if let Some(parent) = md_out.parent() {
        std::fs::create_dir_all(parent)
            .map_err(|e| format!("create markdown parent failed: {e}"))?;
    }
    std::fs::write(&md_out, md).map_err(|e| format!("write markdown failed: {e}"))?;

    Ok(())
}
