use serde::Deserialize;
use std::{
    collections::HashSet,
    env, fs,
    path::{Path, PathBuf},
};

const ACCESS_CONTROL_BYPASS_CATALOG: &str = "data/access_control_bypass_headers.toml";

#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct AccessControlBypassCatalog {
    bypass: Vec<AccessControlBypassEntry>,
}

#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct AccessControlBypassEntry {
    header: String,
    value: AccessControlBypassValueSpec,
    label: String,
}

#[derive(Debug, Deserialize)]
#[serde(tag = "strategy", rename_all = "kebab-case", deny_unknown_fields)]
enum AccessControlBypassValueSpec {
    ProtectedPath,
    Static { value: String },
}

fn main() {
    let manifest_dir = PathBuf::from(
        env::var_os("CARGO_MANIFEST_DIR")
            .unwrap_or_else(|| panic!("CARGO_MANIFEST_DIR is not set for scanclient build")),
    );
    let catalog_path = manifest_dir.join(ACCESS_CONTROL_BYPASS_CATALOG);
    println!("cargo:rerun-if-changed={}", catalog_path.display());
    println!("cargo:rerun-if-changed=build.rs");

    let catalog = load_access_control_bypass_catalog(&catalog_path);
    validate_access_control_bypass_catalog(&catalog, &catalog_path);
    write_access_control_bypass_catalog(&catalog);
}

fn load_access_control_bypass_catalog(path: &Path) -> AccessControlBypassCatalog {
    let contents = fs::read_to_string(path)
        .unwrap_or_else(|error| panic!("failed to read {}: {error}", path.display()));
    toml::from_str(&contents)
        .unwrap_or_else(|error| panic!("failed to parse {}: {error}", path.display()))
}

fn validate_access_control_bypass_catalog(catalog: &AccessControlBypassCatalog, path: &Path) {
    if catalog.bypass.is_empty() {
        panic!(
            "{} must contain at least one [[bypass]] entry",
            path.display()
        );
    }

    let mut seen_headers = HashSet::with_capacity(catalog.bypass.len());
    for entry in &catalog.bypass {
        validate_non_empty_trimmed(&entry.header, "header", path);
        validate_non_empty_trimmed(&entry.label, "label", path);
        if !seen_headers.insert(entry.header.to_ascii_lowercase()) {
            panic!(
                "{} contains duplicate bypass header {:?}",
                path.display(),
                entry.header
            );
        }
        if let AccessControlBypassValueSpec::Static { value } = &entry.value {
            validate_non_empty_trimmed(value, "static value", path);
        }
    }
}

fn validate_non_empty_trimmed(value: &str, field: &str, path: &Path) {
    if value.is_empty() || value.trim() != value {
        panic!(
            "{} contains invalid {field} {:?}; values must be non-empty and trimmed",
            path.display(),
            value
        );
    }
}

fn write_access_control_bypass_catalog(catalog: &AccessControlBypassCatalog) {
    let out_dir = PathBuf::from(
        env::var_os("OUT_DIR").unwrap_or_else(|| panic!("OUT_DIR is not set for scanclient build")),
    );
    let generated_path = out_dir.join("access_control_bypass_headers.rs");
    let mut generated = String::from(
        "/// Access-control bypass probes generated from `data/access_control_bypass_headers.toml`.\n",
    );
    generated.push_str(
        "pub const ACCESS_CONTROL_BYPASS_HEADER_TEMPLATES: &[AccessControlBypassTemplate] = &[\n",
    );

    for entry in &catalog.bypass {
        generated.push_str("    AccessControlBypassTemplate {\n");
        generated.push_str(&format!("        name: {},\n", quoted(&entry.header)));
        generated.push_str("        value: ");
        match &entry.value {
            AccessControlBypassValueSpec::ProtectedPath => {
                generated.push_str("AccessControlBypassValue::RewritePath");
            }
            AccessControlBypassValueSpec::Static { value } => {
                generated.push_str(&format!(
                    "AccessControlBypassValue::Static({})",
                    quoted(value)
                ));
            }
        }
        generated.push_str(",\n");
        generated.push_str(&format!("        label: {},\n", quoted(&entry.label)));
        generated.push_str("    },\n");
    }

    generated.push_str("];\n");
    fs::write(&generated_path, generated).unwrap_or_else(|error| {
        panic!("failed to write {}: {error}", generated_path.display());
    });
}

fn quoted(value: &str) -> String {
    format!("{value:?}")
}
