use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Arc;

use crate::replay::archive::{ReplayArchiveError, ReplayArchiveIndex, ReplayTransport};
use crate::sync::rrdp::{Fetcher, parse_notification};

#[derive(Debug, thiserror::Error)]
pub enum ReplayHttpFetcherError {
    #[error(transparent)]
    Archive(#[from] ReplayArchiveError),

    #[error("read replay RRDP file failed: {path}: {detail}")]
    ReadFile { path: String, detail: String },

    #[error("parse locked notification failed for {notify_uri}: {detail}")]
    ParseNotification { notify_uri: String, detail: String },

    #[error(
        "locked notification session/serial mismatch for {notify_uri}: expected session={expected_session} serial={expected_serial}, got session={actual_session} serial={actual_serial}"
    )]
    NotificationLockMismatch {
        notify_uri: String,
        expected_session: String,
        expected_serial: u64,
        actual_session: String,
        actual_serial: u64,
    },

    #[error("locked delta file not found for {notify_uri} at serial {serial}: {path}")]
    MissingLockedDelta {
        notify_uri: String,
        serial: u64,
        path: String,
    },

    #[error("duplicate replay HTTP URI mapping for {uri}: {first_path} vs {second_path}")]
    DuplicateUriMapping {
        uri: String,
        first_path: String,
        second_path: String,
    },

    #[error("RRDP notification URI is locked to rsync transport in replay: {0}")]
    LockedToRsync(String),

    #[error("replay HTTP URI not found in archive: {0}")]
    MissingUri(String),
}

#[derive(Clone, Debug)]
pub struct PayloadReplayHttpFetcher {
    index: Arc<ReplayArchiveIndex>,
    routes: BTreeMap<String, PathBuf>,
}

impl PayloadReplayHttpFetcher {
    pub fn new(index: Arc<ReplayArchiveIndex>) -> Result<Self, ReplayHttpFetcherError> {
        let mut routes = BTreeMap::new();
        for (notify_uri, lock) in &index.locks.rrdp {
            if lock.transport != ReplayTransport::Rrdp {
                continue;
            }
            let repo = index.require_rrdp_repo(notify_uri)?;
            insert_unique_route(&mut routes, notify_uri, &repo.locked_notification_path)?;

            let notification_xml = fs::read(&repo.locked_notification_path).map_err(|e| {
                ReplayHttpFetcherError::ReadFile {
                    path: repo.locked_notification_path.display().to_string(),
                    detail: e.to_string(),
                }
            })?;
            let notification = parse_notification(&notification_xml).map_err(|e| {
                ReplayHttpFetcherError::ParseNotification {
                    notify_uri: notify_uri.clone(),
                    detail: e.to_string(),
                }
            })?;
            let actual_session = notification.session_id.to_string();
            if actual_session != repo.locked_session || notification.serial != repo.locked_serial {
                return Err(ReplayHttpFetcherError::NotificationLockMismatch {
                    notify_uri: notify_uri.clone(),
                    expected_session: repo.locked_session.clone(),
                    expected_serial: repo.locked_serial,
                    actual_session,
                    actual_serial: notification.serial,
                });
            }

            insert_unique_route(
                &mut routes,
                &notification.snapshot_uri,
                &repo.locked_snapshot_path,
            )?;

            for delta in notification.deltas {
                let expected = repo.session_dir.join(format!(
                    "delta-{}-{}.xml",
                    delta.serial,
                    hex::encode(delta.hash_sha256)
                ));
                if expected.is_file() {
                    insert_unique_route(&mut routes, &delta.uri, &expected)?;
                }
            }
        }
        Ok(Self { index, routes })
    }

    pub fn from_paths(
        archive_root: impl AsRef<Path>,
        locks_path: impl AsRef<Path>,
    ) -> Result<Self, ReplayHttpFetcherError> {
        let index = Arc::new(ReplayArchiveIndex::load(archive_root, locks_path)?);
        Self::new(index)
    }

    pub fn archive_index(&self) -> &ReplayArchiveIndex {
        self.index.as_ref()
    }

    pub fn mapped_route(&self, uri: &str) -> Option<&Path> {
        self.routes.get(uri).map(PathBuf::as_path)
    }
}

impl Fetcher for PayloadReplayHttpFetcher {
    fn fetch(&self, uri: &str) -> Result<Vec<u8>, String> {
        if let Some(path) = self.routes.get(uri) {
            return fs::read(path).map_err(|e| {
                ReplayHttpFetcherError::ReadFile {
                    path: path.display().to_string(),
                    detail: e.to_string(),
                }
                .to_string()
            });
        }

        if self
            .index
            .rrdp_lock(uri)
            .is_some_and(|lock| lock.transport == ReplayTransport::Rsync)
        {
            return Err(ReplayHttpFetcherError::LockedToRsync(uri.to_string()).to_string());
        }

        Err(ReplayHttpFetcherError::MissingUri(uri.to_string()).to_string())
    }
}

fn insert_unique_route(
    routes: &mut BTreeMap<String, PathBuf>,
    uri: &str,
    path: &Path,
) -> Result<(), ReplayHttpFetcherError> {
    if let Some(existing) = routes.get(uri) {
        if existing != path {
            return Err(ReplayHttpFetcherError::DuplicateUriMapping {
                uri: uri.to_string(),
                first_path: existing.display().to_string(),
                second_path: path.display().to_string(),
            });
        }
        return Ok(());
    }
    routes.insert(uri.to_string(), path.to_path_buf());
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::replay::archive::sha256_hex;

    fn build_archive_with_rrdp_and_rsync(
        with_delta: bool,
    ) -> (
        tempfile::TempDir,
        PathBuf,
        PathBuf,
        String,
        String,
        String,
        Option<String>,
    ) {
        let temp = tempfile::tempdir().expect("tempdir");
        let archive_root = temp.path().join("payload-archive");
        let capture = "capture-http";
        let capture_root = archive_root.join("v1").join("captures").join(capture);
        std::fs::create_dir_all(&capture_root).expect("mkdir capture root");
        std::fs::write(
            capture_root.join("capture.json"),
            format!(
                r#"{{"version":1,"captureId":"{capture}","createdAt":"2026-03-13T00:00:00Z","notes":""}}"#
            ),
        )
        .expect("write capture meta");

        let notify_uri = "https://rrdp.example.test/notification.xml".to_string();
        let snapshot_uri = "https://rrdp.example.test/snapshot.xml".to_string();
        let delta_uri = "https://rrdp.example.test/delta.xml".to_string();
        let rsync_locked_notify = "https://rrdp-fallback.example.test/notification.xml".to_string();
        let session = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa".to_string();
        let serial = 99u64;
        let bucket_hash = sha256_hex(notify_uri.as_bytes());
        let session_dir = capture_root
            .join("rrdp/repos")
            .join(bucket_hash)
            .join(&session);
        std::fs::create_dir_all(&session_dir).expect("mkdir session dir");
        std::fs::write(
            session_dir.parent().unwrap().join("meta.json"),
            format!(
                r#"{{"version":1,"rpkiNotify":"{notify_uri}","createdAt":"2026-03-13T00:00:00Z","lastSeenAt":"2026-03-13T00:00:01Z"}}"#
            ),
        )
        .expect("write repo meta");

        let snapshot_hash = "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff";
        let delta_hash = "ffeeddccbbaa99887766554433221100ffeeddccbbaa99887766554433221100";
        let notification_xml = if with_delta {
            format!(
                r#"<?xml version="1.0" encoding="UTF-8"?>
<notification xmlns="http://www.ripe.net/rpki/rrdp" version="1" session_id="{session}" serial="{serial}">
  <snapshot uri="{snapshot_uri}" hash="{snapshot_hash}" />
  <delta serial="99" uri="{delta_uri}" hash="{delta_hash}" />
</notification>"#
            )
        } else {
            format!(
                r#"<?xml version="1.0" encoding="UTF-8"?>
<notification xmlns="http://www.ripe.net/rpki/rrdp" version="1" session_id="{session}" serial="{serial}">
  <snapshot uri="{snapshot_uri}" hash="{snapshot_hash}" />
</notification>"#
            )
        };
        std::fs::write(session_dir.join("notification-99.xml"), notification_xml)
            .expect("write notification");
        std::fs::write(
            session_dir.join(format!("snapshot-99-{snapshot_hash}.xml")),
            b"<snapshot/>",
        )
        .expect("write snapshot");
        if with_delta {
            std::fs::write(
                session_dir.join(format!("delta-99-{delta_hash}.xml")),
                b"<delta/>",
            )
            .expect("write delta");
        }

        let module_uri = "rsync://rsync.example.test/repo/".to_string();
        let module_hash = sha256_hex(module_uri.as_bytes());
        let tree_root = capture_root
            .join("rsync/modules")
            .join(&module_hash)
            .join("tree")
            .join("rsync.example.test")
            .join("repo");
        std::fs::create_dir_all(&tree_root).expect("mkdir tree root");
        let module_bucket_dir = capture_root.join("rsync/modules").join(&module_hash);
        std::fs::write(
            module_bucket_dir.join("meta.json"),
            format!(
                r#"{{"version":1,"module":"{module_uri}","createdAt":"2026-03-13T00:00:00Z","lastSeenAt":"2026-03-13T00:00:01Z"}}"#
            ),
        )
        .expect("write rsync meta");
        std::fs::write(tree_root.join("x.cer"), b"cer").expect("write tree file");

        let locks_path = temp.path().join("locks.json");
        std::fs::write(
            &locks_path,
            format!(
                r#"{{
  "version":1,
  "capture":"{capture}",
  "rrdp":{{
    "{notify_uri}":{{"transport":"rrdp","session":"{session}","serial":{serial}}},
    "{rsync_locked_notify}":{{"transport":"rsync","session":null,"serial":null}}
  }},
  "rsync":{{
    "{module_uri}":{{"transport":"rsync"}}
  }}
}}"#
            ),
        )
        .expect("write locks");

        (
            temp,
            archive_root,
            locks_path,
            notify_uri,
            snapshot_uri,
            rsync_locked_notify,
            with_delta.then_some(delta_uri),
        )
    }

    #[test]
    fn payload_replay_http_fetcher_reads_locked_notification_and_snapshot_and_delta() {
        let (_temp, archive_root, locks_path, notify_uri, snapshot_uri, _rsync_locked, delta_uri) =
            build_archive_with_rrdp_and_rsync(true);
        let fetcher = PayloadReplayHttpFetcher::from_paths(&archive_root, &locks_path)
            .expect("build replay http fetcher");

        let notification = fetcher.fetch(&notify_uri).expect("fetch notification");
        assert!(
            std::str::from_utf8(&notification)
                .expect("utf8")
                .contains("<notification")
        );

        let snapshot = fetcher.fetch(&snapshot_uri).expect("fetch snapshot");
        assert_eq!(snapshot, b"<snapshot/>");

        let delta = fetcher
            .fetch(delta_uri.as_deref().expect("delta uri"))
            .expect("fetch delta");
        assert_eq!(delta, b"<delta/>");
    }

    #[test]
    fn payload_replay_http_fetcher_rejects_notification_locked_to_rsync() {
        let (_temp, archive_root, locks_path, _notify_uri, _snapshot_uri, rsync_locked, _delta_uri) =
            build_archive_with_rrdp_and_rsync(false);
        let fetcher = PayloadReplayHttpFetcher::from_paths(&archive_root, &locks_path)
            .expect("build replay http fetcher");
        let err = fetcher.fetch(&rsync_locked).unwrap_err();
        assert!(err.contains("locked to rsync transport"), "{err}");
    }

    #[test]
    fn payload_replay_http_fetcher_rejects_unknown_uri() {
        let (
            _temp,
            archive_root,
            locks_path,
            _notify_uri,
            _snapshot_uri,
            _rsync_locked,
            _delta_uri,
        ) = build_archive_with_rrdp_and_rsync(false);
        let fetcher = PayloadReplayHttpFetcher::from_paths(&archive_root, &locks_path)
            .expect("build replay http fetcher");
        let err = fetcher
            .fetch("https://unknown.example/test.xml")
            .unwrap_err();
        assert!(err.contains("not found in archive"), "{err}");
    }

    #[test]
    fn payload_replay_http_fetcher_rejects_notification_lock_mismatch() {
        let (_temp, archive_root, locks_path, notify_uri, _snapshot_uri, _rsync_locked, _delta_uri) =
            build_archive_with_rrdp_and_rsync(false);
        let bucket_hash = sha256_hex(notify_uri.as_bytes());
        let notification = archive_root
            .join("v1/captures/capture-http/rrdp/repos")
            .join(bucket_hash)
            .join("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")
            .join("notification-99.xml");
        std::fs::write(
            &notification,
            r#"<?xml version="1.0" encoding="UTF-8"?>
<notification xmlns="http://www.ripe.net/rpki/rrdp" version="1" session_id="bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" serial="99">
  <snapshot uri="https://rrdp.example.test/snapshot.xml" hash="00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff" />
</notification>"#,
        )
        .expect("rewrite notification");

        let err = PayloadReplayHttpFetcher::from_paths(&archive_root, &locks_path).unwrap_err();
        assert!(
            matches!(err, ReplayHttpFetcherError::NotificationLockMismatch { .. }),
            "{err}"
        );
    }

    #[test]
    fn payload_replay_http_fetcher_skips_missing_locked_delta_route() {
        let (_temp, archive_root, locks_path, notify_uri, _snapshot_uri, _rsync_locked, _delta_uri) =
            build_archive_with_rrdp_and_rsync(true);
        let bucket_hash = sha256_hex(notify_uri.as_bytes());
        let delta = archive_root
            .join("v1/captures/capture-http/rrdp/repos")
            .join(bucket_hash)
            .join("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")
            .join("delta-99-ffeeddccbbaa99887766554433221100ffeeddccbbaa99887766554433221100.xml");
        std::fs::remove_file(delta).expect("remove delta");

        let fetcher = PayloadReplayHttpFetcher::from_paths(&archive_root, &locks_path)
            .expect("build replay http fetcher without delta route");
        let err = fetcher
            .fetch("https://rrdp.example.test/delta.xml")
            .unwrap_err();
        assert!(err.contains("not found in archive"), "{err}");
    }
}
