// r8r: rust port of n8n.io
// Initial author: Michael Cochran <mcochran@cochranblock.org>
// Built for: Tanner Woodrum, intended cofounder
// License: Unlicense (public domain dedication, see UNLICENSE)

use super::spec::NodeSpec;
use anyhow::Result;
use serde_json::Value;
use std::path::Path;

fn rs_str(s: &str) -> String {
    let mut out = String::with_capacity(s.len() + 2);
    out.push('"');
    for c in s.chars() {
        match c {
            '\\' => out.push_str("\\\\"),
            '"' => out.push_str("\\\""),
            '\n' => out.push_str("\\n"),
            '\r' => out.push_str("\\r"),
            '\t' => out.push_str("\\t"),
            c if (c as u32) < 0x20 => out.push_str(&format!("\\u{{{:x}}}", c as u32)),
            c => out.push(c),
        }
    }
    out.push('"');
    out
}

fn map_property_kind(n8n_type: &str) -> &'static str {
    match n8n_type {
        "string" | "hidden" | "credentialsSelect" | "resourceMapper" | "button" | "callout" | "filter" => "String",
        "number" => "Number",
        "boolean" => "Boolean",
        "json" => "Json",
        "dateTime" => "DateTime",
        "color" => "Color",
        "code" => "Code",
        "notice" => "Notice",
        "options" => "Options",
        "multiOptions" => "MultiOptions",
        "collection" => "Collection",
        "fixedCollection" => "FixedCollection",
        "resourceLocator" => "ResourceLocator",
        _ => "String",
    }
}

fn render_auth_pairs(pairs: &[(String, String)]) -> String {
    if pairs.is_empty() { return "&[]".to_string(); }
    let entries: Vec<String> = pairs.iter()
        .map(|(k, v)| format!("({}, {})", rs_str(k), rs_str(v)))
        .collect();
    format!("&[{}]", entries.join(", "))
}

fn render_sends(sends: &[super::spec::SendSpec]) -> String {
    if sends.is_empty() { return "&[]".to_string(); }
    let entries: Vec<String> = sends.iter().map(|s| {
        let ops: Vec<String> = s.applies_to_operations.iter().map(|s| rs_str(s)).collect();
        let ress: Vec<String> = s.applies_to_resources.iter().map(|s| rs_str(s)).collect();
        let dest = if s.dest_key.is_empty() { s.param_name.clone() } else { s.dest_key.clone() };
        format!(
            "                crate::engine::routing::SendDescriptor {{\n\
             \x20                   param_name: {p}, send_type: {t}, dest_key: {d},\n\
             \x20                   operations: &[{ops}], resources: &[{ress}],\n\
             \x20               }}",
            p = rs_str(&s.param_name),
            t = rs_str(&s.send_type),
            d = rs_str(&dest),
            ops = ops.join(", "),
            ress = ress.join(", "),
        )
    }).collect();
    format!("&[\n{}\n            ]", entries.join(",\n"))
}

fn render_operations(ops: &[super::spec::OperationSpec]) -> String {
    if ops.is_empty() { return "&[]".to_string(); }
    let entries: Vec<String> = ops.iter().map(|o| {
        // body_template is freeform JSON-ish from n8n; if it has expression
        // markers (={{$value}}) it's not safe to embed as JSON. For now
        // keep it as a literal string and let the runtime pass it through
        // — many nodes have empty bodies anyway and the request still
        // succeeds because params are sent as auth_query / send.type=body
        // which Phase 3 will handle.
        format!(
            "                crate::engine::routing::OperationDescriptor {{\n\
             \x20                   value: {v}, method: {m}, url_template: {u},\n\
             \x20                   body_template: {b}, query: &[],\n\
             \x20               }}",
            v = rs_str(&o.value),
            m = rs_str(&o.method),
            u = rs_str(&o.url),
            b = rs_str(&o.body),
        )
    }).collect();
    format!("&[\n{}\n            ]", entries.join(",\n"))
}

fn render_properties(props: &Value) -> String {
    let arr = match props.as_array() {
        Some(a) if !a.is_empty() => a,
        _ => return "&[]".to_string(),
    };
    let mut entries = Vec::with_capacity(arr.len());
    for p in arr {
        let name = p.get("name").and_then(|v| v.as_str()).unwrap_or("");
        if name.is_empty() { continue; }
        let display_name = p.get("display_name").and_then(|v| v.as_str()).unwrap_or(name);
        let kind = p.get("kind").and_then(|v| v.as_str()).unwrap_or("string");
        let required = p.get("required").and_then(|v| v.as_bool()).unwrap_or(false);
        let default = p.get("default").and_then(|v| v.as_str());
        let default_lit = match default {
            Some(s) => format!("Some({})", rs_str(s)),
            None => "None".to_string(),
        };
        entries.push(format!(
            "    PropertyDef {{ name: {n}, display_name: {dn}, kind: PropertyKind::{k}, required: {req}, default: {d} }}",
            n = rs_str(name),
            dn = rs_str(display_name),
            k = map_property_kind(kind),
            req = required,
            d = default_lit,
        ));
    }
    if entries.is_empty() {
        "&[]".to_string()
    } else {
        format!("&[\n{}\n]", entries.join(",\n"))
    }
}

pub fn emit_spec_json(spec: &NodeSpec, dir: &Path) -> Result<()> {
    std::fs::create_dir_all(dir)?;
    let path = dir.join(format!("{}.json", spec.slug()));
    std::fs::write(path, serde_json::to_string_pretty(spec)?)?;
    Ok(())
}

/// Slugs in `impl/` that should NOT have a generated stub (avoids collision).
const IMPL_OVERRIDES: &[&str] = &[
    "set", "if", "if_node", "http_request", "no_op",
    "wait", "code", "switch", "webhook", "cron",
    "datetime", "crypto", "rename_keys", "filter", "item_lists",
    "postgres", "slack", "github", "openai", "email_send",
    "merge", "interval", "split_in_batches", "stop_and_error", "execute_command",
    "sqlite", "anthropic", "discord", "notion",
    "xml", "markdown", "compression", "html",
    "mysql", "graphql", "schedule",
    "elasticsearch", "gmail", "jira", "stripe", "airtable",
    "trello", "sendgrid", "twilio", "hubspot", "mailchimp",
    "shopify", "linear", "asana", "clickup", "google_sheets",
    "read_binary_file", "write_binary_file", "respond_to_webhook", "convert_to_file", "execution_data",
    "anthropic_tmux_tui", "tmux_pane",
    "dropbox", "zoom", "zendesk", "intercom", "pipedrive",
    "gitlab", "bitbucket", "todoist", "uuid", "summarize",
    "ollama", "lm_studio", "salesforce", "monday", "typeform",
    "calendly", "clickhouse", "influxdb", "grafana", "n8n_api",
    "perplexity", "ssh",
    "telegram", "wordpress", "rss_feed_read", "pagerduty", "sentry_io",
    "whatsapp_business", "microsoft_teams", "microsoft_outlook", "freshdesk", "datadog",
    "splunk", "netlify", "cloudflare", "mattermost", "google_drive",
    "box", "microsoft_onedrive", "microsoft_excel", "gotify", "mailgun",
    "convertkit", "webflow", "mautic", "medium", "bitwarden",
    "strava", "strapi", "mailerlite", "highlevel", "bannerbear",
    "bubble", "cortex", "twist", "mailjet", "customer_io",
    "redis", "mqtt", "coda", "microsoft_dynamics", "coingecko",
    "mongodb", "kafka", "rabbitmq", "supabase", "awss3", "s3",
];

pub fn emit_rust_stub(spec: &NodeSpec, dir: &Path) -> Result<()> {
    std::fs::create_dir_all(dir)?;
    let slug = spec.slug();
    let registration = spec.registration_name();
    let path = dir.join(format!("{}.rs", slug));
    if IMPL_OVERRIDES.contains(&registration.as_str()) {
        if path.exists() {
            // Hand-built T1 lives in impl/; drop any stale stub from a prior codegen run.
            let _ = std::fs::remove_file(&path);
        }
        return Ok(());
    }
    let struct_name = spec.rust_struct_name();
    let upstream = format!(
        "https://github.com/n8n-io/n8n/blob/master/{}",
        spec.source_path
            .split("/n8n/")
            .nth(1)
            .unwrap_or(&spec.source_path),
    );

    let creds_static = if spec.credentials.is_empty() {
        "&[]".to_string()
    } else {
        let entries: Vec<String> = spec.credentials.iter().map(|c| {
            format!(
                "    CredentialRef {{ name: \"{}\", required: {} }}",
                c.name, c.required
            )
        }).collect();
        format!("&[\n{}\n]", entries.join(",\n"))
    };

    let props_static = render_properties(&spec.properties);

    // If we extracted a routing block, emit a routing-runtime impl
    // instead of NotYetPorted. Empty operations is fine — runtime errors
    // with a helpful "no operations declared" if the user sets one.
    let (execute_body, extra_use) = match &spec.routing {
        Some(r) if !r.base_url.is_empty() => {
            let auth_headers_lit = render_auth_pairs(&r.auth_headers);
            let auth_query_lit = render_auth_pairs(&r.auth_query);
            let cred_lit = match &r.credential_name {
                Some(n) => format!("Some({})", rs_str(n)),
                None => "None".to_string(),
            };
            let ops_lit = render_operations(&r.operations);
            let sends_lit = render_sends(&r.sends);
            let body = format!(
                "    static ROUTING: crate::engine::routing::RoutingDescriptor =\n\
                 \x20       crate::engine::routing::RoutingDescriptor {{\n\
                 \x20           base_url: {base},\n\
                 \x20           credential: {cred},\n\
                 \x20           auth_headers: {ahead},\n\
                 \x20           auth_query: {aquery},\n\
                 \x20           dispatch_param: \"operation\",\n\
                 \x20           operations: {ops},\n\
                 \x20           sends: {sends},\n\
                 \x20       }};\n\
                 \x20   crate::engine::routing::execute_routing(&ROUTING, _ctx).await\n",
                base = rs_str(&r.base_url),
                cred = cred_lit,
                ahead = auth_headers_lit,
                aquery = auth_query_lit,
                ops = ops_lit,
                sends = sends_lit,
            );
            (body, "")
        }
        _ => (
            format!(
                "    Err(NodeError::NotYetPorted {{ node: \"{}\", upstream: \"{}\" }})\n",
                registration, upstream
            ),
            "",
        ),
    };

    let body = format!(
r#"// AUTO-GENERATED — do not edit by hand
// Upstream: {upstream}
// To implement: copy to ../impl/{slug}.rs, fill in execute(), add to ../impl/mod.rs
// r8r: rust port of n8n.io
// License: Unlicense

use async_trait::async_trait;
use crate::core::error::NodeError;
use crate::core::types::*;
use crate::engine::{{ExecuteCtx, Node, NodeFactory}};{extra_use}
use std::sync::Arc;

pub struct {struct_name};

static META: NodeMetadata = NodeMetadata {{
    name: "{registration}",
    display_name: "{display_name}",
    version: &[1],
    group: &["transform"],
    description: "{description}",
    inputs: {inputs},
    outputs: {outputs},
    properties: {props_static},
    credentials: {creds_static},
}};

#[async_trait]
impl Node for {struct_name} {{
    fn metadata(&self) -> &'static NodeMetadata {{ &META }}

    async fn execute(&self, _ctx: &mut ExecuteCtx<'_>) -> Result<NodeOutput, NodeError> {{
{execute_body}    }}
}}

inventory::submit! {{ NodeFactory::new("{registration}", || Arc::new({struct_name})) }}
"#,
        execute_body = execute_body,
        extra_use = extra_use,
        upstream = upstream,
        slug = slug,
        registration = registration,
        struct_name = struct_name,
        display_name = spec.display_name.replace('"', "\\\""),
        description = spec.description.replace('"', "\\\""),
        inputs = spec.inputs,
        outputs = spec.outputs,
        creds_static = creds_static,
        props_static = props_static,
    );

    std::fs::write(&path, body)?;
    Ok(())
}

pub fn emit_mod_rs(specs: &[NodeSpec], dir: &Path) -> Result<()> {
    let mut body = String::from(
"// r8r: rust port of n8n.io\n\
// Initial author: Michael Cochran <mcochran@cochranblock.org>\n\
// Built for: Tanner Woodrum, intended cofounder\n\
// License: Unlicense (public domain dedication, see UNLICENSE)\n\
//\n\
// AUTO-GENERATED node skeletons. Re-run `cargo run -p r8r-codegen -- <n8n-root>` to refresh.\n\n",
    );
    let mut slugs: Vec<_> = specs.iter()
        .filter(|s| !IMPL_OVERRIDES.contains(&s.registration_name().as_str()))
        .map(|s| s.slug())
        .collect();
    slugs.sort();
    slugs.dedup();
    for slug in slugs {
        body.push_str(&format!("pub mod {};\n", slug));
    }
    std::fs::write(dir.join("mod.rs"), body)?;
    Ok(())
}
