// 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)
//
// Declarative routing runtime. Mirrors n8n's `requestDefaults` +
// per-property `routing.request` pattern, but in static Rust data.
// Codegen emits one `RoutingDescriptor` per node, the node's
// `execute()` delegates to `execute_routing(&DESC, ctx)`.
//
// What the runtime handles:
//   * base URL substitution (per-node)
//   * operation dispatch on a configurable parameter
//     (default: "operation", n8n nodes often use "resource"+"operation")
//   * URL path interpolation: "/users/{{id}}" → "/users/123"
//   * Body template interpolation: same syntax
//   * Credential injection via auth_headers / auth_query
//   * JSON response parsing
//
// What it deliberately doesn't handle (yet):
//   * pagination (returnAll loops)
//   * preSend / postReceive transforms
//   * resourceLocator / resourceMapper resolution
//   * binary upload/download
//   * OAuth2 refresh (assume creds are fresh)

use crate::core::credentials::CredentialStore;
use crate::core::error::NodeError;
use crate::core::types::*;
use crate::engine::node::ExecuteCtx;
use serde_json::Value;
use std::collections::HashMap;

#[derive(Debug)]
pub struct RoutingDescriptor {
    pub base_url: &'static str,
    pub credential: Option<&'static str>,
    /// Headers to inject from the credential. Each tuple is
    /// (header_name, value_template). Templates can reference
    /// credential fields with `{{<field>}}`.
    pub auth_headers: &'static [(&'static str, &'static str)],
    /// Query string params injected from the credential.
    pub auth_query: &'static [(&'static str, &'static str)],
    /// The node parameter name used to dispatch on. Most n8n nodes use
    /// "operation"; some use "resource" + "operation" jointly. For the
    /// joint case, set this to "resource" and have OperationDescriptor's
    /// `value` be "<resource>.<operation>".
    pub dispatch_param: &'static str,
    pub operations: &'static [OperationDescriptor],
    /// Per-property send descriptors. For each operation, the runtime
    /// walks this list and injects matching params into body/query/header.
    pub sends: &'static [SendDescriptor],
}

#[derive(Debug)]
pub struct SendDescriptor {
    pub param_name: &'static str,
    /// "body" | "query" | "header"
    pub send_type: &'static str,
    pub dest_key: &'static str,
    /// Operations this send applies to. Empty slice ⇒ applies to all.
    pub operations: &'static [&'static str],
    pub resources: &'static [&'static str],
}

#[derive(Debug)]
pub struct OperationDescriptor {
    pub value: &'static str,
    pub method: &'static str,
    pub url_template: &'static str,
    /// JSON body template. Empty string ⇒ no body.
    /// Templates use `{{<param>}}` referencing node parameters.
    pub body_template: &'static str,
    /// Extra query params on top of auth_query.
    pub query: &'static [(&'static str, &'static str)],
}

pub async fn execute_routing(
    desc: &RoutingDescriptor,
    ctx: &mut ExecuteCtx<'_>,
) -> Result<NodeOutput, NodeError> {
    let dispatch_value = ctx.node_def.parameters.get(desc.dispatch_param)
        .and_then(|v| v.as_str())
        .or_else(|| desc.operations.first().map(|o| o.value))
        .ok_or_else(|| NodeError::MissingParameter(desc.dispatch_param.to_string()))?
        .to_string();

    let op = desc.operations.iter().find(|o| o.value == dispatch_value)
        .ok_or_else(|| NodeError::Other(format!(
            "routing: no operation matches `{} = {}`. Known: {}",
            desc.dispatch_param, dispatch_value,
            desc.operations.iter().map(|o| o.value).collect::<Vec<_>>().join(", ")
        )))?;

    let params = &ctx.node_def.parameters;
    let creds = collect_creds(desc.credential, ctx.credentials);

    // Strip n8n's expression-mode `=` prefix from base_url and url
    // before interpolation so `={{ $credentials.foo }}` becomes
    // `{{foo}}` after our normalize step.
    let base_clean = strip_expr_prefix(desc.base_url);
    let url_path = interpolate(&strip_expr_prefix(op.url_template), params, &creds);
    let mut full_url = if url_path.starts_with("http://") || url_path.starts_with("https://") {
        url_path
    } else {
        format!("{}{}", base_clean.trim_end_matches('/'), url_path)
    };

    // Walk per-property sends. Inject any param whose displayOptions
    // matches the current operation/resource.
    let resource_value = ctx.node_def.parameters.get("resource")
        .and_then(|v| v.as_str())
        .unwrap_or("");
    let mut body_obj = serde_json::Map::new();
    let mut header_extras: Vec<(String, String)> = Vec::new();
    let mut query_extras: Vec<(String, String)> = Vec::new();
    for s in desc.sends {
        if !s.operations.is_empty() && !s.operations.iter().any(|o| *o == dispatch_value.as_str()) {
            continue;
        }
        if !s.resources.is_empty() && !s.resources.iter().any(|r| *r == resource_value) {
            continue;
        }
        let raw_value = match params.get(s.param_name) {
            Some(v) => v.clone(),
            None => continue,
        };
        let dest = if s.dest_key.is_empty() { s.param_name } else { s.dest_key };
        match s.send_type {
            "body" => { body_obj.insert(dest.to_string(), raw_value); }
            "query" => {
                let stringy = match &raw_value {
                    Value::String(s) => s.clone(),
                    other => other.to_string(),
                };
                query_extras.push((dest.to_string(), stringy));
            }
            "header" => {
                let stringy = match &raw_value {
                    Value::String(s) => s.clone(),
                    other => other.to_string(),
                };
                header_extras.push((dest.to_string(), stringy));
            }
            _ => {}
        }
    }

    // Append auth + per-op + send-driven query params
    let mut query_pairs: Vec<(String, String)> = Vec::new();
    for (k, v) in desc.auth_query {
        query_pairs.push((k.to_string(), interpolate(v, params, &creds)));
    }
    for (k, v) in op.query {
        query_pairs.push((k.to_string(), interpolate(v, params, &creds)));
    }
    query_pairs.extend(query_extras);
    if !query_pairs.is_empty() {
        let sep = if full_url.contains('?') { '&' } else { '?' };
        full_url.push(sep);
        full_url.push_str(&query_pairs.iter()
            .map(|(k, v)| format!("{}={}", urlencode(k), urlencode(v)))
            .collect::<Vec<_>>().join("&"));
    }

    let method: reqwest::Method = op.method.parse()
        .map_err(|e| NodeError::Http(format!("routing: bad method `{}`: {}", op.method, e)))?;
    let mut req = reqwest::Client::new().request(method, &full_url);

    // Auth headers
    for (name, value_template) in desc.auth_headers {
        let v = interpolate(value_template, params, &creds);
        if !v.is_empty() {
            req = req.header(*name, v);
        }
    }
    // Send-driven extra headers
    for (k, v) in &header_extras {
        req = req.header(k.as_str(), v.as_str());
    }

    // Body
    if !op.body_template.is_empty() {
        let body_str = interpolate(&strip_expr_prefix(op.body_template), params, &creds);
        let parsed: Value = serde_json::from_str(&body_str)
            .unwrap_or(Value::String(body_str.clone()));
        req = req.json(&parsed);
    } else if !body_obj.is_empty() {
        req = req.json(&Value::Object(body_obj));
    }

    let resp = req.send().await
        .map_err(|e| NodeError::Http(format!("routing: {}", e)))?;
    let status = resp.status();
    let parsed: Value = resp.json().await.unwrap_or(Value::Null);
    if !status.is_success() {
        return Err(NodeError::Http(format!("routing {}: {}", status, parsed)));
    }
    Ok(vec![vec![NodeItem { json: parsed, ..Default::default() }]])
}

fn collect_creds(name: Option<&str>, store: &CredentialStore) -> HashMap<String, String> {
    let mut out = HashMap::new();
    if let Some(n) = name {
        if let Some(map) = store.get(n) {
            for (k, v) in map {
                let s = match v {
                    Value::String(s) => s.clone(),
                    other => other.to_string(),
                };
                out.insert(k.clone(), s);
            }
        }
    }
    out
}

/// Substitute `{{<name>}}` placeholders against node parameters first,
/// then credential fields. Unknown names leave the literal placeholder.
fn interpolate(template: &str, params: &HashMap<String, Value>, creds: &HashMap<String, String>) -> String {
    let mut out = String::with_capacity(template.len());
    let mut rest = template;
    while let Some(start) = rest.find("{{") {
        out.push_str(&rest[..start]);
        let body_start = start + 2;
        match rest[body_start..].find("}}") {
            Some(end) => {
                let key = rest[body_start..body_start + end].trim();
                let resolved = params.get(key)
                    .map(|v| match v {
                        Value::String(s) => s.clone(),
                        other => other.to_string(),
                    })
                    .or_else(|| creds.get(key).cloned())
                    .unwrap_or_else(|| format!("{{{{{}}}}}", key));
                out.push_str(&resolved);
                rest = &rest[body_start + end + 2..];
            }
            None => { out.push_str(&rest[start..]); break; }
        }
    }
    out.push_str(rest);
    out
}

/// Drop the leading `=` that n8n uses to mark "this string is an
/// expression". With that prefix gone, our `{{...}}` substitution
/// works on the rest.
fn strip_expr_prefix(s: &str) -> &str {
    s.strip_prefix('=').unwrap_or(s)
}

fn urlencode(s: &str) -> String {
    s.bytes().flat_map(|b| {
        if b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.' | b'~') { vec![b as char] }
        else { format!("%{:02X}", b).chars().collect() }
    }).collect()
}
