use std::collections::BTreeSet;
use std::fs;
use std::io::Read;
use std::path::Path;

use rrt_model::{
    BINARY_SUMMARY_PATH, CANONICAL_EXE_PATH, CONTROL_LOOP_ATLAS_PATH, FUNCTION_MAP_PATH,
    REQUIRED_ATLAS_HEADINGS, REQUIRED_EXPORTS, load_binary_summary, load_function_map,
};
use sha2::{Digest, Sha256};

pub(crate) fn validate_required_files(repo_root: &Path) -> Result<(), Box<dyn std::error::Error>> {
    let mut missing = Vec::new();
    for relative in REQUIRED_EXPORTS {
        let path = repo_root.join(relative);
        if !path.exists() {
            missing.push(path.display().to_string());
        }
    }

    if !missing.is_empty() {
        return Err(format!("missing required exports: {}", missing.join(", ")).into());
    }

    Ok(())
}

pub(crate) fn validate_binary_summary(repo_root: &Path) -> Result<(), Box<dyn std::error::Error>> {
    let summary = load_binary_summary(&repo_root.join(BINARY_SUMMARY_PATH))?;
    let actual_exe = repo_root.join(CANONICAL_EXE_PATH);
    if !actual_exe.exists() {
        return Err(format!("canonical exe missing: {}", actual_exe.display()).into());
    }

    let actual_hash = sha256_file(&actual_exe)?;
    if actual_hash != summary.sha256 {
        return Err(format!(
            "hash mismatch for {}: summary has {}, actual file is {}",
            actual_exe.display(),
            summary.sha256,
            actual_hash
        )
        .into());
    }

    let docs_readme = fs::read_to_string(repo_root.join("docs/README.md"))?;
    if !docs_readme.contains(&summary.sha256) {
        return Err("docs/README.md does not include the canonical SHA-256".into());
    }

    Ok(())
}

pub(crate) fn validate_function_map(repo_root: &Path) -> Result<(), Box<dyn std::error::Error>> {
    let records = load_function_map(&repo_root.join(FUNCTION_MAP_PATH))?;
    let mut seen = BTreeSet::new();

    for record in records {
        if !(1..=5).contains(&record.confidence) {
            return Err(format!(
                "invalid confidence {} for {} {}",
                record.confidence, record.address, record.name
            )
            .into());
        }

        if !seen.insert(record.address) {
            return Err(format!("duplicate function address {}", record.address).into());
        }

        if record.name.trim().is_empty() {
            return Err(format!("blank function name at {}", record.address).into());
        }
    }

    Ok(())
}

pub(crate) fn validate_control_loop_atlas(
    repo_root: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
    let atlas = fs::read_to_string(repo_root.join(CONTROL_LOOP_ATLAS_PATH))?;
    for heading in REQUIRED_ATLAS_HEADINGS {
        if !atlas.contains(heading) {
            return Err(format!("missing atlas heading `{heading}`").into());
        }
    }

    for marker in [
        "- Roots:",
        "- Trigger/Cadence:",
        "- Key Dispatchers:",
        "- State Anchors:",
        "- Subsystem Handoffs:",
        "- Evidence:",
        "- Open Questions:",
    ] {
        if !atlas.contains(marker) {
            return Err(format!("atlas is missing field marker `{marker}`").into());
        }
    }

    Ok(())
}

fn sha256_file(path: &Path) -> Result<String, Box<dyn std::error::Error>> {
    let mut file = fs::File::open(path)?;
    let mut hasher = Sha256::new();
    let mut buffer = [0_u8; 8192];
    loop {
        let read = file.read(&mut buffer)?;
        if read == 0 {
            break;
        }
        hasher.update(&buffer[..read]);
    }

    Ok(format!("{:x}", hasher.finalize()))
}
