use std::path::Path;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Without these, edits to the .proto don't reliably retrigger
    // codegen on incremental builds.
    println!("cargo:rerun-if-changed=proto/daemon.proto");
    println!("cargo:rerun-if-changed=proto/broker_v1/broker_v1_envelope.proto");
    println!("cargo:rerun-if-changed=proto/broker_v1/broker_v1_admin.proto");
    println!("cargo:rerun-if-changed=proto/broker_v1/broker_v1_manifest.proto");
    println!("cargo:rerun-if-changed=proto/broker_v1/broker_v1_service_def.proto");
    println!("cargo:rerun-if-changed=proto/broker_v2/broker_v2_service_def.proto");
    println!("cargo:rerun-if-changed=proto/broker_v2/broker_v2_control.proto");
    println!("cargo:rerun-if-changed=proto/broker_v2/broker_v2_manifest.proto");
    println!("cargo:rerun-if-changed=build.rs");
    let file_descriptors = protox::compile(
        [
            "proto/daemon.proto",
            "proto/broker_v1/broker_v1_envelope.proto",
            "proto/broker_v1/broker_v1_admin.proto",
            "proto/broker_v1/broker_v1_manifest.proto",
            "proto/broker_v1/broker_v1_service_def.proto",
            "proto/broker_v2/broker_v2_service_def.proto",
            "proto/broker_v2/broker_v2_control.proto",
            "proto/broker_v2/broker_v2_manifest.proto",
        ],
        ["proto/"],
    )?;
    prost_build::compile_fds(file_descriptors)?;

    // #447: bake the per-arch SHA-256 verification table for the Win10
    // ConPTY sidecar into the crate. The release workflow rewrites
    // `conpty-sidecar.sha256.toml` in-place before this build runs.
    generate_sidecar_hash_table()?;
    Ok(())
}

fn generate_sidecar_hash_table() -> Result<(), Box<dyn std::error::Error>> {
    let manifest_path = locate_workspace_manifest();
    println!("cargo:rerun-if-changed={}", manifest_path.to_string_lossy());

    let parsed = read_manifest(&manifest_path);
    let rendered = render_hash_table(&parsed);

    let out_dir = std::env::var_os("OUT_DIR").expect("cargo always sets OUT_DIR for build.rs");
    let dest = Path::new(&out_dir).join("conpty_sidecar_hashes.rs");
    std::fs::write(&dest, rendered)?;
    Ok(())
}

fn locate_workspace_manifest() -> std::path::PathBuf {
    // `CARGO_MANIFEST_DIR` is `<workspace>/crates/running-process` for this
    // crate; the manifest TOML lives two levels up at the workspace root.
    let manifest_dir = std::env::var_os("CARGO_MANIFEST_DIR")
        .expect("cargo always sets CARGO_MANIFEST_DIR for build.rs");
    Path::new(&manifest_dir)
        .join("..")
        .join("..")
        .join("conpty-sidecar.sha256.toml")
}

/// `Some((sha256_hex, size_bytes))` per arch when populated; `None` when
/// the manifest omits the arch (or the file is missing / all-comments).
struct ParsedManifest {
    x64: Option<(String, u64)>,
    arm64: Option<(String, u64)>,
    x86: Option<(String, u64)>,
    arm: Option<(String, u64)>,
}

fn read_manifest(path: &Path) -> ParsedManifest {
    let mut parsed = ParsedManifest {
        x64: None,
        arm64: None,
        x86: None,
        arm: None,
    };
    let Ok(raw) = std::fs::read_to_string(path) else {
        println!(
            "cargo:warning=conpty-sidecar.sha256.toml not found at {}; \
             runtime will skip SHA-256 verification on Win10 sidecar fetch",
            path.display()
        );
        return parsed;
    };
    let table: toml::Value = match raw.parse() {
        Ok(v) => v,
        Err(e) => {
            println!(
                "cargo:warning=failed to parse conpty-sidecar.sha256.toml: {e}; \
                 runtime will skip SHA-256 verification"
            );
            return parsed;
        }
    };
    let Some(assets) = table.get("asset").and_then(|v| v.as_table()) else {
        return parsed;
    };
    for arch in ["x64", "arm64", "x86", "arm"] {
        let Some(entry) = assets.get(arch).and_then(|v| v.as_table()) else {
            continue;
        };
        let Some(sha) = entry.get("sha256").and_then(|v| v.as_str()) else {
            continue;
        };
        let size = entry
            .get("size_bytes")
            .and_then(|v| v.as_integer())
            .map(|n| n.max(0) as u64)
            .unwrap_or(0);
        let slot = match arch {
            "x64" => &mut parsed.x64,
            "arm64" => &mut parsed.arm64,
            "x86" => &mut parsed.x86,
            "arm" => &mut parsed.arm,
            _ => unreachable!(),
        };
        *slot = Some((sha.to_owned(), size));
    }
    parsed
}

fn render_hash_table(parsed: &ParsedManifest) -> String {
    let mut out = String::new();
    out.push_str(
        "// Generated by build.rs at compile time from \
         `conpty-sidecar.sha256.toml`. Do not edit.\n\n\
         pub(super) struct ExpectedAsset {\n    \
             pub sha256_hex: &'static str,\n    \
             pub size_bytes: u64,\n\
         }\n\n",
    );
    for (name, value) in [
        ("EXPECTED_X64", &parsed.x64),
        ("EXPECTED_ARM64", &parsed.arm64),
        ("EXPECTED_X86", &parsed.x86),
        ("EXPECTED_ARM", &parsed.arm),
    ] {
        let body = match value {
            Some((sha, size)) => {
                format!("Some(ExpectedAsset {{ sha256_hex: \"{sha}\", size_bytes: {size} }})",)
            }
            None => "None".to_owned(),
        };
        // Only one arch's constant is used per build; suppress the
        // dead-code warnings on the rest without disabling them for
        // genuinely unused items elsewhere.
        out.push_str(&format!(
            "#[allow(dead_code)]\n\
             pub(super) const {name}: Option<ExpectedAsset> = {body};\n",
        ));
    }
    out
}
