use std::cell::RefCell;
use std::collections::{BTreeMap, HashSet};
use std::time::Duration;

use rpki::data_model::crl::RpkixCrl;
use rpki::fetch::http::{BlockingHttpFetcher, HttpFetcherConfig};
use rpki::fetch::rsync_system::{SystemRsyncConfig, SystemRsyncFetcher};
use rpki::policy::Policy;
use rpki::storage::RocksStore;
use rpki::validation::from_tal::discover_root_ca_instance_from_tal_url;
use rpki::validation::run_tree_from_tal::root_handle_from_trust_anchor;
use rpki::validation::tree::{
    CaInstanceHandle, PublicationPointRunResult, PublicationPointRunner, TreeRunConfig,
    run_tree_serial,
};
use rpki::validation::tree_runner::Rpkiv1PublicationPointRunner;

const APNIC_TAL_URL: &str = "https://tal.apnic.net/tal-archive/apnic-rfc7730-https.tal";

#[derive(Default, Debug)]
struct LiveStats {
    publication_points_processed: usize,
    publication_points_fresh: usize,
    publication_points_cached: usize,
    rrdp_repos_unique: HashSet<String>,

    pack_file_uris_unique: HashSet<String>,
    pack_uris_total: usize,
    pack_uris_by_ext_total: BTreeMap<String, usize>,

    crl_total: usize,
    crl_decode_ok: usize,

    child_ca_cert_candidates_total: usize,
    child_ca_cert_validated_ok: usize,

    roa_total: usize,
    roa_ok: usize,
    aspa_total: usize,
    aspa_ok: usize,

    objects_dropped_publication_points: usize,
}

fn ext_of_uri(uri: &str) -> String {
    let last = uri.rsplit('/').next().unwrap_or(uri);
    match last.rsplit_once('.') {
        Some((_base, ext)) if !ext.is_empty() => ext.to_ascii_lowercase(),
        _ => "(none)".to_string(),
    }
}

impl LiveStats {
    fn record_ca_inputs(&mut self, ca: &CaInstanceHandle) {
        if let Some(u) = ca.rrdp_notification_uri.as_ref() {
            self.rrdp_repos_unique.insert(u.clone());
        }
    }

    fn record(&mut self, res: &PublicationPointRunResult) {
        self.publication_points_processed += 1;
        match res.source {
            rpki::validation::manifest::PublicationPointSource::Fresh => {
                self.publication_points_fresh += 1
            }
            rpki::validation::manifest::PublicationPointSource::VcirCurrentInstance => {
                self.publication_points_cached += 1
            }
            rpki::validation::manifest::PublicationPointSource::PublicationPointCache => {
                self.publication_points_cached += 1
            }
            rpki::validation::manifest::PublicationPointSource::FailedFetchNoCache => {}
        }

        if let Some(pack) = res.snapshot.as_ref() {
            self.pack_uris_total += 1;
            self.pack_file_uris_unique
                .insert(pack.manifest_rsync_uri.clone());
            *self
                .pack_uris_by_ext_total
                .entry(ext_of_uri(&pack.manifest_rsync_uri))
                .or_insert(0) += 1;

            for f in &pack.files {
                self.pack_uris_total += 1;
                self.pack_file_uris_unique.insert(f.rsync_uri.clone());
                *self
                    .pack_uris_by_ext_total
                    .entry(ext_of_uri(&f.rsync_uri))
                    .or_insert(0) += 1;

                if f.rsync_uri.ends_with(".crl") {
                    self.crl_total += 1;
                    if f.bytes()
                        .ok()
                        .and_then(|b| RpkixCrl::decode_der(b).ok())
                        .is_some()
                    {
                        self.crl_decode_ok += 1;
                    }
                }

                if f.rsync_uri.ends_with(".cer") {
                    self.child_ca_cert_candidates_total += 1;
                }
            }
        }

        self.child_ca_cert_validated_ok += res.discovered_children.len();

        self.roa_total += res.objects.stats.roa_total;
        self.roa_ok += res.objects.stats.roa_ok;
        self.aspa_total += res.objects.stats.aspa_total;
        self.aspa_ok += res.objects.stats.aspa_ok;

        if res.objects.stats.publication_point_dropped {
            self.objects_dropped_publication_points += 1;
        }

        if self.publication_points_processed % 25 == 0 {
            println!(
                "progress: processed={} fresh={} cached={} failed_objects_pps={} vrps={} aspas={}",
                self.publication_points_processed,
                self.publication_points_fresh,
                self.publication_points_cached,
                self.objects_dropped_publication_points,
                self.roa_ok,
                self.aspa_ok,
            );
        }
    }
}

struct CountingRunner<'a> {
    inner: &'a dyn PublicationPointRunner,
    stats: &'a RefCell<LiveStats>,
}

impl<'a> PublicationPointRunner for CountingRunner<'a> {
    fn run_publication_point(
        &self,
        ca: &CaInstanceHandle,
    ) -> Result<PublicationPointRunResult, String> {
        self.stats.borrow_mut().record_ca_inputs(ca);
        let res = self.inner.run_publication_point(ca)?;
        self.stats.borrow_mut().record(&res);
        Ok(res)
    }
}

#[test]
#[ignore = "live network + rsync full-tree stats (APNIC TAL); may take minutes"]
fn apnic_tree_full_stats_serial() {
    let http = BlockingHttpFetcher::new(HttpFetcherConfig {
        timeout: Duration::from_secs(30 * 60),
        ..HttpFetcherConfig::default()
    })
    .expect("http fetcher");
    let rsync = SystemRsyncFetcher::new(SystemRsyncConfig {
        timeout: Duration::from_secs(30 * 60),
        ..SystemRsyncConfig::default()
    });
    let validation_time = time::OffsetDateTime::now_utc();

    let temp = tempfile::tempdir().expect("tempdir");
    let store = RocksStore::open(temp.path()).expect("open rocksdb");
    let policy = Policy::default();

    let discovery =
        discover_root_ca_instance_from_tal_url(&http, APNIC_TAL_URL).expect("discover APNIC TAL");

    let runner = Rpkiv1PublicationPointRunner {
        store: &store,
        policy: &policy,
        http_fetcher: &http,
        rsync_fetcher: &rsync,
        validation_time,
        timing: None,
        download_log: None,
        replay_archive_index: None,
        replay_delta_index: None,
        rrdp_dedup: true,
        rrdp_repo_cache: std::sync::Mutex::new(std::collections::HashMap::new()),
        rsync_dedup: true,
        rsync_repo_cache: std::sync::Mutex::new(std::collections::HashMap::new()),
        current_repo_index: None,
        repo_sync_runtime: None,
        parallel_phase2_config: None,
        parallel_roa_worker_pool: None,
        ccr_accumulator: None,
        persist_vcir: true,
        enable_roa_validation_cache: false,
        enable_child_certificate_validation_cache: false,
        publication_point_cache_observe_only: false,
        enable_publication_point_validation_cache: false,
    };

    let stats = RefCell::new(LiveStats::default());
    let counting = CountingRunner {
        inner: &runner,
        stats: &stats,
    };

    let root: CaInstanceHandle = root_handle_from_trust_anchor(
        &discovery.trust_anchor,
        "test-tal".to_string(),
        None,
        &discovery.ca_instance,
    );

    let max_depth = std::env::var("RPKI_APNIC_MAX_DEPTH")
        .ok()
        .and_then(|s| s.parse::<usize>().ok());
    let max_instances = std::env::var("RPKI_APNIC_MAX_INSTANCES")
        .ok()
        .and_then(|s| s.parse::<usize>().ok());

    let out = run_tree_serial(
        root,
        &counting,
        &TreeRunConfig {
            max_depth,
            max_instances,
            compact_audit: false,
            persist_vcir: true,
            build_ccr_accumulator: true,
            enable_roa_validation_cache: false,
            enable_child_certificate_validation_cache: false,
            publication_point_cache_observe_only: false,
            enable_publication_point_validation_cache: false,
            enable_transport_request_prefetch: false,
        },
    )
    .expect("run tree");

    let stats = stats.into_inner();

    let mut raw_total = 0usize;
    let mut raw_by_ext: BTreeMap<String, usize> = BTreeMap::new();
    for entry in store
        .list_repository_view_entries_with_prefix("rsync://")
        .expect("list repository view")
    {
        raw_total += 1;
        *raw_by_ext.entry(ext_of_uri(&entry.rsync_uri)).or_insert(0) += 1;
    }

    println!("APNIC Stage2 full-tree serial stats");
    println!("tal_url={APNIC_TAL_URL}");
    println!(
        "validation_time={}",
        validation_time
            .format(&time::format_description::well_known::Rfc3339)
            .unwrap()
    );
    println!();
    println!(
        "publication_points_processed={} publication_points_failed={} fresh={} cached={}",
        stats.publication_points_processed,
        out.instances_failed,
        stats.publication_points_fresh,
        stats.publication_points_cached
    );
    println!("rrdp_repos_unique={}", stats.rrdp_repos_unique.len());
    println!(
        "objects_dropped_publication_points={}",
        stats.objects_dropped_publication_points
    );
    println!();
    println!(
        "pack_uris_total={} pack_uris_unique={}",
        stats.pack_uris_total,
        stats.pack_file_uris_unique.len()
    );
    println!("pack_uris_by_ext_total={:?}", stats.pack_uris_by_ext_total);
    println!();
    println!(
        "crl_total={} crl_decode_ok={}",
        stats.crl_total, stats.crl_decode_ok
    );
    println!(
        "child_ca_cert_candidates_total={} child_ca_cert_validated_ok={}",
        stats.child_ca_cert_candidates_total, stats.child_ca_cert_validated_ok
    );
    println!(
        "roa_total={} roa_ok={} vrps_out={}",
        stats.roa_total,
        stats.roa_ok,
        out.vrps.len()
    );
    println!(
        "aspa_total={} aspa_ok={} aspas_out={}",
        stats.aspa_total,
        stats.aspa_ok,
        out.aspas.len()
    );
    println!();
    println!(
        "current_repository_view_total={} by_ext={:?}",
        raw_total, raw_by_ext
    );

    // Loose sanity assertions (avoid flakiness due to repository churn).
    //
    // This test supports bounded manual runs via env vars:
    // - `RPKI_APNIC_MAX_DEPTH=0` or `RPKI_APNIC_MAX_INSTANCES=1` implies root-only.
    // Otherwise we expect to reach at least one child.
    let min_expected = if max_depth == Some(0) || max_instances.is_some_and(|n| n <= 1) {
        1
    } else {
        2
    };
    assert!(
        out.instances_processed >= min_expected,
        "expected to process at least {min_expected} publication point(s)"
    );
}
