use std::collections::HashMap;
use std::path::Path;

use serde::Deserialize;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    tonic_prost_build::configure()
        .build_server(false)
        .compile_protos(
            &["proto/endpoints.proto", "proto/v3_endpoints.proto"],
            &["proto"],
        )?;

    generate_endpoint_registry()?;
    generate_tick_types()?;
    Ok(())
}

// ─────────────────────────────────────────────────────────────────────────────
//  Code-generate tick structs + parsers from endpoint_schema.toml
// ─────────────────────────────────────────────────────────────────────────────

#[derive(Debug, Deserialize)]
struct Schema {
    types: HashMap<String, TickTypeDef>,
}

#[derive(Debug, Deserialize)]
struct TickTypeDef {
    doc: String,
    copy: bool,
    #[serde(default)]
    align: Option<u32>,
    parser: String,
    #[serde(default)]
    required: Vec<String>,
    #[serde(default)]
    price_typed_columns: Vec<String>,
    #[serde(default)]
    eod_style: bool,
    columns: Vec<ColumnDef>,
}

#[derive(Debug, Deserialize)]
struct ColumnDef {
    name: String,
    field: String,
    r#type: String,
    /// For `price_type` fields: which price column to extract the type from.
    #[serde(default)]
    price_source: Option<String>,
}

fn generate_tick_types() -> Result<(), Box<dyn std::error::Error>> {
    let schema_path = "endpoint_schema.toml";
    let schema_str = std::fs::read_to_string(schema_path)?;
    let schema: Schema = toml::from_str(&schema_str)?;

    let out_dir = std::env::var("OUT_DIR")?;

    // Collect type names and sort for deterministic output.
    let mut type_names: Vec<&String> = schema.types.keys().collect();
    type_names.sort();

    // ── Generate tick_generated.rs (struct definitions) ──────────────────────
    let mut structs = String::new();
    structs.push_str(
        "// Auto-generated by build.rs from endpoint_schema.toml \u{2014} do not edit manually.\n\n",
    );
    structs.push_str("use crate::types::price::Price;\n\n");

    for type_name in &type_names {
        let def = &schema.types[*type_name];
        generate_struct(&mut structs, type_name, def);
    }

    let structs_dest = Path::new(&out_dir).join("tick_generated.rs");
    std::fs::write(&structs_dest, &structs)?;

    // ── Generate decode_generated.rs (parser functions) ──────────────────────
    let mut parsers = String::new();
    parsers.push_str(
        "// Auto-generated by build.rs from endpoint_schema.toml \u{2014} do not edit manually.\n\n",
    );

    for type_name in &type_names {
        let def = &schema.types[*type_name];
        generate_parser(&mut parsers, type_name, def);
    }

    let parsers_dest = Path::new(&out_dir).join("decode_generated.rs");
    std::fs::write(&parsers_dest, &parsers)?;

    // Rerun if schema changes
    println!("cargo:rerun-if-changed={}", schema_path);

    Ok(())
}

/// Generate a single tick struct definition.
fn generate_struct(out: &mut String, type_name: &str, def: &TickTypeDef) {
    // Doc comment — handle embedded \n for multi-line docs.
    for line in def.doc.lines() {
        out.push_str(&format!("/// {}\n", line));
    }

    // Derive
    if def.copy {
        out.push_str("#[derive(Debug, Clone, Copy)]\n");
    } else {
        out.push_str("#[derive(Debug, Clone)]\n");
    }

    // Repr
    if let Some(align) = def.align {
        out.push_str(&format!("#[repr(C, align({align}))]\n"));
    }

    out.push_str(&format!("pub struct {type_name} {{\n"));

    for col in &def.columns {
        let rust_type = match col.r#type.as_str() {
            "i32" => "i32",
            "i64" => "i64",
            "f64" => "f64",
            "String" => "String",
            "price" => "i32",
            "price_value" => "i32",
            "eod_num" => "i32",
            "bool" => "i32",
            other => panic!("unknown column type '{other}' in schema for {type_name}"),
        };
        out.push_str(&format!("    pub {}: {},\n", col.field, rust_type));
    }

    out.push_str("}\n\n");
}

/// Generate a single parser function.
fn generate_parser(out: &mut String, type_name: &str, def: &TickTypeDef) {
    let fn_name = &def.parser;

    out.push_str(&format!(
        "pub fn {fn_name}(table: &crate::proto::DataTable) -> Vec<{type_name}> {{\n"
    ));

    // If eod_style, emit the local eod_num helper inline.
    if def.eod_style {
        out.push_str("    // EOD rows may have Price-typed cells or plain Number cells.\n");
        out.push_str("    fn eod_num(row: &crate::proto::DataValueList, idx: usize) -> i32 {\n");
        out.push_str("        row.values\n");
        out.push_str("            .get(idx)\n");
        out.push_str("            .and_then(|dv| dv.data_type.as_ref())\n");
        out.push_str("            .and_then(|dt| match dt {\n");
        out.push_str(
            "                crate::proto::data_value::DataType::Number(n) => Some(*n as i32),\n",
        );
        out.push_str(
            "                crate::proto::data_value::DataType::Price(p) => Some(p.value),\n",
        );
        out.push_str("                _ => None,\n");
        out.push_str("            })\n");
        out.push_str("            .unwrap_or(0)\n");
        out.push_str("    }\n\n");
    }

    out.push_str("    let h: Vec<&str> = table.headers.iter().map(|s| s.as_str()).collect();\n");

    // For eod_style, use a simple closure. For others, use find_header.
    if def.eod_style {
        out.push_str("    let find = |name: &str| h.iter().position(|&s| s == name);\n\n");
    }

    // Determine which columns need the opt_number helper
    let needs_opt_number = def
        .columns
        .iter()
        .any(|c| c.r#type == "i32" && !def.required.contains(&c.name) && c.price_source.is_none());
    let needs_opt_float = def
        .columns
        .iter()
        .any(|c| c.r#type == "f64" && !def.required.contains(&c.name));
    let needs_opt_i64 = def
        .columns
        .iter()
        .any(|c| c.r#type == "i64" && !def.required.contains(&c.name));

    // Emit opt_number / opt_float / opt_i64 helpers (non-eod only)
    if !def.eod_style {
        if needs_opt_number {
            out.push_str("    fn opt_number(row: &crate::proto::DataValueList, idx: Option<usize>) -> i32 {\n");
            out.push_str("        match idx {\n");
            out.push_str("            Some(i) => row_number(row, i),\n");
            out.push_str("            None => 0,\n");
            out.push_str("        }\n");
            out.push_str("    }\n\n");
        }
        if needs_opt_float {
            out.push_str("    fn opt_float(row: &crate::proto::DataValueList, idx: Option<usize>) -> f64 {\n");
            out.push_str("        match idx {\n");
            out.push_str("            Some(i) => row_float(row, i),\n");
            out.push_str("            None => 0.0,\n");
            out.push_str("        }\n");
            out.push_str("    }\n\n");
        }
        if needs_opt_i64 {
            out.push_str(
                "    fn opt_i64(row: &crate::proto::DataValueList, idx: Option<usize>) -> i64 {\n",
            );
            out.push_str("        match idx {\n");
            out.push_str("            Some(i) => row_number_i64(row, i),\n");
            out.push_str("            None => 0,\n");
            out.push_str("        }\n");
            out.push_str("    }\n\n");
        }
    }

    // Required header guards (non-eod only)
    if !def.eod_style {
        for req in &def.required {
            let var = idx_var_name(req);
            out.push_str(&format!(
                "    let Some({var}) = find_header(&h, \"{req}\") else {{\n        return vec![];\n    }};\n"
            ));
        }
        if !def.required.is_empty() {
            out.push('\n');
        }
    }

    // Declare index variables for all columns.
    for col in &def.columns {
        // Skip declaring index for columns that have price_source in eod_style,
        // since they use the source column's index instead.
        if def.eod_style && col.price_source.is_some() {
            continue;
        }
        let var = idx_var_name(&col.name);
        if def.eod_style {
            out.push_str(&format!("    let {var} = find(\"{}\");\n", col.name));
        } else if def.required.contains(&col.name) {
            // Already declared above as required.
        } else {
            out.push_str(&format!(
                "    let {var} = find_header(&h, \"{}\");\n",
                col.name
            ));
        }
    }

    // Precompute price_is_typed flags for price_typed_columns.
    if !def.price_typed_columns.is_empty() && !def.eod_style {
        out.push('\n');
        for ptc in &def.price_typed_columns {
            let var = idx_var_name(ptc);
            let typed_var = format!("{ptc}_is_typed");
            // For required columns, the idx is usize directly. For optional, it's Option<usize>.
            if def.required.contains(ptc) {
                out.push_str(&format!("    let {typed_var} = h.contains(&\"{ptc}\");\n"));
            } else {
                out.push_str(&format!(
                    "    let {typed_var} = {var}.is_some() && h.contains(&\"{ptc}\");\n"
                ));
            }
        }
    }

    out.push('\n');

    // Emit the row mapping.
    out.push_str("    table\n");
    out.push_str("        .data_table\n");
    out.push_str("        .iter()\n");
    out.push_str("        .map(|row| {\n");

    // For price-typed parsers, compute the price_type first (before the struct literal)
    // if any column has a price_source.
    let price_source_cols: Vec<&ColumnDef> = def
        .columns
        .iter()
        .filter(|c| c.price_source.is_some())
        .collect();
    for psc in &price_source_cols {
        let source = psc.price_source.as_ref().unwrap();
        let source_var = idx_var_name(source);
        let pt_var = format!("_pt_{}", psc.field);

        if def.eod_style {
            out.push_str(&format!(
                "            let {pt_var} = {source_var}.map(|i| row_price_type(row, i)).unwrap_or(0);\n"
            ));
        } else if def.price_typed_columns.contains(source) {
            let typed_var = format!("{source}_is_typed");
            if def.required.contains(source) {
                // source_var is usize
                let pt_field_var = idx_var_name(&psc.name);
                out.push_str(&format!("            let {pt_var} = if {typed_var} {{\n"));
                out.push_str(&format!(
                    "                row_price_type(row, {source_var})\n"
                ));
                out.push_str("            } else {\n");
                out.push_str(&format!(
                    "                opt_number(row, {pt_field_var})\n"
                ));
                out.push_str("            };\n");
            } else {
                // source_var is Option<usize>
                let pt_field_var = idx_var_name(&psc.name);
                out.push_str(&format!("            let {pt_var} = if {typed_var} {{\n"));
                out.push_str(&format!(
                    "                {source_var}.map(|i| row_price_type(row, i)).unwrap_or(0)\n"
                ));
                out.push_str("            } else {\n");
                out.push_str(&format!(
                    "                opt_number(row, {pt_field_var})\n"
                ));
                out.push_str("            };\n");
            }
        } else {
            // Not price-typed: just use opt_number
            let pt_field_var = idx_var_name(&psc.name);
            out.push_str(&format!(
                "            let {pt_var} = opt_number(row, {pt_field_var});\n"
            ));
        }
    }

    // Struct literal
    out.push_str(&format!("            {type_name} {{\n"));

    for col in &def.columns {
        let var = idx_var_name(&col.name);
        let is_required = def.required.contains(&col.name);

        // If this column has a price_source, use the precomputed _pt_ variable.
        if col.price_source.is_some() {
            let pt_var = format!("_pt_{}", col.field);
            out.push_str(&format!("                {}: {pt_var},\n", col.field));
            continue;
        }

        match col.r#type.as_str() {
            "i32" => {
                if def.eod_style {
                    // eod_style: all are Option<usize>, no required
                    out.push_str(&format!(
                        "                {}: {var}.map(|i| eod_num(row, i)).unwrap_or(0),\n",
                        col.field
                    ));
                } else if is_required {
                    out.push_str(&format!(
                        "                {}: row_number(row, {var}),\n",
                        col.field
                    ));
                } else {
                    out.push_str(&format!(
                        "                {}: opt_number(row, {var}),\n",
                        col.field
                    ));
                }
            }
            "i64" => {
                if is_required {
                    out.push_str(&format!(
                        "                {}: row_number_i64(row, {var}),\n",
                        col.field
                    ));
                } else {
                    out.push_str(&format!(
                        "                {}: opt_i64(row, {var}),\n",
                        col.field
                    ));
                }
            }
            "f64" => {
                if is_required {
                    out.push_str(&format!(
                        "                {}: row_float(row, {var}),\n",
                        col.field
                    ));
                } else {
                    out.push_str(&format!(
                        "                {}: opt_float(row, {var}),\n",
                        col.field
                    ));
                }
            }
            "String" => {
                if is_required {
                    out.push_str(&format!(
                        "                {}: row_text(row, {var}),\n",
                        col.field
                    ));
                } else {
                    out.push_str(&format!(
                        "                {}: {var}.map(|i| row_text(row, i)).unwrap_or_default(),\n",
                        col.field
                    ));
                }
            }
            "price" => {
                // Price column: uses row_price_value if price-typed, otherwise row_number.
                let typed_var = format!("{}_is_typed", col.name);
                let field = &col.field;
                if is_required {
                    out.push_str(&format!("                {field}: if {typed_var} {{\n"));
                    out.push_str(&format!(
                        "                    row_price_value(row, {var})\n"
                    ));
                    out.push_str("                } else {\n");
                    out.push_str(&format!("                    row_number(row, {var})\n"));
                    out.push_str("                },\n");
                } else {
                    out.push_str(&format!("                {field}: match {var} {{\n"));
                    out.push_str(&format!(
                        "                    Some(i) if {typed_var} => row_price_value(row, i),\n"
                    ));
                    out.push_str("                    Some(i) => row_number(row, i),\n");
                    out.push_str("                    None => 0,\n");
                    out.push_str("                },\n");
                }
            }
            "price_value" => {
                // Always row_price_value, regardless of typed check (used for bid/ask in TradeQuoteTick).
                let field = &col.field;
                if is_required {
                    out.push_str(&format!(
                        "                {field}: row_price_value(row, {var}),\n"
                    ));
                } else {
                    out.push_str(&format!("                {field}: match {var} {{\n"));
                    out.push_str("                    Some(i) => row_price_value(row, i),\n");
                    out.push_str("                    None => 0,\n");
                    out.push_str("                },\n");
                }
            }
            "eod_num" => {
                out.push_str(&format!(
                    "                {}: {var}.map(|i| eod_num(row, i)).unwrap_or(0),\n",
                    col.field
                ));
            }
            "bool" => {
                if is_required {
                    out.push_str(&format!(
                        "                {}: row_number(row, {var}),\n",
                        col.field
                    ));
                } else {
                    out.push_str(&format!(
                        "                {}: opt_number(row, {var}),\n",
                        col.field
                    ));
                }
            }
            other => panic!("unknown column type '{other}' in parser for {type_name}"),
        }
    }

    out.push_str("            }\n");
    out.push_str("        })\n");
    out.push_str("        .collect()\n");
    out.push_str("}\n\n");
}

/// Convert a column name to a consistent index variable name.
fn idx_var_name(name: &str) -> String {
    format!("{name}_idx")
}

// ─────────────────────────────────────────────────────────────────────────────
//  Code-generate the ENDPOINTS static array from v3_endpoints.proto
// ─────────────────────────────────────────────────────────────────────────────

/// A parsed proto field.
#[derive(Debug, Clone)]
struct ProtoField {
    name: String,
    proto_type: String, // "string", "int32", "double", "bool", or "endpoints.ContractSpec"
    is_optional: bool,
    is_repeated: bool,
}

/// A parsed RPC entry.
#[derive(Debug)]
struct Rpc {
    rpc_name: String,     // e.g. "GetStockHistoryEod"
    request_type: String, // e.g. "StockHistoryEodRequest"
}

fn generate_endpoint_registry() -> Result<(), Box<dyn std::error::Error>> {
    let proto = std::fs::read_to_string("proto/v3_endpoints.proto")?;

    // ── Parse RPCs ──────────────────────────────────────────────────────────
    let rpc_re = regex::Regex::new(r"rpc\s+(Get\w+)\s*\((\w+)\)\s*returns")?;
    let rpcs: Vec<Rpc> = rpc_re
        .captures_iter(&proto)
        .map(|c| Rpc {
            rpc_name: c[1].to_string(),
            request_type: c[2].to_string(),
        })
        .collect();

    // ── Parse query messages ────────────────────────────────────────────────
    let msg_re = regex::Regex::new(r"message\s+(\w+RequestQuery)\s*\{([^}]*)}")?;
    let field_re = regex::Regex::new(
        r"(optional\s+|repeated\s+)?(string|int32|double|bool|endpoints\.ContractSpec)\s+(\w+)\s*=\s*\d+",
    )?;

    let mut query_messages: HashMap<String, Vec<ProtoField>> = HashMap::new();
    for cap in msg_re.captures_iter(&proto) {
        let msg_name = cap[1].to_string();
        let body = &cap[2];
        let fields: Vec<ProtoField> = field_re
            .captures_iter(body)
            .map(|f| ProtoField {
                name: f[3].to_string(),
                proto_type: f[2].to_string(),
                is_optional: f.get(1).is_some_and(|m| m.as_str().starts_with("optional")),
                is_repeated: f.get(1).is_some_and(|m| m.as_str().starts_with("repeated")),
            })
            .collect();
        query_messages.insert(msg_name, fields);
    }

    // ── Generate Rust code ──────────────────────────────────────────────────
    let mut code = String::new();
    code.push_str(
        "// Auto-generated by build.rs from v3_endpoints.proto — do not edit manually.\n\n",
    );
    code.push_str("pub static ENDPOINTS: &[EndpointMeta] = &[\n");

    for rpc in &rpcs {
        // Derive snake_case method name: GetStockHistoryEod → stock_history_eod
        let method = rpc_to_method(&rpc.rpc_name);

        // Find the query message: StockHistoryEodRequest → StockHistoryEodRequestQuery
        let query_msg_name = format!("{}Query", rpc.request_type);
        let fields = match query_messages.get(&query_msg_name) {
            Some(f) => f.clone(),
            None => {
                eprintln!(
                    "warning: no query message '{}' found, skipping {}",
                    query_msg_name, rpc.rpc_name
                );
                continue;
            }
        };

        // Expand fields (contract_spec → symbol, expiration, strike, right)
        let params = expand_fields(&fields);

        let category = derive_category(&method);
        let subcategory = derive_subcategory(&method, &category);
        let return_type = derive_return_type(&method);
        let description = derive_description(&method);

        code.push_str("    EndpointMeta {\n");
        code.push_str(&format!("        name: \"{}\",\n", method));
        code.push_str(&format!("        description: \"{}\",\n", description));
        code.push_str(&format!("        category: \"{}\",\n", category));
        code.push_str(&format!("        subcategory: \"{}\",\n", subcategory));

        if params.is_empty() {
            code.push_str("        params: &[],\n");
        } else {
            code.push_str("        params: &[\n");
            for p in &params {
                code.push_str("            ParamMeta {\n");
                code.push_str(&format!("                name: \"{}\",\n", p.0));
                code.push_str(&format!("                description: \"{}\",\n", p.1));
                code.push_str(&format!(
                    "                param_type: ParamType::{},\n",
                    p.2
                ));
                code.push_str(&format!("                required: {},\n", p.3));
                code.push_str("            },\n");
            }
            code.push_str("        ],\n");
        }

        code.push_str(&format!("        returns: ReturnType::{},\n", return_type));
        code.push_str("    },\n");
    }

    // ── Manual extra: stock_history_ohlc_range (not in proto) ───────────────
    // stock_history_ohlc_range is a convenience wrapper over GetStockHistoryOhlc
    // with start_date/end_date instead of single date. Not in the proto but
    // useful for users. Manually appended to the auto-generated list.
    code.push_str("    // Manual entry — convenience method not in proto\n");
    code.push_str("    EndpointMeta {\n");
    code.push_str("        name: \"stock_history_ohlc_range\",\n");
    code.push_str("        description: \"Fetch intraday OHLC bars across a date range.\",\n");
    code.push_str("        category: \"stock\",\n");
    code.push_str("        subcategory: \"history\",\n");
    code.push_str("        params: &[\n");
    code.push_str("            ParamMeta {\n");
    code.push_str("                name: \"symbol\",\n");
    code.push_str("                description: \"Ticker symbol (e.g. AAPL)\",\n");
    code.push_str("                param_type: ParamType::Symbol,\n");
    code.push_str("                required: true,\n");
    code.push_str("            },\n");
    code.push_str("            ParamMeta {\n");
    code.push_str("                name: \"start_date\",\n");
    code.push_str("                description: \"Start date YYYYMMDD\",\n");
    code.push_str("                param_type: ParamType::Date,\n");
    code.push_str("                required: true,\n");
    code.push_str("            },\n");
    code.push_str("            ParamMeta {\n");
    code.push_str("                name: \"end_date\",\n");
    code.push_str("                description: \"End date YYYYMMDD\",\n");
    code.push_str("                param_type: ParamType::Date,\n");
    code.push_str("                required: true,\n");
    code.push_str("            },\n");
    code.push_str("            ParamMeta {\n");
    code.push_str("                name: \"interval\",\n");
    code.push_str(
        "                description: \"Interval in milliseconds (e.g. 60000 for 1-min bars)\",\n",
    );
    code.push_str("                param_type: ParamType::Interval,\n");
    code.push_str("                required: true,\n");
    code.push_str("            },\n");
    code.push_str("        ],\n");
    code.push_str("        returns: ReturnType::OhlcTicks,\n");
    code.push_str("    },\n");

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

    let out_dir = std::env::var("OUT_DIR")?;
    let dest = Path::new(&out_dir).join("registry_generated.rs");
    std::fs::write(&dest, &code)?;

    // Rerun if proto changes
    println!("cargo:rerun-if-changed=proto/v3_endpoints.proto");

    Ok(())
}

/// Convert `GetStockHistoryEod` → `stock_history_eod`.
fn rpc_to_method(rpc_name: &str) -> String {
    // Strip leading "Get"
    let name = rpc_name.strip_prefix("Get").unwrap_or(rpc_name); // build script: panic is intentional
                                                                 // PascalCase → snake_case
    let mut result = String::new();
    for (i, ch) in name.chars().enumerate() {
        if ch.is_uppercase() {
            if i > 0 {
                result.push('_');
            }
            result.push(ch.to_lowercase().next().unwrap()); // build script: panic is intentional
        } else {
            result.push(ch);
        }
    }
    result
}

/// Expand proto fields, replacing `contract_spec` with (symbol, expiration, strike, right).
fn expand_fields(fields: &[ProtoField]) -> Vec<(String, String, String, bool)> {
    let mut params = Vec::new();
    for f in fields {
        if f.proto_type == "endpoints.ContractSpec" {
            // Expand to the 4 contract spec fields
            params.push((
                "symbol".into(),
                "Underlying symbol (e.g. AAPL)".into(),
                "Symbol".into(),
                true,
            ));
            params.push((
                "expiration".into(),
                "Expiration date YYYYMMDD".into(),
                "Expiration".into(),
                true,
            ));
            params.push((
                "strike".into(),
                "Strike price (raw integer)".into(),
                "Strike".into(),
                true,
            ));
            params.push((
                "right".into(),
                "C for call, P for put".into(),
                "Right".into(),
                true,
            ));
        } else {
            let (param_type, desc) = map_field(&f.name, &f.proto_type, f.is_repeated);
            let required = !f.is_optional;
            params.push((f.name.clone(), desc, param_type, required));
        }
    }
    params
}

/// Map a proto field (name + type + repeated) to (ParamType variant name, description).
fn map_field(name: &str, proto_type: &str, is_repeated: bool) -> (String, String) {
    // Repeated string symbol → Symbols
    if is_repeated && name == "symbol" {
        return (
            "Symbols".into(),
            "Comma-separated ticker symbols (e.g. AAPL,MSFT)".into(),
        );
    }

    match (proto_type, name) {
        ("string", "symbol") => ("Symbol".into(), "Ticker symbol (e.g. AAPL)".into()),
        ("string", "start_date") => ("Date".into(), "Start date YYYYMMDD".into()),
        ("string", "end_date") => ("Date".into(), "End date YYYYMMDD".into()),
        ("string", "date") => ("Date".into(), "Date YYYYMMDD".into()),
        ("string", "interval") => (
            "Interval".into(),
            "Interval in milliseconds (e.g. 60000 for 1-min bars)".into(),
        ),
        ("string", "right") => ("Right".into(), "C for call, P for put".into()),
        ("string", "strike") => ("Strike".into(), "Strike price (raw integer)".into()),
        ("string", "expiration") => ("Expiration".into(), "Expiration date YYYYMMDD".into()),
        ("string", "request_type") => (
            "RequestType".into(),
            "Request type: EOD, TRADE, QUOTE, OHLC, etc.".into(),
        ),
        ("string", "year") => ("Year".into(), "4-digit year (e.g. 2024)".into()),
        ("string", "time_of_day") => (
            "Str".into(),
            "Milliseconds from midnight ET (e.g. 34200000 = 9:30 AM)".into(),
        ),
        ("string", "venue") => ("Str".into(), "Venue/exchange filter".into()),
        ("string", "min_time") => ("Str".into(), "Minimum time filter".into()),
        ("string", "start_time") => ("Str".into(), "Start time filter".into()),
        ("string", "end_time") => ("Str".into(), "End time filter".into()),
        ("string", "rate_type") => ("Str".into(), "Rate type".into()),
        ("string", "version") => ("Str".into(), "Greeks model version".into()),
        ("double", _) => ("Float".into(), humanize_name(name).to_string()),
        ("int32", "max_dte") => ("Int".into(), "Maximum days to expiration".into()),
        ("int32", "strike_range") => ("Int".into(), "Strike range filter".into()),
        ("int32", _) => ("Int".into(), humanize_name(name).to_string()),
        ("bool", "exclusive") => ("Bool".into(), "Exclusive time boundary".into()),
        ("bool", "use_market_value") => ("Bool".into(), "Use market value for Greeks".into()),
        ("bool", "underlyer_use_nbbo") => ("Bool".into(), "Use NBBO for underlyer price".into()),
        ("bool", _) => ("Bool".into(), humanize_name(name).to_string()),
        _ => ("Str".into(), humanize_name(name).to_string()),
    }
}

fn humanize_name(name: &str) -> String {
    name.replace('_', " ")
        .split_whitespace()
        .enumerate()
        .map(|(i, w)| {
            if i == 0 {
                let mut c = w.chars();
                match c.next() {
                    Some(first) => first.to_uppercase().to_string() + c.as_str(),
                    None => String::new(),
                }
            } else {
                w.to_string()
            }
        })
        .collect::<Vec<_>>()
        .join(" ")
}

fn derive_category(method: &str) -> String {
    if method.starts_with("stock_") {
        "stock".into()
    } else if method.starts_with("option_") {
        "option".into()
    } else if method.starts_with("index_") {
        "index".into()
    } else if method.starts_with("calendar_") {
        "calendar".into()
    } else if method.starts_with("interest_rate_") {
        "rate".into()
    } else {
        "other".into()
    }
}

fn derive_subcategory(method: &str, category: &str) -> String {
    // Strip the category prefix to get the rest
    let rest = match category {
        // build script: panic is intentional (unwrap_or provides safe fallback)
        "stock" => method.strip_prefix("stock_").unwrap_or(method),
        "option" => method.strip_prefix("option_").unwrap_or(method),
        "index" => method.strip_prefix("index_").unwrap_or(method),
        "calendar" => method.strip_prefix("calendar_").unwrap_or(method),
        "rate" => method.strip_prefix("interest_rate_").unwrap_or(method),
        _ => method,
    };

    if rest.starts_with("list_") {
        "list".into()
    } else if rest.starts_with("snapshot_greeks_") || rest.starts_with("snapshot_greeks") {
        "snapshot_greeks".into()
    } else if rest.starts_with("snapshot_") {
        "snapshot".into()
    } else if rest.starts_with("history_trade_greeks_") {
        "history_trade_greeks".into()
    } else if rest.starts_with("history_greeks_") {
        "history_greeks".into()
    } else if rest.starts_with("history_") {
        "history".into()
    } else if rest.starts_with("at_time_") {
        "at_time".into()
    } else if rest == "open_today" {
        "status".into()
    } else if rest == "on_date" || rest == "year" {
        "query".into()
    } else {
        "other".into()
    }
}

fn derive_return_type(method: &str) -> String {
    // List endpoints → StringList
    if method.contains("_list_symbols")
        || method.contains("_list_expirations")
        || method.contains("_list_strikes")
        || method.contains("_list_dates")
    {
        return "StringList".into();
    }

    // Trade-quote is a combined type → DataTable (before trade/quote checks)
    if method.contains("_trade_quote") {
        return "DataTable".into();
    }

    // Trade Greeks endpoints → DataTable (before trade check)
    if method.contains("_trade_greeks_") {
        return "DataTable".into();
    }

    // History EOD → EodTicks (but not greeks_eod)
    if method.ends_with("_history_eod") {
        return "EodTicks".into();
    }

    // Greeks EOD → DataTable
    if method.contains("_greeks_eod") {
        return "DataTable".into();
    }

    // OHLC → OhlcTicks
    if method.contains("_ohlc") {
        return "OhlcTicks".into();
    }

    // Trade → TradeTicks
    if method.contains("_trade") || method.ends_with("_trade") {
        return "TradeTicks".into();
    }

    // Quote → QuoteTicks
    if method.contains("_quote") || method.ends_with("_quote") {
        return "QuoteTicks".into();
    }

    // Everything else: greeks, open_interest, market_value, calendar, rate, price, list_contracts
    "DataTable".into()
}

fn derive_description(method: &str) -> String {
    // Hand-crafted descriptions for known patterns
    match method {
        // Stock List
        "stock_list_symbols" => "List all available stock ticker symbols.".into(),
        "stock_list_dates" => {
            "List available dates for a stock by request type (EOD, TRADE, QUOTE, etc.).".into()
        }
        // Stock Snapshot
        "stock_snapshot_ohlc" => "Get the latest OHLC snapshot for one or more stocks.".into(),
        "stock_snapshot_trade" => "Get the latest trade snapshot for one or more stocks.".into(),
        "stock_snapshot_quote" => {
            "Get the latest NBBO quote snapshot for one or more stocks.".into()
        }
        "stock_snapshot_market_value" => {
            "Get the latest market value snapshot for one or more stocks.".into()
        }
        // Stock History
        "stock_history_eod" => {
            "Fetch end-of-day stock data for a date range. Returns OHLCV + bid/ask per trading day."
                .into()
        }
        "stock_history_ohlc" => "Fetch intraday OHLC bars for a stock on a single date.".into(),
        "stock_history_trade" => "Fetch all trades for a stock on a given date.".into(),
        "stock_history_quote" => {
            "Fetch NBBO quotes for a stock on a given date at a given interval.".into()
        }
        "stock_history_trade_quote" => {
            "Fetch combined trade + quote ticks for a stock on a given date. Returns raw DataTable."
                .into()
        }
        // Stock At-Time
        "stock_at_time_trade" => {
            "Fetch the trade at a specific time of day across a date range.".into()
        }
        "stock_at_time_quote" => {
            "Fetch the quote at a specific time of day across a date range.".into()
        }
        // Option List
        "option_list_symbols" => "List all available option underlying symbols.".into(),
        "option_list_dates" => {
            "List available dates for an option contract by request type.".into()
        }
        "option_list_expirations" => {
            "List available expiration dates for an option underlying.".into()
        }
        "option_list_strikes" => {
            "List available strike prices for an option at a given expiration.".into()
        }
        "option_list_contracts" => "List all option contracts for a symbol on a given date.".into(),
        // Option Snapshot
        "option_snapshot_ohlc" => "Get the latest OHLC snapshot for an option contract.".into(),
        "option_snapshot_trade" => "Get the latest trade snapshot for an option contract.".into(),
        "option_snapshot_quote" => {
            "Get the latest NBBO quote snapshot for an option contract.".into()
        }
        "option_snapshot_open_interest" => {
            "Get the latest open interest snapshot for an option contract.".into()
        }
        "option_snapshot_market_value" => {
            "Get the latest market value snapshot for an option contract.".into()
        }
        // Option Snapshot Greeks
        "option_snapshot_greeks_implied_volatility" => {
            "Get implied volatility snapshot for an option contract (from ThetaData server).".into()
        }
        "option_snapshot_greeks_all" => {
            "Get all Greeks snapshot for an option contract (from ThetaData server).".into()
        }
        "option_snapshot_greeks_first_order" => {
            "Get first-order Greeks snapshot (delta, theta, rho) for an option contract.".into()
        }
        "option_snapshot_greeks_second_order" => {
            "Get second-order Greeks snapshot (gamma, vanna, charm) for an option contract.".into()
        }
        "option_snapshot_greeks_third_order" => {
            "Get third-order Greeks snapshot (speed, color, ultima) for an option contract.".into()
        }
        // Option History
        "option_history_eod" => {
            "Fetch end-of-day option data for a contract over a date range.".into()
        }
        "option_history_ohlc" => "Fetch intraday OHLC bars for an option contract.".into(),
        "option_history_trade" => "Fetch all trades for an option contract on a given date.".into(),
        "option_history_quote" => {
            "Fetch NBBO quotes for an option contract on a given date.".into()
        }
        "option_history_trade_quote" => {
            "Fetch combined trade + quote ticks for an option contract.".into()
        }
        "option_history_open_interest" => {
            "Fetch open interest history for an option contract.".into()
        }
        // Option History Greeks
        "option_history_greeks_eod" => {
            "Fetch end-of-day Greeks history for an option contract.".into()
        }
        "option_history_greeks_all" => {
            "Fetch all Greeks history for an option contract (intraday, sampled by interval)."
                .into()
        }
        "option_history_trade_greeks_all" => {
            "Fetch all Greeks on each trade for an option contract.".into()
        }
        "option_history_greeks_first_order" => {
            "Fetch first-order Greeks history (intraday, sampled by interval).".into()
        }
        "option_history_trade_greeks_first_order" => {
            "Fetch first-order Greeks on each trade for an option contract.".into()
        }
        "option_history_greeks_second_order" => {
            "Fetch second-order Greeks history (intraday, sampled by interval).".into()
        }
        "option_history_trade_greeks_second_order" => {
            "Fetch second-order Greeks on each trade for an option contract.".into()
        }
        "option_history_greeks_third_order" => {
            "Fetch third-order Greeks history (intraday, sampled by interval).".into()
        }
        "option_history_trade_greeks_third_order" => {
            "Fetch third-order Greeks on each trade for an option contract.".into()
        }
        "option_history_greeks_implied_volatility" => {
            "Fetch implied volatility history (intraday, sampled by interval).".into()
        }
        "option_history_trade_greeks_implied_volatility" => {
            "Fetch implied volatility on each trade for an option contract.".into()
        }
        // Option At-Time
        "option_at_time_trade" => {
            "Fetch the trade at a specific time of day across a date range for an option.".into()
        }
        "option_at_time_quote" => {
            "Fetch the quote at a specific time of day across a date range for an option.".into()
        }
        // Index
        "index_list_symbols" => "List all available index symbols.".into(),
        "index_list_dates" => "List available dates for an index symbol.".into(),
        "index_snapshot_ohlc" => "Get the latest OHLC snapshot for one or more indices.".into(),
        "index_snapshot_price" => "Get the latest price snapshot for one or more indices.".into(),
        "index_snapshot_market_value" => {
            "Get the latest market value snapshot for one or more indices.".into()
        }
        "index_history_eod" => "Fetch end-of-day index data for a date range.".into(),
        "index_history_ohlc" => "Fetch intraday OHLC bars for an index.".into(),
        "index_history_price" => "Fetch intraday price history for an index.".into(),
        "index_at_time_price" => {
            "Fetch the index price at a specific time of day across a date range.".into()
        }
        // Calendar
        "calendar_open_today" => "Check whether the market is open today.".into(),
        "calendar_on_date" => "Get calendar information for a specific date.".into(),
        "calendar_year" => "Get calendar information for an entire year.".into(),
        // Rate
        "interest_rate_history_eod" => "Fetch end-of-day interest rate history.".into(),
        // Fallback: auto-generate from method name
        _ => {
            let words = method.replace('_', " ");
            let mut chars = words.chars();
            match chars.next() {
                Some(c) => {
                    format!("{}{}.", c.to_uppercase(), chars.as_str())
                }
                None => method.to_string(),
            }
        }
    }
}
