// 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 serde::{Deserialize, Serialize};
use serde_json::Value;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NodeSpec {
    pub name: String,
    pub display_name: String,
    pub description: String,
    pub group: Vec<String>,
    pub versions: Vec<f64>,
    pub inputs: u8,
    pub outputs: u8,
    pub credentials: Vec<CredSpec>,
    pub properties: Value,
    pub source_path: String,
    /// Declarative routing data, when present. Codegen emits a working
    /// routing-runtime impl for these instead of NotYetPorted.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub routing: Option<RoutingSpec>,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct RoutingSpec {
    /// `requestDefaults.baseURL` — the host every operation hits.
    pub base_url: String,
    /// Header injection from the credential. Pulled from
    /// `authenticate.properties.headers` of the credential file. Each
    /// entry is (header_name, value_template) where the template can
    /// reference credential fields with `{{<field>}}`.
    pub auth_headers: Vec<(String, String)>,
    /// Query-string injection from the credential.
    pub auth_query: Vec<(String, String)>,
    /// The credential's machine name (e.g. "perplexityApi") that the
    /// node references via `credentials = "..."`.
    pub credential_name: Option<String>,
    /// Per-operation routing pulled from each `value: '...'` option
    /// whose enclosing object contains `routing.request.{method, url}`.
    pub operations: Vec<OperationSpec>,
    /// Per-property `routing.send` blocks — sibling input fields whose
    /// values get injected into the request body/query/header at execute
    /// time. Filtered by displayOptions per-operation.
    #[serde(default)]
    pub sends: Vec<SendSpec>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OperationSpec {
    pub value: String,
    pub method: String,
    pub url: String,
    /// Optional JSON body template extracted from `routing.request.body`.
    /// Empty string ⇒ no body.
    #[serde(default)]
    pub body: String,
}

/// A `routing.send` block on a non-dispatch property — n8n's mechanism
/// for "inject this property's value into the body/query/header at
/// request time, but only for the operations in displayOptions.show".
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SendSpec {
    /// Parameter name in workflow.toml — what the user types under
    /// `[nodes.parameters]`.
    pub param_name: String,
    /// "body" | "query" | "header"
    pub send_type: String,
    /// Destination key in the body / query / header map.
    pub dest_key: String,
    /// Operation values from displayOptions.show.operation that this
    /// send applies to. Empty ⇒ applies to all operations.
    #[serde(default)]
    pub applies_to_operations: Vec<String>,
    /// Resource values from displayOptions.show.resource. Empty ⇒ all.
    #[serde(default)]
    pub applies_to_resources: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CredSpec {
    pub name: String,
    pub required: bool,
}

const RUST_KEYWORDS: &[&str] = &[
    "as", "break", "const", "continue", "crate", "else", "enum", "extern", "false", "fn",
    "for", "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref",
    "return", "self", "static", "struct", "super", "trait", "true", "type", "unsafe", "use",
    "where", "while", "async", "await", "dyn", "abstract", "become", "box", "do", "final",
    "macro", "override", "priv", "typeof", "unsized", "virtual", "yield", "try",
];

impl NodeSpec {
    pub fn slug(&self) -> String {
        let raw = self.name.replace('-', "_").to_lowercase();
        if RUST_KEYWORDS.contains(&raw.as_str()) {
            format!("{}_node", raw)
        } else {
            raw
        }
    }

    /// The machine name used by workflow.toml `type =` and inventory registration.
    /// Keep this stable even if the module slug needs an `_node` suffix.
    pub fn registration_name(&self) -> String {
        self.name.replace('-', "_").to_lowercase()
    }

    pub fn rust_struct_name(&self) -> String {
        let mut out = String::new();
        let mut upper = true;
        for c in self.name.chars() {
            if c == '_' || c == '-' { upper = true; continue; }
            if upper { out.extend(c.to_uppercase()); upper = false; }
            else { out.push(c); }
        }
        format!("{}Node", out)
    }
}
