use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};

use rpki::fetch::http::{BlockingHttpFetcher, HttpFetcherConfig};
use rpki::fetch::rsync::{RsyncFetchError, RsyncFetcher};
use rpki::policy::{CaFailedFetchPolicy, Policy, SyncPreference};
use rpki::storage::RocksStore;
use rpki::sync::repo::{RepoSyncSource, sync_publication_point};
use rpki::sync::rrdp::{Fetcher, parse_notification, sync_from_notification};
use rpki::validation::from_tal::discover_root_ca_instance_from_tal_url;
use rpki::validation::manifest::{PublicationPointSource, process_manifest_publication_point};

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

fn persistent_db_dir() -> PathBuf {
    if let Ok(s) = std::env::var("RPKI_LIVE_DB_DIR") {
        return PathBuf::from(s);
    }
    PathBuf::from("target/live/apnic_rrdp_db")
}

fn live_http_fetcher() -> BlockingHttpFetcher {
    let timeout_secs: u64 = std::env::var("RPKI_LIVE_HTTP_TIMEOUT_SECS")
        .ok()
        .and_then(|s| s.parse().ok())
        .unwrap_or(15 * 60);
    BlockingHttpFetcher::new(HttpFetcherConfig {
        timeout: Duration::from_secs(timeout_secs),
        large_body_timeout: Duration::from_secs(timeout_secs),
        user_agent: "panda-rpki/0.2 (live rrdp delta test)".to_string(),
        ..HttpFetcherConfig::default()
    })
    .expect("http fetcher")
}

struct AlwaysFailRsyncFetcher;

impl RsyncFetcher for AlwaysFailRsyncFetcher {
    fn fetch_objects(
        &self,
        _rsync_base_uri: &str,
    ) -> Result<Vec<(String, Vec<u8>)>, RsyncFetchError> {
        Err(RsyncFetchError::Fetch(
            "rsync disabled for this test".to_string(),
        ))
    }
}

#[derive(Clone)]
struct CountingDenyUriFetcher {
    inner: BlockingHttpFetcher,
    deny_uri: String,
    counts: Arc<Mutex<HashMap<String, u64>>>,
}

impl CountingDenyUriFetcher {
    fn new(inner: BlockingHttpFetcher, deny_uri: String) -> Self {
        Self {
            inner,
            deny_uri,
            counts: Arc::new(Mutex::new(HashMap::new())),
        }
    }

    fn count(&self, uri: &str) -> u64 {
        *self
            .counts
            .lock()
            .expect("lock counts")
            .get(uri)
            .unwrap_or(&0)
    }
}

impl Fetcher for CountingDenyUriFetcher {
    fn fetch(&self, uri: &str) -> Result<Vec<u8>, String> {
        *self
            .counts
            .lock()
            .expect("lock counts")
            .entry(uri.to_string())
            .or_insert(0) += 1;
        if uri == self.deny_uri {
            return Err(format!("snapshot fetch denied: {uri}"));
        }
        self.inner.fetch(uri)
    }
}

fn live_policy() -> Policy {
    let mut p = Policy::default();
    p.sync_preference = SyncPreference::RrdpThenRsync;
    p.ca_failed_fetch_policy = CaFailedFetchPolicy::ReuseCurrentInstanceVcir;
    p
}

#[test]
#[ignore = "live network: APNIC RRDP snapshot bootstrap into persistent RocksDB"]
fn apnic_live_bootstrap_snapshot_and_persist_root_vcir_to_persistent_db() {
    let http = live_http_fetcher();
    let rsync = AlwaysFailRsyncFetcher;

    let db_dir = persistent_db_dir();
    std::fs::create_dir_all(&db_dir).expect("create db dir");
    let store = RocksStore::open(&db_dir).expect("open rocksdb");

    let policy = live_policy();
    let validation_time = time::OffsetDateTime::now_utc();

    let discovery = discover_root_ca_instance_from_tal_url(&http, APNIC_TAL_URL)
        .expect("discover root CA instance from APNIC TAL");
    let ca_instance = discovery.ca_instance;

    let rrdp_notification_uri = ca_instance
        .rrdp_notification_uri
        .as_deref()
        .expect("APNIC root must have rrdpNotification");

    let sync = sync_publication_point(
        &store,
        &policy,
        Some(rrdp_notification_uri),
        &ca_instance.rsync_base_uri,
        &http,
        &rsync,
        None,
        None,
    )
    .expect("repo sync");

    assert_eq!(sync.source, RepoSyncSource::Rrdp);

    // Build the root publication point and persist the latest VCIR so later runs can
    // validate current-instance failed-fetch reuse behavior (RFC 9286 §6.6).
    let ta_der = discovery.trust_anchor.ta_certificate.raw_der;
    let pp = process_manifest_publication_point(
        &store,
        &policy,
        &ca_instance.manifest_rsync_uri,
        &ca_instance.publication_point_rsync_uri,
        &ta_der,
        None,
        validation_time,
    )
    .expect("process manifest publication point");

    assert_eq!(pp.source, PublicationPointSource::Fresh);

    let cached = store
        .get_vcir(&ca_instance.manifest_rsync_uri)
        .expect("get vcir");
    assert!(cached.is_some(), "expected VCIR to be stored");

    eprintln!(
        "OK: bootstrap complete; persistent db at: {}",
        db_dir.display()
    );
    eprintln!(
        "Next: run `cargo test --release -q --test test_apnic_rrdp_delta_live_20260226 -- --ignored` later to exercise delta sync."
    );
}

#[test]
#[ignore = "live network: waits for APNIC RRDP serial advance, then sync via deltas only (no snapshot) using persistent RocksDB"]
fn apnic_live_delta_only_from_persistent_db() {
    let http = live_http_fetcher();

    let db_dir = persistent_db_dir();
    let store = RocksStore::open(&db_dir).expect("open rocksdb (must have been bootstrapped)");
    let policy = live_policy();

    let discovery = discover_root_ca_instance_from_tal_url(&http, APNIC_TAL_URL)
        .expect("discover root CA instance from APNIC TAL");
    let ca_instance = discovery.ca_instance;

    let rrdp_notification_uri = ca_instance
        .rrdp_notification_uri
        .as_deref()
        .expect("APNIC root must have rrdpNotification");

    let source = store
        .get_rrdp_source_record(rrdp_notification_uri)
        .expect("get rrdp source")
        .unwrap_or_else(|| {
            panic!(
                "missing RRDP source state for APNIC notification URI; run bootstrap test first. db_dir={}",
                db_dir.display()
            )
        });
    let old_serial = source.last_serial.expect("last serial");
    let old_session = source.last_session_id.expect("last session");

    let max_wait_secs: u64 = std::env::var("RPKI_LIVE_MAX_WAIT_SECS")
        .ok()
        .and_then(|s| s.parse().ok())
        .unwrap_or(30 * 60);
    let poll_secs: u64 = std::env::var("RPKI_LIVE_POLL_SECS")
        .ok()
        .and_then(|s| s.parse().ok())
        .unwrap_or(60);

    let start = Instant::now();
    loop {
        if start.elapsed() > Duration::from_secs(max_wait_secs) {
            panic!(
                "timed out waiting for APNIC RRDP serial to advance for delta sync; old_session={} old_serial={} waited={}s",
                old_session, old_serial, max_wait_secs
            );
        }

        let notif_xml = http
            .fetch(rrdp_notification_uri)
            .unwrap_or_else(|e| panic!("fetch notification failed: {e}"));
        let notif = parse_notification(&notif_xml).expect("parse notification");

        if notif.session_id.to_string() != old_session {
            panic!(
                "RRDP session_id changed; this delta-only test assumes same snapshot baseline. old_session={} new_session={}",
                old_session, notif.session_id
            );
        }

        if notif.serial <= old_serial {
            eprintln!(
                "waiting for serial advance: session={} old_serial={} current_serial={}",
                old_session, old_serial, notif.serial
            );
            std::thread::sleep(Duration::from_secs(poll_secs));
            continue;
        }

        let want_first = old_serial + 1;
        let min_delta = notif.deltas.first().map(|d| d.serial).unwrap_or(u64::MAX);
        if notif.deltas.is_empty() || min_delta > want_first {
            panic!(
                "notification deltas do not cover required serial gap for delta-only sync; old_serial={} want_first={} min_delta={} current_serial={}. rerun bootstrap to refresh snapshot baseline.",
                old_serial, want_first, min_delta, notif.serial
            );
        }

        // Deny snapshot fetch to ensure we truly test the delta path and keep the stored snapshot
        // baseline unchanged.
        let deny = notif.snapshot_uri.clone();
        let fetcher = CountingDenyUriFetcher::new(http.clone(), deny.clone());

        match sync_from_notification(&store, rrdp_notification_uri, &notif_xml, &fetcher) {
            Ok(written) => {
                assert!(
                    written > 0,
                    "expected delta sync to apply changes (written={written})"
                );
                assert_eq!(
                    fetcher.count(&deny),
                    0,
                    "delta sync should not fetch snapshot"
                );
                eprintln!(
                    "OK: delta sync applied: written={} old_serial={} new_serial={}",
                    written, old_serial, notif.serial
                );
                break;
            }
            Err(e) => {
                eprintln!("delta sync attempt failed (will retry): {e}");
                std::thread::sleep(Duration::from_secs(poll_secs));
            }
        }
    }

    // Keep policy variable used, to avoid warnings if this test evolves.
    let _ = policy;
}

#[test]
#[ignore = "offline/synthetic: after bootstrap, force repo sync failure and assert current-instance VCIR is reused (RFC 9286 §6.6)"]
fn apnic_root_repo_sync_failure_reuses_current_instance_vcir() {
    let http = live_http_fetcher();
    let db_dir = persistent_db_dir();
    let store = RocksStore::open(&db_dir).expect("open rocksdb (must have been bootstrapped)");

    let mut policy = live_policy();
    policy.sync_preference = SyncPreference::RrdpThenRsync;
    policy.ca_failed_fetch_policy = CaFailedFetchPolicy::ReuseCurrentInstanceVcir;

    let validation_time = time::OffsetDateTime::now_utc();

    let discovery = discover_root_ca_instance_from_tal_url(&http, APNIC_TAL_URL)
        .expect("discover root CA instance from APNIC TAL");
    let ca_instance = discovery.ca_instance;

    // Ensure current-instance VCIR exists (created by bootstrap).
    let cached = store
        .get_vcir(&ca_instance.manifest_rsync_uri)
        .expect("get vcir");
    assert!(
        cached.is_some(),
        "missing VCIR; run bootstrap test first. db_dir={}",
        db_dir.display()
    );

    // Simulate repo sync failure: skip calling sync_publication_point and directly drive manifest
    // processing with repo_sync_ok=false.
    let ta_der = discovery.trust_anchor.ta_certificate.raw_der;
    let pp = rpki::validation::manifest::process_manifest_publication_point_after_repo_sync(
        &store,
        &policy,
        &ca_instance.manifest_rsync_uri,
        &ca_instance.publication_point_rsync_uri,
        &ta_der,
        None,
        validation_time,
        false,
        Some("synthetic repo sync failure"),
    )
    .expect("must reuse current-instance VCIR");

    assert_eq!(pp.source, PublicationPointSource::VcirCurrentInstance);
    assert!(
        pp.warnings.iter().all(|w| {
            !w.message
                .contains("using latest validated result for current CA instance")
                && !w.message.contains("manifest failed fetch")
        }),
        "successful current-instance VCIR reuse should not emit fallback bookkeeping warnings"
    );
}
