use std::fs;
use std::path::{Path, PathBuf};

use crate::cir::model::CanonicalInputRepresentation;

#[derive(Debug, thiserror::Error)]
pub enum CirMaterializeError {
    #[error("invalid rsync URI: {0}")]
    InvalidRsyncUri(String),

    #[error("rsync URI must reference a file object, got directory-like URI: {0}")]
    DirectoryLikeRsyncUri(String),

    #[error("create mirror root failed: {path}: {detail}")]
    CreateMirrorRoot { path: String, detail: String },

    #[error("remove mirror root failed: {path}: {detail}")]
    RemoveMirrorRoot { path: String, detail: String },

    #[error("create parent directory failed: {path}: {detail}")]
    CreateParent { path: String, detail: String },

    #[error("remove existing target failed: {path}: {detail}")]
    RemoveExistingTarget { path: String, detail: String },

    #[error("static object not found for sha256={sha256_hex}")]
    MissingStaticObject { sha256_hex: String },

    #[error("link target failed: {src} -> {dst}: {detail}")]
    Link { src: String, dst: String, detail: String },

    #[error("copy target failed: {src} -> {dst}: {detail}")]
    Copy { src: String, dst: String, detail: String },

    #[error("mirror tree mismatch after materialize: {0}")]
    TreeMismatch(String),
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CirMaterializeSummary {
    pub object_count: usize,
    pub linked_files: usize,
    pub copied_files: usize,
}

pub fn materialize_cir(
    cir: &CanonicalInputRepresentation,
    static_root: &Path,
    mirror_root: &Path,
    clean_rebuild: bool,
) -> Result<CirMaterializeSummary, CirMaterializeError> {
    cir.validate()
        .map_err(CirMaterializeError::TreeMismatch)?;

    if clean_rebuild && mirror_root.exists() {
        fs::remove_dir_all(mirror_root).map_err(|e| CirMaterializeError::RemoveMirrorRoot {
            path: mirror_root.display().to_string(),
            detail: e.to_string(),
        })?;
    }
    fs::create_dir_all(mirror_root).map_err(|e| CirMaterializeError::CreateMirrorRoot {
        path: mirror_root.display().to_string(),
        detail: e.to_string(),
    })?;

    let mut linked_files = 0usize;
    let mut copied_files = 0usize;

    for object in &cir.objects {
        let sha256_hex = hex::encode(&object.sha256);
        let source = resolve_static_pool_file(static_root, &sha256_hex)?;
        let relative = mirror_relative_path_for_rsync_uri(&object.rsync_uri)?;
        let target = mirror_root.join(&relative);

        if let Some(parent) = target.parent() {
            fs::create_dir_all(parent).map_err(|e| CirMaterializeError::CreateParent {
                path: parent.display().to_string(),
                detail: e.to_string(),
            })?;
        }

        if target.exists() {
            fs::remove_file(&target).map_err(|e| CirMaterializeError::RemoveExistingTarget {
                path: target.display().to_string(),
                detail: e.to_string(),
            })?;
        }

        match fs::hard_link(&source, &target) {
            Ok(()) => linked_files += 1,
            Err(link_err) => {
                fs::copy(&source, &target).map_err(|copy_err| CirMaterializeError::Copy {
                    src: source.display().to_string(),
                    dst: target.display().to_string(),
                    detail: format!("{copy_err}; original link error: {link_err}"),
                })?;
                copied_files += 1;
            }
        }
    }

    let actual = collect_materialized_uris(mirror_root)?;
    let expected = cir
        .objects
        .iter()
        .map(|item| item.rsync_uri.clone())
        .collect::<std::collections::BTreeSet<_>>();
    if actual != expected {
        return Err(CirMaterializeError::TreeMismatch(format!(
            "expected {} files, got {} files",
            expected.len(),
            actual.len()
        )));
    }

    Ok(CirMaterializeSummary {
        object_count: cir.objects.len(),
        linked_files,
        copied_files,
    })
}

pub fn mirror_relative_path_for_rsync_uri(rsync_uri: &str) -> Result<PathBuf, CirMaterializeError> {
    let url = url::Url::parse(rsync_uri)
        .map_err(|_| CirMaterializeError::InvalidRsyncUri(rsync_uri.to_string()))?;
    if url.scheme() != "rsync" {
        return Err(CirMaterializeError::InvalidRsyncUri(rsync_uri.to_string()));
    }
    let host = url
        .host_str()
        .ok_or_else(|| CirMaterializeError::InvalidRsyncUri(rsync_uri.to_string()))?;
    let segments = url
        .path_segments()
        .ok_or_else(|| CirMaterializeError::InvalidRsyncUri(rsync_uri.to_string()))?
        .collect::<Vec<_>>();
    if segments.is_empty() || segments.last().copied().unwrap_or_default().is_empty() {
        return Err(CirMaterializeError::DirectoryLikeRsyncUri(
            rsync_uri.to_string(),
        ));
    }

    let mut path = PathBuf::from(host);
    for segment in segments {
        if !segment.is_empty() {
            path.push(segment);
        }
    }
    Ok(path)
}

pub fn resolve_static_pool_file(
    static_root: &Path,
    sha256_hex: &str,
) -> Result<PathBuf, CirMaterializeError> {
    if sha256_hex.len() != 64 || !sha256_hex.as_bytes().iter().all(u8::is_ascii_hexdigit) {
        return Err(CirMaterializeError::MissingStaticObject {
            sha256_hex: sha256_hex.to_string(),
        });
    }
    let prefix1 = &sha256_hex[0..2];
    let prefix2 = &sha256_hex[2..4];

    let entries = fs::read_dir(static_root)
        .map_err(|_| CirMaterializeError::MissingStaticObject {
            sha256_hex: sha256_hex.to_string(),
        })?;
    let mut dates = entries
        .filter_map(Result::ok)
        .filter(|entry| entry.path().is_dir())
        .map(|entry| entry.path())
        .collect::<Vec<_>>();
    dates.sort();

    for date_dir in dates {
        let candidate = date_dir.join(prefix1).join(prefix2).join(sha256_hex);
        if candidate.is_file() {
            return Ok(candidate);
        }
    }
    Err(CirMaterializeError::MissingStaticObject {
        sha256_hex: sha256_hex.to_string(),
    })
}

fn collect_materialized_uris(
    mirror_root: &Path,
) -> Result<std::collections::BTreeSet<String>, CirMaterializeError> {
    let mut out = std::collections::BTreeSet::new();
    let mut stack = vec![mirror_root.to_path_buf()];
    while let Some(path) = stack.pop() {
        for entry in fs::read_dir(&path).map_err(|e| CirMaterializeError::CreateMirrorRoot {
            path: path.display().to_string(),
            detail: e.to_string(),
        })? {
            let entry = entry.map_err(|e| CirMaterializeError::CreateMirrorRoot {
                path: path.display().to_string(),
                detail: e.to_string(),
            })?;
            let path = entry.path();
            if path.is_dir() {
                stack.push(path);
            } else {
                let rel = path
                    .strip_prefix(mirror_root)
                    .expect("materialized path under mirror root")
                    .to_string_lossy()
                    .replace('\\', "/");
                let uri = format!("rsync://{rel}");
                out.insert(uri);
            }
        }
    }
    Ok(out)
}

#[cfg(test)]
mod tests {
    use super::{CirMaterializeError, materialize_cir, mirror_relative_path_for_rsync_uri, resolve_static_pool_file};
    use crate::cir::model::{
        CIR_VERSION_V1, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirTal,
    };
    use std::path::{Path, PathBuf};

    fn sample_time() -> time::OffsetDateTime {
        time::OffsetDateTime::parse(
            "2026-04-07T12:34:56Z",
            &time::format_description::well_known::Rfc3339,
        )
        .unwrap()
    }

    fn sample_cir() -> CanonicalInputRepresentation {
        CanonicalInputRepresentation {
            version: CIR_VERSION_V1,
            hash_alg: CirHashAlgorithm::Sha256,
            validation_time: sample_time(),
            objects: vec![
                CirObject {
                    rsync_uri: "rsync://example.net/repo/a.cer".to_string(),
                    sha256: hex::decode(
                        "1111111111111111111111111111111111111111111111111111111111111111",
                    )
                    .unwrap(),
                },
                CirObject {
                    rsync_uri: "rsync://example.net/repo/nested/b.roa".to_string(),
                    sha256: hex::decode(
                        "2222222222222222222222222222222222222222222222222222222222222222",
                    )
                    .unwrap(),
                },
            ],
            tals: vec![CirTal {
                tal_uri: "https://tal.example.net/root.tal".to_string(),
                tal_bytes: b"x".to_vec(),
            }],
        }
    }

    #[test]
    fn mirror_relative_path_for_rsync_uri_maps_host_and_path() {
        let path =
            mirror_relative_path_for_rsync_uri("rsync://example.net/repo/nested/b.roa").unwrap();
        assert_eq!(path, PathBuf::from("example.net").join("repo").join("nested").join("b.roa"));
    }

    #[test]
    fn resolve_static_pool_file_finds_hash_across_dates() {
        let td = tempfile::tempdir().unwrap();
        let path = td
            .path()
            .join("20260407")
            .join("11")
            .join("11");
        std::fs::create_dir_all(&path).unwrap();
        let file = path.join("1111111111111111111111111111111111111111111111111111111111111111");
        std::fs::write(&file, b"x").unwrap();

        let resolved = resolve_static_pool_file(
            td.path(),
            "1111111111111111111111111111111111111111111111111111111111111111",
        )
        .unwrap();
        assert_eq!(resolved, file);
    }

    #[test]
    fn resolve_static_pool_file_rejects_invalid_hash_and_missing_hash() {
        let td = tempfile::tempdir().unwrap();
        let err = resolve_static_pool_file(td.path(), "not-a-hash")
            .expect_err("invalid hash should fail");
        assert!(matches!(err, CirMaterializeError::MissingStaticObject { .. }));

        let err = resolve_static_pool_file(
            td.path(),
            "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
        )
        .expect_err("missing hash should fail");
        assert!(matches!(err, CirMaterializeError::MissingStaticObject { .. }));
    }

    #[test]
    fn mirror_relative_path_rejects_non_rsync_and_directory_like_uris() {
        let err = mirror_relative_path_for_rsync_uri("https://example.net/repo/a.roa")
            .expect_err("non-rsync uri must fail");
        assert!(matches!(err, CirMaterializeError::InvalidRsyncUri(_)));

        let err = mirror_relative_path_for_rsync_uri("rsync://example.net/repo/")
            .expect_err("directory-like uri must fail");
        assert!(matches!(err, CirMaterializeError::DirectoryLikeRsyncUri(_)));
    }

    #[test]
    fn materialize_clean_rebuild_creates_exact_tree_and_removes_stale_files() {
        let td = tempfile::tempdir().unwrap();
        let static_root = td.path().join("static");
        let mirror_root = td.path().join("mirror");

        write_static(
            &static_root,
            "20260407",
            "1111111111111111111111111111111111111111111111111111111111111111",
            b"a",
        );
        write_static(
            &static_root,
            "20260407",
            "2222222222222222222222222222222222222222222222222222222222222222",
            b"b",
        );
        std::fs::create_dir_all(mirror_root.join("stale")).unwrap();
        std::fs::write(mirror_root.join("stale/old.txt"), b"old").unwrap();

        let summary = materialize_cir(&sample_cir(), &static_root, &mirror_root, true).unwrap();
        assert_eq!(summary.object_count, 2);
        assert_eq!(std::fs::read(mirror_root.join("example.net/repo/a.cer")).unwrap(), b"a");
        assert_eq!(
            std::fs::read(mirror_root.join("example.net/repo/nested/b.roa")).unwrap(),
            b"b"
        );
        assert!(!mirror_root.join("stale/old.txt").exists());
    }

    #[test]
    fn materialize_fails_when_static_object_missing() {
        let td = tempfile::tempdir().unwrap();
        let err = materialize_cir(
            &sample_cir(),
            td.path(),
            &td.path().join("mirror"),
            true,
        )
        .expect_err("missing static object must fail");
        assert!(matches!(err, CirMaterializeError::MissingStaticObject { .. }));
    }

    #[test]
    fn materialize_without_clean_rebuild_detects_stale_extra_files() {
        let td = tempfile::tempdir().unwrap();
        let static_root = td.path().join("static");
        let mirror_root = td.path().join("mirror");

        write_static(
            &static_root,
            "20260407",
            "1111111111111111111111111111111111111111111111111111111111111111",
            b"a",
        );
        write_static(
            &static_root,
            "20260407",
            "2222222222222222222222222222222222222222222222222222222222222222",
            b"b",
        );
        std::fs::create_dir_all(mirror_root.join("extra")).unwrap();
        std::fs::write(mirror_root.join("extra/stale.txt"), b"stale").unwrap();

        let err = materialize_cir(&sample_cir(), &static_root, &mirror_root, false)
            .expect_err("stale extra files should fail exact tree check");
        assert!(matches!(err, CirMaterializeError::TreeMismatch(_)));
    }

    fn write_static(root: &Path, date: &str, hash: &str, bytes: &[u8]) {
        let path = root.join(date).join(&hash[0..2]).join(&hash[2..4]);
        std::fs::create_dir_all(&path).unwrap();
        std::fs::write(path.join(hash), bytes).unwrap();
    }
}
