use url::Url;

use crate::data_model::ta::{TrustAnchor, TrustAnchorError};
use crate::data_model::tal::{Tal, TalDecodeError};
use crate::fetch::rsync::RsyncFetcher;
use crate::sync::rrdp::Fetcher;
use crate::validation::ca_instance::{
    CaInstanceUris, CaInstanceUrisError, ca_instance_uris_from_ca_certificate,
};
use crate::validation::run::{RunError, RunOutput, run_publication_point_once};

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DiscoveredRootCaInstance {
    pub tal_url: Option<String>,
    pub trust_anchor: TrustAnchor,
    pub ca_instance: CaInstanceUris,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RunFromTalOutput {
    pub discovery: DiscoveredRootCaInstance,
    pub run: RunOutput,
}

#[derive(Debug, thiserror::Error)]
pub enum FromTalError {
    #[error("TAL fetch failed: {0} (RFC 8630 §2.2)")]
    TalFetch(String),

    #[error("TAL decode failed: {0} (RFC 8630 §2.2)")]
    TalDecode(#[from] TalDecodeError),

    #[error("failed to fetch TA certificate from TAL: {0} (RFC 8630 §2.3)")]
    TaFetch(String),

    #[error("failed to bind TAL and TA certificate: {0} (RFC 8630 §2.3)")]
    Bind(#[from] TrustAnchorError),

    #[error("failed to discover CA instance URIs from TA certificate: {0}")]
    CaInstanceUris(#[from] CaInstanceUrisError),

    #[error("run failed: {0}")]
    Run(#[from] RunError),

    #[error("TAL contains no TA URIs (RFC 8630 §2.2)")]
    NoTaUris,
}

pub fn discover_root_ca_instance_from_tal_url(
    http_fetcher: &dyn Fetcher,
    tal_url: &str,
) -> Result<DiscoveredRootCaInstance, FromTalError> {
    let tal_bytes = http_fetcher
        .fetch(tal_url)
        .map_err(FromTalError::TalFetch)?;
    let tal = Tal::decode_bytes(&tal_bytes)?;
    discover_root_ca_instance_from_tal(http_fetcher, tal, Some(tal_url.to_string()))
}

pub fn discover_root_ca_instance_from_tal_url_with_strict_name(
    http_fetcher: &dyn Fetcher,
    tal_url: &str,
) -> Result<DiscoveredRootCaInstance, FromTalError> {
    let tal_bytes = http_fetcher
        .fetch(tal_url)
        .map_err(FromTalError::TalFetch)?;
    let tal = Tal::decode_bytes(&tal_bytes)?;
    discover_root_ca_instance_from_tal_with_strict_name(
        http_fetcher,
        tal,
        Some(tal_url.to_string()),
    )
}

pub fn discover_root_ca_instance_from_tal_url_with_fetchers(
    http_fetcher: &dyn Fetcher,
    rsync_fetcher: &dyn RsyncFetcher,
    tal_url: &str,
) -> Result<DiscoveredRootCaInstance, FromTalError> {
    let tal_bytes = http_fetcher
        .fetch(tal_url)
        .map_err(FromTalError::TalFetch)?;
    let tal = Tal::decode_bytes(&tal_bytes)?;
    discover_root_ca_instance_from_tal_with_fetchers_impl(
        http_fetcher,
        rsync_fetcher,
        tal,
        Some(tal_url.to_string()),
        false,
        TaUriOrder::HttpFirst,
    )
}

pub fn discover_root_ca_instance_from_tal_url_with_fetchers_strict_name(
    http_fetcher: &dyn Fetcher,
    rsync_fetcher: &dyn RsyncFetcher,
    tal_url: &str,
) -> Result<DiscoveredRootCaInstance, FromTalError> {
    let tal_bytes = http_fetcher
        .fetch(tal_url)
        .map_err(FromTalError::TalFetch)?;
    let tal = Tal::decode_bytes(&tal_bytes)?;
    discover_root_ca_instance_from_tal_with_fetchers_impl(
        http_fetcher,
        rsync_fetcher,
        tal,
        Some(tal_url.to_string()),
        true,
        TaUriOrder::HttpFirst,
    )
}

pub fn discover_root_ca_instance_from_tal(
    http_fetcher: &dyn Fetcher,
    tal: Tal,
    tal_url: Option<String>,
) -> Result<DiscoveredRootCaInstance, FromTalError> {
    discover_root_ca_instance_from_tal_impl(http_fetcher, tal, tal_url, false)
}

pub fn discover_root_ca_instance_from_tal_with_strict_name(
    http_fetcher: &dyn Fetcher,
    tal: Tal,
    tal_url: Option<String>,
) -> Result<DiscoveredRootCaInstance, FromTalError> {
    discover_root_ca_instance_from_tal_impl(http_fetcher, tal, tal_url, true)
}

fn discover_root_ca_instance_from_tal_impl(
    http_fetcher: &dyn Fetcher,
    tal: Tal,
    tal_url: Option<String>,
    strict_name: bool,
) -> Result<DiscoveredRootCaInstance, FromTalError> {
    if tal.ta_uris.is_empty() {
        return Err(FromTalError::NoTaUris);
    }

    let mut last_err: Option<String> = None;
    for ta_uri in tal.ta_uris.iter() {
        let ta_der = match http_fetcher.fetch(ta_uri.as_str()) {
            Ok(b) => b,
            Err(e) => {
                last_err = Some(format!("fetch {ta_uri} failed: {e}"));
                continue;
            }
        };

        let trust_anchor =
            match bind_trust_anchor_der(tal.clone(), &ta_der, Some(ta_uri), strict_name) {
                Ok(ta) => ta,
                Err(e) => {
                    last_err = Some(format!("bind {ta_uri} failed: {e}"));
                    continue;
                }
            };

        let ca_instance =
            match ca_instance_uris_from_ca_certificate(&trust_anchor.ta_certificate.rc_ca) {
                Ok(v) => v,
                Err(e) => {
                    last_err = Some(format!("CA instance discovery failed: {e}"));
                    continue;
                }
            };

        return Ok(DiscoveredRootCaInstance {
            tal_url,
            trust_anchor,
            ca_instance,
        });
    }

    Err(FromTalError::TaFetch(last_err.unwrap_or_else(|| {
        "unknown TA candidate error".to_string()
    })))
}

pub fn discover_root_ca_instance_from_tal_with_fetchers(
    http_fetcher: &dyn Fetcher,
    rsync_fetcher: &dyn RsyncFetcher,
    tal: Tal,
    tal_url: Option<String>,
) -> Result<DiscoveredRootCaInstance, FromTalError> {
    discover_root_ca_instance_from_tal_with_fetchers_impl(
        http_fetcher,
        rsync_fetcher,
        tal,
        tal_url,
        false,
        TaUriOrder::RsyncFirst,
    )
}

pub fn discover_root_ca_instance_from_tal_with_fetchers_strict_name(
    http_fetcher: &dyn Fetcher,
    rsync_fetcher: &dyn RsyncFetcher,
    tal: Tal,
    tal_url: Option<String>,
) -> Result<DiscoveredRootCaInstance, FromTalError> {
    discover_root_ca_instance_from_tal_with_fetchers_impl(
        http_fetcher,
        rsync_fetcher,
        tal,
        tal_url,
        true,
        TaUriOrder::RsyncFirst,
    )
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum TaUriOrder {
    HttpFirst,
    RsyncFirst,
}

fn discover_root_ca_instance_from_tal_with_fetchers_impl(
    http_fetcher: &dyn Fetcher,
    rsync_fetcher: &dyn RsyncFetcher,
    tal: Tal,
    tal_url: Option<String>,
    strict_name: bool,
    ta_uri_order: TaUriOrder,
) -> Result<DiscoveredRootCaInstance, FromTalError> {
    if tal.ta_uris.is_empty() {
        return Err(FromTalError::NoTaUris);
    }

    let mut last_err: Option<String> = None;
    let mut ta_uris = tal.ta_uris.clone();
    ta_uris.sort_by_key(|uri| ta_uri_priority(uri, ta_uri_order));
    for ta_uri in ta_uris.iter() {
        let ta_der = match fetch_ta_der(http_fetcher, rsync_fetcher, ta_uri) {
            Ok(b) => b,
            Err(e) => {
                last_err = Some(format!("fetch {ta_uri} failed: {e}"));
                continue;
            }
        };

        let trust_anchor =
            match bind_trust_anchor_der(tal.clone(), &ta_der, Some(ta_uri), strict_name) {
                Ok(ta) => ta,
                Err(e) => {
                    last_err = Some(format!("bind {ta_uri} failed: {e}"));
                    continue;
                }
            };

        let ca_instance =
            match ca_instance_uris_from_ca_certificate(&trust_anchor.ta_certificate.rc_ca) {
                Ok(v) => v,
                Err(e) => {
                    last_err = Some(format!("CA instance discovery failed: {e}"));
                    continue;
                }
            };

        return Ok(DiscoveredRootCaInstance {
            tal_url,
            trust_anchor,
            ca_instance,
        });
    }

    Err(FromTalError::TaFetch(last_err.unwrap_or_else(|| {
        "unknown TA candidate error".to_string()
    })))
}

fn ta_uri_priority(uri: &Url, order: TaUriOrder) -> u8 {
    match (order, uri.scheme()) {
        (TaUriOrder::HttpFirst, "https" | "http") => 0,
        (TaUriOrder::HttpFirst, "rsync") => 1,
        (TaUriOrder::RsyncFirst, "rsync") => 0,
        (TaUriOrder::RsyncFirst, "https" | "http") => 1,
        _ => 2,
    }
}

fn fetch_ta_der(
    http_fetcher: &dyn Fetcher,
    rsync_fetcher: &dyn RsyncFetcher,
    ta_uri: &Url,
) -> Result<Vec<u8>, String> {
    match ta_uri.scheme() {
        "https" | "http" => http_fetcher.fetch(ta_uri.as_str()),
        "rsync" => fetch_ta_der_via_rsync(rsync_fetcher, ta_uri.as_str()),
        scheme => Err(format!("unsupported TA URI scheme: {scheme}")),
    }
}

fn bind_trust_anchor_der(
    tal: Tal,
    ta_der: &[u8],
    resolved_uri: Option<&Url>,
    strict_name: bool,
) -> Result<TrustAnchor, TrustAnchorError> {
    if strict_name {
        TrustAnchor::bind_der_with_strict_name(tal, ta_der, resolved_uri)
    } else {
        TrustAnchor::bind_der(tal, ta_der, resolved_uri)
    }
}

fn fetch_ta_der_via_rsync(
    rsync_fetcher: &dyn RsyncFetcher,
    ta_rsync_uri: &str,
) -> Result<Vec<u8>, String> {
    rsync_fetcher
        .fetch_object(ta_rsync_uri)
        .map_err(|e| e.to_string())
}

pub fn discover_root_ca_instance_from_tal_and_ta_der(
    tal_bytes: &[u8],
    ta_der: &[u8],
    resolved_ta_uri: Option<&Url>,
) -> Result<DiscoveredRootCaInstance, FromTalError> {
    discover_root_ca_instance_from_tal_and_ta_der_impl(tal_bytes, ta_der, resolved_ta_uri, false)
}

pub fn discover_root_ca_instance_from_tal_and_ta_der_with_strict_name(
    tal_bytes: &[u8],
    ta_der: &[u8],
    resolved_ta_uri: Option<&Url>,
) -> Result<DiscoveredRootCaInstance, FromTalError> {
    discover_root_ca_instance_from_tal_and_ta_der_impl(tal_bytes, ta_der, resolved_ta_uri, true)
}

fn discover_root_ca_instance_from_tal_and_ta_der_impl(
    tal_bytes: &[u8],
    ta_der: &[u8],
    resolved_ta_uri: Option<&Url>,
    strict_name: bool,
) -> Result<DiscoveredRootCaInstance, FromTalError> {
    let tal = Tal::decode_bytes(tal_bytes)?;
    let trust_anchor = bind_trust_anchor_der(tal, ta_der, resolved_ta_uri, strict_name)?;
    let ca_instance = ca_instance_uris_from_ca_certificate(&trust_anchor.ta_certificate.rc_ca)?;
    Ok(DiscoveredRootCaInstance {
        tal_url: None,
        trust_anchor,
        ca_instance,
    })
}

pub fn canonical_tal_rsync_uri_from_bytes(tal_bytes: &[u8]) -> Result<Url, FromTalError> {
    let tal = Tal::decode_bytes(tal_bytes)?;
    tal.ta_uris
        .iter()
        .find(|uri| uri.scheme() == "rsync")
        .cloned()
        .ok_or_else(|| {
            FromTalError::TaFetch("TAL contains no rsync TA URI for offline TA binding".to_string())
        })
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::fetch::rsync::LocalDirRsyncFetcher;
    use std::collections::HashMap;

    struct MapHttpFetcher {
        map: HashMap<String, Vec<u8>>,
    }

    impl Fetcher for MapHttpFetcher {
        fn fetch(&self, uri: &str) -> Result<Vec<u8>, String> {
            self.map
                .get(uri)
                .cloned()
                .ok_or_else(|| format!("no fixture mapped for {uri}"))
        }
    }

    struct FailingHttpFetcher;

    impl Fetcher for FailingHttpFetcher {
        fn fetch(&self, uri: &str) -> Result<Vec<u8>, String> {
            Err(format!("blocked test HTTP fetch: {uri}"))
        }
    }

    #[test]
    fn discover_root_ca_instance_from_tal_with_fetchers_supports_rsync_ta_uri() {
        let tal_bytes = std::fs::read(
            std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
                .join("tests/fixtures/tal/apnic-rfc7730-https.tal"),
        )
        .unwrap();
        let ta_der = std::fs::read(
            std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
                .join("tests/fixtures/ta/apnic-ta.cer"),
        )
        .unwrap();
        let tal = Tal::decode_bytes(&tal_bytes).unwrap();
        let rsync_uri = tal
            .ta_uris
            .iter()
            .find(|uri| uri.scheme() == "rsync")
            .unwrap()
            .clone();

        let td = tempfile::tempdir().unwrap();
        let mirror_root = td
            .path()
            .join(rsync_uri.host_str().unwrap())
            .join("repository");
        std::fs::create_dir_all(&mirror_root).unwrap();
        std::fs::write(mirror_root.join("apnic-rpki-root-iana-origin.cer"), ta_der).unwrap();

        let rsync = LocalDirRsyncFetcher::new(
            td.path()
                .join(rsync_uri.host_str().unwrap())
                .join("repository"),
        );
        let discovery = discover_root_ca_instance_from_tal_with_fetchers(
            &FailingHttpFetcher,
            &rsync,
            tal,
            None,
        )
        .expect("discover via rsync TA fallback");
        assert!(
            discovery
                .trust_anchor
                .resolved_ta_uri
                .unwrap()
                .as_str()
                .starts_with("rsync://")
        );
    }

    struct PanicRsyncFetcher;

    impl RsyncFetcher for PanicRsyncFetcher {
        fn fetch_objects(
            &self,
            rsync_base_uri: &str,
        ) -> crate::fetch::rsync::RsyncFetchResult<Vec<(String, Vec<u8>)>> {
            panic!("rsync should not be used when HTTPS TA URI is available: {rsync_base_uri}")
        }
    }

    #[test]
    fn discover_root_ca_instance_from_tal_url_with_fetchers_prefers_https_ta_uri() {
        let tal_url = "https://example.test/apnic.tal";
        let tal_bytes = std::fs::read(
            std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
                .join("tests/fixtures/tal/apnic-rfc7730-https.tal"),
        )
        .unwrap();
        let ta_der = std::fs::read(
            std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
                .join("tests/fixtures/ta/apnic-ta.cer"),
        )
        .unwrap();
        let https_uri = "https://rpki.apnic.net/repository/apnic-rpki-root-iana-origin.cer";
        let mut map = HashMap::new();
        map.insert(tal_url.to_string(), tal_bytes);
        map.insert(https_uri.to_string(), ta_der);
        let discovery = discover_root_ca_instance_from_tal_url_with_fetchers(
            &MapHttpFetcher { map },
            &PanicRsyncFetcher,
            tal_url,
        )
        .expect("discover via HTTPS TA URI");
        assert_eq!(
            discovery.trust_anchor.resolved_ta_uri.unwrap().as_str(),
            https_uri
        );
    }

    #[test]
    fn discover_root_ca_instance_from_tal_url_with_fetchers_supports_rsync_only_tal() {
        let tal_url = "https://example.test/apnic-rsync-only.tal";
        let tal_bytes = std::fs::read_to_string(
            std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
                .join("tests/fixtures/tal/apnic-rfc7730-https.tal"),
        )
        .unwrap();
        let rsync_uri = "rsync://rpki.apnic.net/repository/apnic-rpki-root-iana-origin.cer";
        let key_material = tal_bytes
            .split_once("\n\n")
            .map(|(_, key)| key)
            .expect("fixture contains TAL key material");
        let rsync_only_tal = format!("{rsync_uri}\n\n{key_material}");
        let ta_der = std::fs::read(
            std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
                .join("tests/fixtures/ta/apnic-ta.cer"),
        )
        .unwrap();
        let td = tempfile::tempdir().unwrap();
        let mirror_root = td.path().join("repository");
        std::fs::create_dir_all(&mirror_root).unwrap();
        std::fs::write(mirror_root.join("apnic-rpki-root-iana-origin.cer"), ta_der).unwrap();

        let mut map = HashMap::new();
        map.insert(tal_url.to_string(), rsync_only_tal.into_bytes());
        let discovery = discover_root_ca_instance_from_tal_url_with_fetchers(
            &MapHttpFetcher { map },
            &LocalDirRsyncFetcher::new(mirror_root),
            tal_url,
        )
        .expect("discover via rsync-only TAL");
        assert_eq!(
            discovery.trust_anchor.resolved_ta_uri.unwrap().as_str(),
            rsync_uri
        );
    }
}

pub fn run_root_from_tal_url_once(
    store: &crate::storage::RocksStore,
    policy: &crate::policy::Policy,
    tal_url: &str,
    http_fetcher: &dyn Fetcher,
    rsync_fetcher: &dyn crate::fetch::rsync::RsyncFetcher,
    validation_time: time::OffsetDateTime,
) -> Result<RunFromTalOutput, FromTalError> {
    let discovery =
        discover_root_ca_instance_from_tal_url_with_fetchers(http_fetcher, rsync_fetcher, tal_url)?;

    let run = run_publication_point_once(
        store,
        policy,
        discovery.ca_instance.rrdp_notification_uri.as_deref(),
        &discovery.ca_instance.rsync_base_uri,
        &discovery.ca_instance.manifest_rsync_uri,
        &discovery.ca_instance.publication_point_rsync_uri,
        http_fetcher,
        rsync_fetcher,
        &discovery.trust_anchor.ta_certificate.raw_der,
        None,
        discovery
            .trust_anchor
            .ta_certificate
            .rc_ca
            .tbs
            .extensions
            .ip_resources
            .as_ref(),
        discovery
            .trust_anchor
            .ta_certificate
            .rc_ca
            .tbs
            .extensions
            .as_resources
            .as_ref(),
        validation_time,
    )?;

    Ok(RunFromTalOutput { discovery, run })
}
