// 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)
//
// `item_lists` — split arrays into items, aggregate items into arrays,
// dedupe, sort, limit. n8n's ItemLists umbrella node, simplified.

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

pub struct ItemListsNode;

static META: NodeMetadata = NodeMetadata {
    name: "item_lists",
    display_name: "Item Lists",
    version: &[1, 2, 3],
    group: &["transform"],
    description: "Split, aggregate, sort, dedupe, or limit items",
    inputs: 1,
    outputs: 1,
    properties: &[],
    credentials: &[],
};

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

    async fn execute(&self, ctx: &mut ExecuteCtx<'_>) -> Result<NodeOutput, NodeError> {
        let op = ctx.node_def.parameters.get("operation")
            .and_then(|v| v.as_str())
            .unwrap_or("split")
            .to_string();
        match op.as_str() {
            "split" => split(ctx),
            "aggregate" => aggregate(ctx),
            "sort" => sort(ctx),
            "dedupe" => dedupe(ctx),
            "limit" => limit(ctx),
            other => Err(NodeError::Other(format!("item_lists: unknown operation `{}`", other))),
        }
    }
}

fn split(ctx: &mut ExecuteCtx<'_>) -> Result<NodeOutput, NodeError> {
    let field = ctx.node_def.parameters.get("field")
        .and_then(|v| v.as_str())
        .ok_or_else(|| NodeError::MissingParameter("field".into()))?
        .to_string();
    let mut out = Vec::new();
    for item in &ctx.input {
        if let Some(arr) = item.json.get(&field).and_then(|v| v.as_array()) {
            for entry in arr {
                let json = if entry.is_object() { entry.clone() } else { json!({ &field: entry }) };
                out.push(NodeItem { json, ..item.clone() });
            }
        }
    }
    Ok(vec![out])
}

fn aggregate(ctx: &mut ExecuteCtx<'_>) -> Result<NodeOutput, NodeError> {
    let field = ctx.node_def.parameters.get("field")
        .and_then(|v| v.as_str())
        .map(|s| s.to_string());
    let collected: Vec<Value> = ctx.input.iter().map(|i| {
        match &field {
            Some(f) => i.json.get(f).cloned().unwrap_or(Value::Null),
            None => i.json.clone(),
        }
    }).collect();
    let combined_field = ctx.node_def.parameters.get("output_field")
        .and_then(|v| v.as_str())
        .unwrap_or("data")
        .to_string();
    let item = NodeItem {
        json: json!({ combined_field: Value::Array(collected) }),
        ..Default::default()
    };
    Ok(vec![vec![item]])
}

fn sort(ctx: &mut ExecuteCtx<'_>) -> Result<NodeOutput, NodeError> {
    let field = ctx.node_def.parameters.get("field")
        .and_then(|v| v.as_str())
        .ok_or_else(|| NodeError::MissingParameter("field".into()))?
        .to_string();
    let descending = ctx.node_def.parameters.get("descending")
        .and_then(|v| v.as_bool()).unwrap_or(false);
    let mut items = ctx.input.clone();
    items.sort_by(|a, b| {
        let av = a.json.get(&field).cloned().unwrap_or(Value::Null);
        let bv = b.json.get(&field).cloned().unwrap_or(Value::Null);
        let ord = match (av.as_f64(), bv.as_f64()) {
            (Some(x), Some(y)) => x.partial_cmp(&y).unwrap_or(std::cmp::Ordering::Equal),
            _ => av.to_string().cmp(&bv.to_string()),
        };
        if descending { ord.reverse() } else { ord }
    });
    Ok(vec![items])
}

fn dedupe(ctx: &mut ExecuteCtx<'_>) -> Result<NodeOutput, NodeError> {
    let field = ctx.node_def.parameters.get("field")
        .and_then(|v| v.as_str())
        .map(|s| s.to_string());
    let mut seen = std::collections::HashSet::new();
    let mut out = Vec::new();
    for item in &ctx.input {
        let key = match &field {
            Some(f) => item.json.get(f).cloned().unwrap_or(Value::Null).to_string(),
            None => item.json.to_string(),
        };
        if seen.insert(key) {
            out.push(item.clone());
        }
    }
    Ok(vec![out])
}

fn limit(ctx: &mut ExecuteCtx<'_>) -> Result<NodeOutput, NodeError> {
    let n = ctx.node_def.parameters.get("n")
        .and_then(|v| v.as_u64())
        .ok_or_else(|| NodeError::MissingParameter("n".into()))? as usize;
    Ok(vec![ctx.input.iter().take(n).cloned().collect()])
}

inventory::submit! { NodeFactory::new("item_lists", || Arc::new(ItemListsNode)) }
