// 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)
//
// Pure-Rust expression evaluator. No JS sandbox.
// Handles n8n syntax: {{ $json.foo.bar }}, {{ $node.X.json.y }}, {{ $now }}, etc.
// v0.1: literal substitution + path lookup. v0.2 (optional feature): boa_engine for full JS.

use super::error::NodeError;
use super::types::NodeItem;
use serde_json::Value;
use std::collections::HashMap;

pub struct ExprCtx<'a> {
    pub current_item: &'a NodeItem,
    pub item_index: u32,
    pub node_data: &'a HashMap<String, Vec<NodeItem>>,
    pub workflow_name: &'a str,
    pub execution_id: &'a str,
    pub parameters: &'a HashMap<String, Value>,
    pub now: chrono::DateTime<chrono::Utc>,
}

pub fn eval_str(template: &str, ctx: &ExprCtx) -> Result<String, NodeError> {
    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;
        let end = rest[body_start..].find("}}")
            .ok_or_else(|| NodeError::Expression("unmatched {{ in template".into()))?;
        let expr = rest[body_start..body_start + end].trim();
        let value = eval_expr(expr, ctx)?;
        out.push_str(&value_to_str(&value));
        rest = &rest[body_start + end + 2..];
    }
    out.push_str(rest);
    Ok(out)
}

pub fn eval_value(value: &Value, ctx: &ExprCtx) -> Result<Value, NodeError> {
    match value {
        Value::String(s) if s.contains("{{") => {
            // If the entire value is one expression, preserve native type
            let trimmed = s.trim();
            if trimmed.starts_with("{{") && trimmed.ends_with("}}") {
                let inner = &trimmed[2..trimmed.len() - 2].trim();
                eval_expr(inner, ctx)
            } else {
                Ok(Value::String(eval_str(s, ctx)?))
            }
        }
        Value::Array(arr) => {
            let mapped: Result<Vec<_>, _> = arr.iter().map(|v| eval_value(v, ctx)).collect();
            Ok(Value::Array(mapped?))
        }
        Value::Object(obj) => {
            let mut mapped = serde_json::Map::with_capacity(obj.len());
            for (k, v) in obj {
                mapped.insert(k.clone(), eval_value(v, ctx)?);
            }
            Ok(Value::Object(mapped))
        }
        _ => Ok(value.clone()),
    }
}

fn eval_expr(expr: &str, ctx: &ExprCtx) -> Result<Value, NodeError> {
    let expr = expr.trim();
    if let Some(rest) = expr.strip_prefix("$json") {
        return Ok(walk_path(&ctx.current_item.json, rest));
    }
    if let Some(rest) = expr.strip_prefix("$parameter") {
        let path = rest.trim();
        if path.is_empty() {
            return Ok(Value::Object(serde_json::Map::from_iter(
                ctx.parameters.iter().map(|(k, v)| (k.clone(), v.clone())),
            )));
        }
        let (key, sub) = split_first_segment(path);
        let v = ctx.parameters.get(&key).cloned().unwrap_or(Value::Null);
        return Ok(walk_path(&v, &sub));
    }
    if let Some(rest) = expr.strip_prefix("$node.") {
        let (node_name, sub) = split_first_segment(rest);
        let items = ctx.node_data.get(&node_name);
        let first = items.and_then(|v| v.first());
        return Ok(match first {
            Some(item) => {
                let s = sub.trim_start_matches('.');
                if let Some(after) = s.strip_prefix("json") {
                    walk_path(&item.json, after)
                } else if s.is_empty() {
                    serde_json::to_value(item).unwrap_or(Value::Null)
                } else {
                    Value::Null
                }
            }
            None => Value::Null,
        });
    }
    if expr == "$now" {
        return Ok(Value::String(ctx.now.to_rfc3339()));
    }
    if expr == "$today" {
        return Ok(Value::String(ctx.now.date_naive().to_string()));
    }
    if expr == "$workflow.name" {
        return Ok(Value::String(ctx.workflow_name.to_string()));
    }
    if expr == "$execution.id" {
        return Ok(Value::String(ctx.execution_id.to_string()));
    }
    if expr == "$position" || expr == "$itemIndex" {
        return Ok(Value::Number(ctx.item_index.into()));
    }
    if let Some(rest) = expr.strip_prefix("$env.") {
        let val = std::env::var(rest.trim()).unwrap_or_default();
        return Ok(Value::String(val));
    }
    Err(NodeError::Expression(format!("unsupported expression: `{}`", expr)))
}

fn walk_path(root: &Value, path: &str) -> Value {
    let mut cur = root;
    for seg in path.split(['.', '[', ']']).filter(|s| !s.is_empty()) {
        cur = match cur {
            Value::Object(o) => o.get(seg).unwrap_or(&Value::Null),
            Value::Array(a) => seg.parse::<usize>().ok().and_then(|i| a.get(i)).unwrap_or(&Value::Null),
            _ => &Value::Null,
        };
    }
    cur.clone()
}

fn split_first_segment(s: &str) -> (String, String) {
    let s = s.trim_start_matches('.');
    if let Some(idx) = s.find('.') {
        (s[..idx].to_string(), s[idx..].to_string())
    } else {
        (s.to_string(), String::new())
    }
}

fn value_to_str(v: &Value) -> String {
    match v {
        Value::String(s) => s.clone(),
        Value::Null => String::new(),
        other => other.to_string(),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    fn ctx_with_item(item: NodeItem) -> (NodeItem, HashMap<String, Vec<NodeItem>>, HashMap<String, Value>) {
        (item, HashMap::new(), HashMap::new())
    }

    #[test]
    fn json_path_basic() {
        let item = NodeItem { json: json!({"foo": {"bar": 42}}), ..Default::default() };
        let (item, nodes, params) = ctx_with_item(item);
        let ctx = ExprCtx {
            current_item: &item,
            item_index: 0,
            node_data: &nodes,
            workflow_name: "t",
            execution_id: "e1",
            parameters: &params,
            now: chrono::Utc::now(),
        };
        assert_eq!(eval_str("hello {{ $json.foo.bar }}", &ctx).unwrap(), "hello 42");
    }

    #[test]
    fn cross_node_lookup() {
        let item = NodeItem::default();
        let mut nodes = HashMap::new();
        nodes.insert("Webhook".to_string(), vec![NodeItem {
            json: json!({"id": "abc"}),
            ..Default::default()
        }]);
        let params = HashMap::new();
        let ctx = ExprCtx {
            current_item: &item, item_index: 0, node_data: &nodes,
            workflow_name: "t", execution_id: "e1", parameters: &params,
            now: chrono::Utc::now(),
        };
        assert_eq!(eval_str("{{ $node.Webhook.json.id }}", &ctx).unwrap(), "abc");
    }
}
