#[path = "benchmark/sap.rs"]
mod sap;

use serde::Serialize;
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};

#[derive(Clone, Debug, Default)]
struct Sizes {
    vals: Vec<u64>,
    total: u64,
}

impl Sizes {
    fn push(&mut self, v: u64) {
        self.vals.push(v);
        self.total = self.total.saturating_add(v);
    }

    fn summarize(&mut self) -> Option<SizesSummary> {
        if self.vals.is_empty() {
            return None;
        }
        self.vals.sort_unstable();
        let min = *self.vals.first().unwrap();
        let max = *self.vals.last().unwrap();
        let p50 = percentile(&self.vals, 0.50);
        let p90 = percentile(&self.vals, 0.90);
        let p99 = percentile(&self.vals, 0.99);
        Some(SizesSummary {
            count: self.vals.len() as u64,
            total_bytes: self.total,
            min,
            p50,
            p90,
            p99,
            max,
        })
    }
}

#[derive(Clone, Debug, Serialize)]
struct SizesSummary {
    count: u64,
    total_bytes: u64,
    min: u64,
    p50: u64,
    p90: u64,
    p99: u64,
    max: u64,
}

#[derive(Clone, Debug, Serialize)]
struct InventoryReport {
    store_dir: String,
    pack_files_found: u64,
    packs_success: u64,
    packs_last_attempt: u64,
    packs_decode_error: u64,
    objects_total: u64,
    by_type: BTreeMap<String, SizesSummary>,
}

fn percentile(sorted: &[u64], p: f64) -> u64 {
    if sorted.is_empty() {
        return 0;
    }
    if sorted.len() == 1 {
        return sorted[0];
    }
    let p = p.clamp(0.0, 1.0);
    let idx = ((sorted.len() - 1) as f64 * p).round() as usize;
    sorted[idx]
}

fn env_string(name: &str) -> Option<String> {
    std::env::var(name).ok().filter(|s| !s.trim().is_empty())
}

fn env_u64_opt(name: &str) -> Option<u64> {
    std::env::var(name).ok().and_then(|s| s.parse::<u64>().ok())
}

fn create_parent_dirs(path: &Path) {
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent).unwrap_or_else(|e| {
            panic!("create_dir_all {}: {e}", parent.display());
        });
    }
}

fn walk_collect_files(root: &Path, out: &mut Vec<PathBuf>) -> std::io::Result<()> {
    for entry in std::fs::read_dir(root)? {
        let entry = entry?;
        let path = entry.path();
        let meta = entry.metadata()?;
        if meta.is_dir() {
            walk_collect_files(&path, out)?;
            continue;
        }
        if !meta.is_file() {
            continue;
        }
        out.push(path);
    }
    Ok(())
}

fn object_type_from_rsync_uri(uri: &str) -> String {
    let file = uri.rsplit('/').next().unwrap_or(uri);
    if let Some((_, ext)) = file.rsplit_once('.') {
        return ext.to_ascii_lowercase();
    }
    "unknown".to_string()
}

#[test]
#[ignore = "manual inventory of routinator stored publication point packs (SAP)"]
fn stage2_inventory_routinator_sap_store() {
    let store_dir = env_string("BENCH_STORE_DIR")
        .map(PathBuf::from)
        .unwrap_or_else(|| {
            panic!(
                "set BENCH_STORE_DIR to routinator stored/ dir, e.g. /home/yuyr/dev/rust_playground/routinator/bench/full/cache/stored"
            )
        });
    let max_packs = env_u64_opt("BENCH_MAX_PACKS");
    let out_json = env_string("BENCH_OUT_JSON").map(PathBuf::from);

    let mut pack_paths = Vec::new();
    walk_collect_files(&store_dir, &mut pack_paths)
        .unwrap_or_else(|e| panic!("walk {}: {e}", store_dir.display()));
    pack_paths.retain(|p| p.extension().and_then(|s| s.to_str()) == Some("mft"));
    pack_paths.sort();

    if let Some(max) = max_packs {
        pack_paths.truncate(max as usize);
    }

    println!("# Stage2 inventory: routinator SAP packs");
    println!();
    println!("- store_dir: {}", store_dir.display());
    println!("- pack_files_found: {}", pack_paths.len());
    if let Some(max) = max_packs {
        println!("- max_packs: {} (truncating input list)", max);
    }
    if let Some(p) = out_json.as_ref() {
        println!("- out_json: {}", p.display());
    }
    println!();

    let mut by_type: BTreeMap<String, Sizes> = BTreeMap::new();
    let mut packs_success = 0u64;
    let mut packs_last_attempt = 0u64;
    let mut packs_decode_error = 0u64;
    let mut objects_total = 0u64;

    for p in &pack_paths {
        let sap = match sap::SapPublicationPoint::decode_path(p.as_path()) {
            Ok(v) => v,
            Err(_e) => {
                packs_decode_error += 1;
                continue;
            }
        };

        match sap.header.update_status {
            sap::SapUpdateStatus::LastAttempt { .. } => {
                packs_last_attempt += 1;
                continue;
            }
            sap::SapUpdateStatus::Success { .. } => {
                packs_success += 1;
            }
        }

        let Some(m) = sap.manifest.as_ref() else {
            packs_decode_error += 1;
            continue;
        };

        by_type
            .entry("mft".to_string())
            .or_default()
            .push(m.manifest_der.len() as u64);
        by_type
            .entry("crl".to_string())
            .or_default()
            .push(m.crl_der.len() as u64);
        objects_total += 2;

        for o in &sap.objects {
            let typ = object_type_from_rsync_uri(&o.uri);
            by_type
                .entry(typ)
                .or_default()
                .push(o.content_der.len() as u64);
            objects_total += 1;
        }
    }

    let mut summaries: BTreeMap<String, SizesSummary> = BTreeMap::new();
    for (typ, mut sizes) in by_type {
        if let Some(summary) = sizes.summarize() {
            summaries.insert(typ, summary);
        }
    }

    let report = InventoryReport {
        store_dir: store_dir.display().to_string(),
        pack_files_found: pack_paths.len() as u64,
        packs_success,
        packs_last_attempt,
        packs_decode_error,
        objects_total,
        by_type: summaries.clone(),
    };

    println!("| type | count | total_bytes | p50 | p90 | p99 | max |");
    println!("|---|---:|---:|---:|---:|---:|---:|");
    for (typ, s) in &summaries {
        println!(
            "| {} | {} | {} | {} | {} | {} | {} |",
            typ, s.count, s.total_bytes, s.p50, s.p90, s.p99, s.max
        );
    }

    if let Some(path) = out_json.as_ref() {
        create_parent_dirs(path);
        let bytes = serde_json::to_vec_pretty(&report).expect("encode json");
        std::fs::write(path, bytes).unwrap_or_else(|e| panic!("write {}: {e}", path.display()));
    }
}
