use std::collections::BTreeSet;
use std::fs::{self, OpenOptions};
use std::io::Write;
use std::path::{Path, PathBuf};

use crate::storage::{RawByHashEntry, RocksStore};

#[derive(Debug, thiserror::Error)]
pub enum CirStaticPoolError {
    #[error("invalid sha256 hex: {0}")]
    InvalidSha256Hex(String),

    #[error("raw bytes are empty for sha256={sha256_hex}")]
    EmptyBytes { sha256_hex: String },

    #[error("raw bytes do not match sha256 hex: {sha256_hex}")]
    HashMismatch { sha256_hex: String },

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

    #[error("create temp file failed: {path}: {detail}")]
    CreateTemp { path: String, detail: String },

    #[error("write temp file failed: {path}: {detail}")]
    WriteTemp { path: String, detail: String },

    #[error("sync temp file failed: {path}: {detail}")]
    SyncTemp { path: String, detail: String },

    #[error("publish temp file failed: {temp_path} -> {final_path}: {detail}")]
    Publish {
        temp_path: String,
        final_path: String,
        detail: String,
    },

    #[error("remove temp file failed: {path}: {detail}")]
    RemoveTemp { path: String, detail: String },

    #[error("raw_by_hash entry missing for sha256={sha256_hex}")]
    MissingRawByHash { sha256_hex: String },

    #[error("storage error: {0}")]
    Storage(String),
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CirStaticPoolWriteResult {
    pub final_path: PathBuf,
    pub written: bool,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CirStaticPoolExportSummary {
    pub unique_hashes: usize,
    pub written_files: usize,
    pub reused_files: usize,
}

pub fn static_pool_relative_path(
    capture_date_utc: time::Date,
    sha256_hex: &str,
) -> Result<PathBuf, CirStaticPoolError> {
    validate_sha256_hex(sha256_hex)?;
    let date = format_utc_date(capture_date_utc);
    Ok(PathBuf::from(date)
        .join(&sha256_hex[0..2])
        .join(&sha256_hex[2..4])
        .join(sha256_hex))
}

pub fn static_pool_path(
    static_root: &Path,
    capture_date_utc: time::Date,
    sha256_hex: &str,
) -> Result<PathBuf, CirStaticPoolError> {
    Ok(static_root.join(static_pool_relative_path(capture_date_utc, sha256_hex)?))
}

pub fn write_bytes_to_static_pool(
    static_root: &Path,
    capture_date_utc: time::Date,
    sha256_hex_value: &str,
    bytes: &[u8],
) -> Result<CirStaticPoolWriteResult, CirStaticPoolError> {
    validate_sha256_hex(sha256_hex_value)?;
    if bytes.is_empty() {
        return Err(CirStaticPoolError::EmptyBytes {
            sha256_hex: sha256_hex_value.to_string(),
        });
    }
    let computed = compute_sha256_hex(bytes);
    if computed != sha256_hex_value.to_ascii_lowercase() {
        return Err(CirStaticPoolError::HashMismatch {
            sha256_hex: sha256_hex_value.to_string(),
        });
    }

    let final_path = static_pool_path(static_root, capture_date_utc, sha256_hex_value)?;
    if final_path.exists() {
        return Ok(CirStaticPoolWriteResult {
            final_path,
            written: false,
        });
    }

    let parent = final_path.parent().expect("static pool file has parent");
    fs::create_dir_all(parent).map_err(|e| CirStaticPoolError::CreateDir {
        path: parent.display().to_string(),
        detail: e.to_string(),
    })?;

    let temp_path = parent.join(format!("{sha256_hex_value}.tmp.{}", uuid::Uuid::new_v4()));
    let mut file = OpenOptions::new()
        .create_new(true)
        .write(true)
        .open(&temp_path)
        .map_err(|e| CirStaticPoolError::CreateTemp {
            path: temp_path.display().to_string(),
            detail: e.to_string(),
        })?;
    file.write_all(bytes)
        .map_err(|e| CirStaticPoolError::WriteTemp {
            path: temp_path.display().to_string(),
            detail: e.to_string(),
        })?;
    file.sync_all().map_err(|e| CirStaticPoolError::SyncTemp {
        path: temp_path.display().to_string(),
        detail: e.to_string(),
    })?;
    drop(file);

    match fs::hard_link(&temp_path, &final_path) {
        Ok(()) => {
            fs::remove_file(&temp_path).map_err(|e| CirStaticPoolError::RemoveTemp {
                path: temp_path.display().to_string(),
                detail: e.to_string(),
            })?;
            Ok(CirStaticPoolWriteResult {
                final_path,
                written: true,
            })
        }
        Err(e) if final_path.exists() => {
            fs::remove_file(&temp_path).map_err(|remove_err| CirStaticPoolError::RemoveTemp {
                path: temp_path.display().to_string(),
                detail: remove_err.to_string(),
            })?;
            let _ = e;
            Ok(CirStaticPoolWriteResult {
                final_path,
                written: false,
            })
        }
        Err(e) => Err(CirStaticPoolError::Publish {
            temp_path: temp_path.display().to_string(),
            final_path: final_path.display().to_string(),
            detail: e.to_string(),
        }),
    }
}

pub fn write_raw_entry_to_static_pool(
    static_root: &Path,
    capture_date_utc: time::Date,
    entry: &RawByHashEntry,
) -> Result<CirStaticPoolWriteResult, CirStaticPoolError> {
    write_bytes_to_static_pool(
        static_root,
        capture_date_utc,
        &entry.sha256_hex,
        &entry.bytes,
    )
}

pub fn export_hashes_from_store(
    store: &RocksStore,
    static_root: &Path,
    capture_date_utc: time::Date,
    sha256_hexes: &[String],
) -> Result<CirStaticPoolExportSummary, CirStaticPoolError> {
    let unique: BTreeSet<String> = sha256_hexes
        .iter()
        .map(|item| item.to_ascii_lowercase())
        .collect();

    let mut written_files = 0usize;
    let mut reused_files = 0usize;
    for sha256_hex in &unique {
        let bytes = store
            .get_blob_bytes(sha256_hex)
            .map_err(|e| CirStaticPoolError::Storage(e.to_string()))?
            .ok_or_else(|| CirStaticPoolError::MissingRawByHash {
                sha256_hex: sha256_hex.clone(),
            })?;
        let entry = RawByHashEntry::from_bytes(sha256_hex.clone(), bytes);
        let result = write_raw_entry_to_static_pool(static_root, capture_date_utc, &entry)?;
        if result.written {
            written_files += 1;
        } else {
            reused_files += 1;
        }
    }

    Ok(CirStaticPoolExportSummary {
        unique_hashes: unique.len(),
        written_files,
        reused_files,
    })
}

fn format_utc_date(date: time::Date) -> String {
    format!(
        "{:04}{:02}{:02}",
        date.year(),
        u8::from(date.month()),
        date.day()
    )
}

fn validate_sha256_hex(sha256_hex: &str) -> Result<(), CirStaticPoolError> {
    if sha256_hex.len() != 64 || !sha256_hex.as_bytes().iter().all(u8::is_ascii_hexdigit) {
        return Err(CirStaticPoolError::InvalidSha256Hex(sha256_hex.to_string()));
    }
    Ok(())
}

fn compute_sha256_hex(bytes: &[u8]) -> String {
    use sha2::{Digest, Sha256};
    hex::encode(Sha256::digest(bytes))
}

#[cfg(test)]
mod tests {
    use super::{
        CirStaticPoolError, compute_sha256_hex, export_hashes_from_store,
        static_pool_relative_path, write_bytes_to_static_pool,
    };
    use crate::storage::{RawByHashEntry, RepositoryViewEntry, RepositoryViewState, RocksStore};
    use std::fs;

    fn sample_date() -> time::Date {
        time::Date::from_calendar_date(2026, time::Month::April, 7).unwrap()
    }

    #[test]
    fn static_pool_relative_path_uses_date_and_hash_prefixes() {
        let path = static_pool_relative_path(
            sample_date(),
            "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
        )
        .expect("relative path");
        assert_eq!(
            path,
            std::path::PathBuf::from("20260407")
                .join("ab")
                .join("cd")
                .join("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789")
        );
    }

    #[test]
    fn write_bytes_to_static_pool_is_idempotent_and_leaves_no_temp_files() {
        let td = tempfile::tempdir().expect("tempdir");
        let bytes = b"static-pool-object";
        let sha = compute_sha256_hex(bytes);

        let first =
            write_bytes_to_static_pool(td.path(), sample_date(), &sha, bytes).expect("first write");
        let second = write_bytes_to_static_pool(td.path(), sample_date(), &sha, bytes)
            .expect("second write");

        assert!(first.written);
        assert!(!second.written);
        assert_eq!(fs::read(&first.final_path).expect("read final"), bytes);

        let all_files: Vec<_> = walk_files(td.path());
        assert_eq!(all_files.len(), 1);
        assert!(
            !all_files[0]
                .file_name()
                .and_then(|name| name.to_str())
                .unwrap_or_default()
                .contains(".tmp.")
        );
    }

    #[test]
    fn write_bytes_to_static_pool_rejects_bad_hash_and_empty_bytes() {
        let td = tempfile::tempdir().expect("tempdir");
        let err = write_bytes_to_static_pool(td.path(), sample_date(), "not-a-hash", b"x")
            .expect_err("bad hash must fail");
        assert!(matches!(err, CirStaticPoolError::InvalidSha256Hex(_)));

        let err = write_bytes_to_static_pool(
            td.path(),
            sample_date(),
            "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
            b"",
        )
        .expect_err("empty bytes must fail");
        assert!(matches!(err, CirStaticPoolError::EmptyBytes { .. }));
    }

    #[test]
    fn export_hashes_from_store_writes_unique_entries_and_fails_when_missing() {
        let td = tempfile::tempdir().expect("tempdir");
        let store_dir = td.path().join("db");
        let static_root = td.path().join("static");
        let store = RocksStore::open(&store_dir).expect("open rocksdb");

        let bytes = b"store-object".to_vec();
        let sha = compute_sha256_hex(&bytes);
        let mut entry = RawByHashEntry::from_bytes(sha.clone(), bytes.clone());
        entry
            .origin_uris
            .push("rsync://example.test/repo/object.cer".to_string());
        store.put_raw_by_hash_entry(&entry).expect("put raw entry");
        store
            .put_repository_view_entry(&RepositoryViewEntry {
                rsync_uri: "rsync://example.test/repo/object.cer".to_string(),
                current_hash: Some(sha.clone()),
                repository_source: Some("https://rrdp.example.test/notification.xml".to_string()),
                object_type: Some("cer".to_string()),
                state: RepositoryViewState::Present,
            })
            .expect("put repository view");

        let summary = export_hashes_from_store(
            &store,
            &static_root,
            sample_date(),
            &[sha.clone(), sha.clone()],
        )
        .expect("export hashes");
        assert_eq!(summary.unique_hashes, 1);
        assert_eq!(summary.written_files, 1);
        assert_eq!(summary.reused_files, 0);

        let summary = export_hashes_from_store(&store, &static_root, sample_date(), &[sha.clone()])
            .expect("re-export hashes");
        assert_eq!(summary.unique_hashes, 1);
        assert_eq!(summary.written_files, 0);
        assert_eq!(summary.reused_files, 1);

        let err = export_hashes_from_store(
            &store,
            &static_root,
            sample_date(),
            &[String::from(
                "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
            )],
        )
        .expect_err("missing raw_by_hash must fail");
        assert!(matches!(err, CirStaticPoolError::MissingRawByHash { .. }));
    }

    fn walk_files(root: &std::path::Path) -> Vec<std::path::PathBuf> {
        let mut out = Vec::new();
        let mut stack = vec![root.to_path_buf()];
        while let Some(path) = stack.pop() {
            for entry in fs::read_dir(path).expect("read_dir") {
                let entry = entry.expect("dir entry");
                let path = entry.path();
                if path.is_dir() {
                    stack.push(path);
                } else {
                    out.push(path);
                }
            }
        }
        out.sort();
        out
    }
}
