//! The build script

use std::{env, fs, path::PathBuf};

#[derive(Debug)]
struct Operation {
    method: String,
    path: String,
}

fn main() {
    println!("cargo:rerun-if-changed=API/openapi.yaml");

    let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR"));
    let spec_path = manifest_dir.join("API/openapi.yaml");
    let spec = fs::read_to_string(&spec_path).expect("failed to read API/openapi.yaml");

    let server_url = parse_server_url(&spec).unwrap_or("https://api.drivethrurpg.com/api");
    let operations = parse_operations(&spec);

    let mut generated = String::new();
    generated.push_str("// Generated by build.rs from API/openapi.yaml. Do not edit.\n\n");
    generated.push_str(
        "/// A single `method`/`path` operation parsed from the OpenAPI spec's `paths:` section.\n",
    );
    generated.push_str("#[derive(Clone, Copy, Debug, Eq, PartialEq)]\n");
    generated.push_str("pub struct OpenApiOperation {\n");
    generated.push_str("    /// The HTTP method, upper-cased (e.g. `\"GET\"`).\n");
    generated.push_str("    pub method: &'static str,\n");
    generated.push_str(
        "    /// The request path template, as written in the spec (e.g. `\"/order_products\"`).\n",
    );
    generated.push_str("    pub path: &'static str,\n");
    generated.push_str("}\n\n");
    generated.push_str(
        "/// The API server base URL declared in `API/openapi.yaml`'s `servers:` section, or the\n",
    );
    generated
        .push_str("/// production DriveThruRPG API URL if the spec has no `servers:` entry.\n");
    generated.push_str(&format!(
        "pub const DEFAULT_SERVER_URL: &str = {:?};\n",
        server_url
    ));
    generated.push_str("\n/// The byte length of `API/openapi.yaml` at build time.\n");
    generated.push_str(
        "///\n/// Exists as a build-time sanity check that the spec file was read successfully\n",
    );
    generated.push_str("/// and is non-empty; not otherwise meaningful to consumers.\n");
    generated.push_str(&format!(
        "pub const OPENAPI_SPEC_BYTES: usize = {};\n",
        spec.len()
    ));
    generated.push_str(
        "\n/// Every `method`/`path` operation declared under `API/openapi.yaml`'s `paths:` section.\n",
    );
    generated.push_str("pub const OPERATIONS: &[OpenApiOperation] = &[\n");

    for operation in operations {
        generated.push_str(&format!(
            "    OpenApiOperation {{ method: {:?}, path: {:?} }},\n",
            operation.method, operation.path
        ));
    }

    generated.push_str("];\n");

    let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR"));
    fs::write(out_dir.join("openapi.rs"), generated)
        .expect("failed to write generated OpenAPI Rust");
}

fn parse_server_url(spec: &str) -> Option<&str> {
    let mut in_servers = false;

    for line in spec.lines() {
        let trimmed = line.trim();
        if trimmed == "servers:" {
            in_servers = true;
            continue;
        }

        if in_servers && trimmed.starts_with("- url:") {
            return trimmed.split_once(':').map(|(_, value)| value.trim());
        }

        if in_servers && !line.starts_with(' ') && !trimmed.is_empty() {
            break;
        }
    }

    None
}

fn parse_operations(spec: &str) -> Vec<Operation> {
    let mut operations = Vec::new();
    let mut in_paths = false;
    let mut current_path: Option<String> = None;

    for line in spec.lines() {
        let trimmed = line.trim();

        if trimmed == "paths:" {
            in_paths = true;
            continue;
        }

        if in_paths && !line.starts_with(' ') && !trimmed.is_empty() {
            break;
        }

        if !in_paths {
            continue;
        }

        if line.starts_with("  /") && trimmed.ends_with(':') {
            current_path = Some(trimmed.trim_end_matches(':').to_string());
            continue;
        }

        if line.starts_with("    ") && !line.starts_with("      ") && trimmed.ends_with(':') {
            let method = trimmed.trim_end_matches(':');
            if matches!(method, "get" | "post" | "put" | "patch" | "delete")
                && let Some(path) = &current_path
            {
                operations.push(Operation {
                    method: method.to_ascii_uppercase(),
                    path: path.clone(),
                });
            }
        }
    }

    operations
}
