use std::fs;
use std::path::Path;

use serde::Serialize;
use sha2::Digest;
use time::format_description::well_known::Rfc3339;

use super::{BundleManifest, BundleManifestEntry};

pub fn sha256_hex(bytes: &[u8]) -> String {
    hex::encode(sha2::Sha256::digest(bytes))
}

pub fn write_json(path: &Path, value: &impl Serialize) -> Result<(), String> {
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)
            .map_err(|e| format!("create parent failed: {}: {e}", parent.display()))?;
    }
    let bytes = serde_json::to_vec_pretty(value).map_err(|e| e.to_string())?;
    fs::write(path, bytes).map_err(|e| format!("write json failed: {}: {e}", path.display()))
}

pub fn write_timing_json(
    path: &Path,
    mode: &str,
    validation_time: &time::OffsetDateTime,
    duration: std::time::Duration,
) -> Result<(), String> {
    write_json(
        path,
        &serde_json::json!({
            "mode": mode,
            "validationTime": validation_time
                .format(&Rfc3339)
                .map_err(|e| format!("format validation time failed: {e}"))?,
            "durationSeconds": duration.as_secs_f64(),
        }),
    )
}

pub fn write_live_bundle_top_readme(path: &Path, rir: &str) -> Result<(), String> {
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)
            .map_err(|e| format!("create parent failed: {}: {e}", parent.display()))?;
    }
    fs::write(
        path,
        format!(
            "# Ours Live Replay Bundle\n\nThis run contains one per-RIR bundle recorded online by `ours`.\n\n- RIR: `{rir}`\n- Reference result format: `CCR`\n"
        ),
    )
    .map_err(|e| format!("write readme failed: {}: {e}", path.display()))
}

pub fn write_live_bundle_rir_readme(
    path: &Path,
    rir: &str,
    base_validation_time: &str,
) -> Result<(), String> {
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)
            .map_err(|e| format!("create parent failed: {}: {e}", parent.display()))?;
    }
    fs::write(
        path,
        format!(
            "# {rir} live replay bundle\n\n- `tal.tal` and `ta.cer` are the actual live run inputs.\n- `base-locks.json.validationTime` = `{base_validation_time}`.\n- `base.ccr` is the authoritative reference result.\n- `base-vrps.csv` and `base-vaps.csv` are compare views derived from `base.ccr`.\n"
        ),
    )
    .map_err(|e| format!("write rir readme failed: {}: {e}", path.display()))
}

pub fn write_bytes(path: &Path, bytes: &[u8]) -> Result<(), String> {
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)
            .map_err(|e| format!("create parent failed: {}: {e}", parent.display()))?;
    }
    fs::write(path, bytes).map_err(|e| format!("write file failed: {}: {e}", path.display()))
}

pub fn copy_dir_all(src: &Path, dst: &Path) -> Result<(), String> {
    fs::create_dir_all(dst)
        .map_err(|e| format!("create directory failed: {}: {e}", dst.display()))?;
    for entry in fs::read_dir(src).map_err(|e| format!("read_dir failed: {}: {e}", src.display()))? {
        let entry = entry.map_err(|e| format!("read_dir entry failed: {}: {e}", src.display()))?;
        let ty = entry
            .file_type()
            .map_err(|e| format!("file_type failed: {}: {e}", entry.path().display()))?;
        let to = dst.join(entry.file_name());
        if ty.is_dir() {
            copy_dir_all(&entry.path(), &to)?;
        } else if ty.is_file() {
            if let Some(parent) = to.parent() {
                fs::create_dir_all(parent)
                    .map_err(|e| format!("create parent failed: {}: {e}", parent.display()))?;
            }
            fs::copy(entry.path(), &to)
                .map_err(|e| format!("copy failed: {} -> {}: {e}", entry.path().display(), to.display()))?;
        }
    }
    Ok(())
}

pub fn load_validation_time(path: &Path) -> Result<time::OffsetDateTime, String> {
    let json: serde_json::Value = serde_json::from_slice(
        &fs::read(path).map_err(|e| format!("read json failed: {}: {e}", path.display()))?,
    )
    .map_err(|e| format!("parse json failed: {}: {e}", path.display()))?;
    let value = json
        .get("validationTime")
        .or_else(|| json.get("validation_time"))
        .and_then(|v| v.as_str())
        .ok_or_else(|| format!("validationTime missing in {}", path.display()))?;
    time::OffsetDateTime::parse(value, &Rfc3339)
        .map_err(|e| format!("invalid validationTime in {}: {e}", path.display()))
}

pub fn build_single_rir_bundle_manifest(
    schema_version: &str,
    bundle_producer: &str,
    rir: &str,
    base_validation_time: &time::OffsetDateTime,
    delta_validation_time: Option<&time::OffsetDateTime>,
    has_aspa: bool,
) -> Result<BundleManifest, String> {
    Ok(BundleManifest {
        schema_version: schema_version.to_string(),
        bundle_producer: bundle_producer.to_string(),
        recorded_at_rfc3339_utc: time::OffsetDateTime::now_utc()
            .format(&Rfc3339)
            .map_err(|e| format!("format recorded_at failed: {e}"))?,
        rirs: vec![rir.to_string()],
        per_rir_bundles: vec![BundleManifestEntry {
            rir: rir.to_string(),
            relative_path: rir.to_string(),
            base_validation_time: base_validation_time
                .format(&Rfc3339)
                .map_err(|e| format!("format base validation time failed: {e}"))?,
            delta_validation_time: match delta_validation_time {
                Some(value) => Some(
                    value
                        .format(&Rfc3339)
                        .map_err(|e| format!("format delta validation time failed: {e}"))?,
                ),
                None => None,
            },
            has_aspa,
        }],
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use tempfile::tempdir;

    #[test]
    fn load_validation_time_reads_validation_time_field() {
        let td = tempdir().expect("tempdir");
        let path = td.path().join("locks.json");
        fs::write(&path, r#"{"validationTime":"2026-04-01T00:00:00Z"}"#).expect("write");
        let parsed = load_validation_time(&path).expect("load");
        assert_eq!(
            parsed.format(&Rfc3339).expect("format"),
            "2026-04-01T00:00:00Z"
        );
    }

    #[test]
    fn copy_dir_all_copies_nested_files() {
        let td = tempdir().expect("tempdir");
        let src = td.path().join("src");
        let dst = td.path().join("dst");
        fs::create_dir_all(src.join("nested")).expect("mkdir");
        fs::write(src.join("root.txt"), b"root").expect("write root");
        fs::write(src.join("nested/child.txt"), b"child").expect("write child");
        copy_dir_all(&src, &dst).expect("copy");
        assert_eq!(fs::read(dst.join("root.txt")).expect("read root"), b"root");
        assert_eq!(
            fs::read(dst.join("nested/child.txt")).expect("read child"),
            b"child"
        );
    }

    #[test]
    fn build_single_rir_bundle_manifest_formats_times() {
        let base = time::OffsetDateTime::parse("2026-04-01T00:00:00Z", &Rfc3339).expect("base");
        let delta = time::OffsetDateTime::parse("2026-04-01T00:10:00Z", &Rfc3339).expect("delta");
        let manifest = build_single_rir_bundle_manifest(
            "20260330-v1",
            "ours",
            "apnic",
            &base,
            Some(&delta),
            true,
        )
        .expect("manifest");
        assert_eq!(manifest.schema_version, "20260330-v1");
        assert_eq!(manifest.rirs, vec!["apnic".to_string()]);
        assert_eq!(manifest.per_rir_bundles[0].base_validation_time, "2026-04-01T00:00:00Z");
        assert_eq!(
            manifest.per_rir_bundles[0].delta_validation_time.as_deref(),
            Some("2026-04-01T00:10:00Z")
        );
    }

    #[test]
    fn write_json_and_write_bytes_create_parent_directories() {
        let td = tempdir().expect("tempdir");
        let json_path = td.path().join("nested/meta/data.json");
        write_json(&json_path, &serde_json::json!({"ok": true})).expect("write json");
        let json: serde_json::Value =
            serde_json::from_slice(&fs::read(&json_path).expect("read json")).expect("parse");
        assert_eq!(json["ok"], true);

        let bytes_path = td.path().join("nested/raw/file.bin");
        write_bytes(&bytes_path, b"payload").expect("write bytes");
        assert_eq!(fs::read(&bytes_path).expect("read bytes"), b"payload");
    }

    #[test]
    fn write_timing_and_readmes_emit_expected_text() {
        let td = tempdir().expect("tempdir");
        let timing_path = td.path().join("timings/base-produce.json");
        let validation_time =
            time::OffsetDateTime::parse("2026-04-01T00:00:00Z", &Rfc3339).expect("time");
        write_timing_json(
            &timing_path,
            "base",
            &validation_time,
            std::time::Duration::from_secs_f64(1.25),
        )
        .expect("write timing");
        let timing: serde_json::Value =
            serde_json::from_slice(&fs::read(&timing_path).expect("read timing")).expect("parse");
        assert_eq!(timing["mode"], "base");
        assert_eq!(timing["validationTime"], "2026-04-01T00:00:00Z");
        assert_eq!(timing["durationSeconds"], 1.25);

        let top_readme = td.path().join("README.md");
        write_live_bundle_top_readme(&top_readme, "apnic").expect("write top readme");
        let top_text = fs::read_to_string(&top_readme).expect("read top readme");
        assert!(top_text.contains("RIR: `apnic`"));
        assert!(top_text.contains("Reference result format: `CCR`"));

        let rir_readme = td.path().join("apnic/README.md");
        write_live_bundle_rir_readme(&rir_readme, "apnic", "2026-04-01T00:00:00Z")
            .expect("write rir readme");
        let rir_text = fs::read_to_string(&rir_readme).expect("read rir readme");
        assert!(rir_text.contains("base-locks.json.validationTime"));
        assert!(rir_text.contains("base-vrps.csv"));
        assert!(rir_text.contains("base-vaps.csv"));
    }

    #[test]
    fn build_single_rir_bundle_manifest_supports_none_delta_time() {
        let base = time::OffsetDateTime::parse("2026-04-01T00:00:00Z", &Rfc3339).expect("base");
        let manifest = build_single_rir_bundle_manifest(
            "20260330-v1",
            "ours",
            "afrinic",
            &base,
            None,
            false,
        )
        .expect("manifest");
        assert_eq!(manifest.per_rir_bundles[0].delta_validation_time, None);
        assert!(!manifest.per_rir_bundles[0].has_aspa);
    }
}
