// 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)
//
// `datetime` — format/parse timestamps and arithmetic on dates.
// Operations: now, format, parse, add, subtract.

use async_trait::async_trait;
use chrono::{DateTime, Duration, NaiveDateTime, TimeZone, Utc};
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 DateTimeNode;

static META: NodeMetadata = NodeMetadata {
    name: "datetime",
    display_name: "Date & Time",
    version: &[1, 2],
    group: &["transform"],
    description: "Format, parse, and do arithmetic on date/time values",
    inputs: 1,
    outputs: 1,
    properties: &[],
    credentials: &[],
};

#[async_trait]
impl Node for DateTimeNode {
    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("now")
            .to_string();
        let format = ctx.node_def.parameters.get("format")
            .and_then(|v| v.as_str())
            .unwrap_or("%Y-%m-%dT%H:%M:%S%.3fZ")
            .to_string();
        let field = ctx.node_def.parameters.get("output_field")
            .and_then(|v| v.as_str())
            .unwrap_or("datetime")
            .to_string();

        let items_in = if ctx.input.is_empty() { vec![NodeItem::default()] } else { ctx.input.clone() };
        let mut out = Vec::with_capacity(items_in.len());
        for item in items_in {
            let result = match op.as_str() {
                "now" => Value::String(Utc::now().format(&format).to_string()),
                "format" => {
                    let value = ctx.node_def.parameters.get("value")
                        .and_then(|v| v.as_str())
                        .ok_or_else(|| NodeError::MissingParameter("value".into()))?;
                    let dt = parse_dt(value)?;
                    Value::String(dt.format(&format).to_string())
                }
                "parse" => {
                    let value = ctx.node_def.parameters.get("value")
                        .and_then(|v| v.as_str())
                        .ok_or_else(|| NodeError::MissingParameter("value".into()))?;
                    let dt = parse_dt(value)?;
                    json!({
                        "rfc3339": dt.to_rfc3339(),
                        "year": dt.format("%Y").to_string().parse::<i32>().unwrap_or(0),
                        "month": dt.format("%m").to_string().parse::<u32>().unwrap_or(0),
                        "day": dt.format("%d").to_string().parse::<u32>().unwrap_or(0),
                        "epoch_ms": dt.timestamp_millis(),
                    })
                }
                "add" | "subtract" => {
                    let value = ctx.node_def.parameters.get("value")
                        .and_then(|v| v.as_str())
                        .ok_or_else(|| NodeError::MissingParameter("value".into()))?;
                    let amount = ctx.node_def.parameters.get("amount")
                        .and_then(|v| v.as_i64())
                        .ok_or_else(|| NodeError::MissingParameter("amount".into()))?;
                    let unit = ctx.node_def.parameters.get("unit")
                        .and_then(|v| v.as_str())
                        .unwrap_or("seconds");
                    let signed = if op == "subtract" { -amount } else { amount };
                    let dur = duration_for(unit, signed)?;
                    let dt = parse_dt(value)? + dur;
                    Value::String(dt.format(&format).to_string())
                }
                other => return Err(NodeError::Other(format!("datetime: unknown operation `{}`", other))),
            };
            let mut new_json = if item.json.is_object() {
                item.json.clone()
            } else {
                Value::Object(Default::default())
            };
            new_json.as_object_mut().unwrap().insert(field.clone(), result);
            out.push(NodeItem { json: new_json, ..item });
        }
        Ok(vec![out])
    }
}

fn parse_dt(s: &str) -> Result<DateTime<Utc>, NodeError> {
    if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
        return Ok(dt.with_timezone(&Utc));
    }
    if let Ok(dt) = DateTime::parse_from_rfc2822(s) {
        return Ok(dt.with_timezone(&Utc));
    }
    if let Ok(n) = s.parse::<i64>() {
        // epoch seconds (or ms if huge)
        let dt = if n > 10_000_000_000 {
            Utc.timestamp_millis_opt(n).single()
        } else {
            Utc.timestamp_opt(n, 0).single()
        };
        if let Some(dt) = dt { return Ok(dt); }
    }
    if let Ok(naive) = NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S") {
        return Ok(Utc.from_utc_datetime(&naive));
    }
    Err(NodeError::Other(format!("datetime: cannot parse `{}`", s)))
}

fn duration_for(unit: &str, n: i64) -> Result<Duration, NodeError> {
    Ok(match unit {
        "ms" | "milliseconds" => Duration::milliseconds(n),
        "s" | "seconds" => Duration::seconds(n),
        "m" | "minutes" => Duration::minutes(n),
        "h" | "hours" => Duration::hours(n),
        "d" | "days" => Duration::days(n),
        "w" | "weeks" => Duration::weeks(n),
        other => return Err(NodeError::Other(format!("datetime: unknown unit `{}`", other))),
    })
}

inventory::submit! { NodeFactory::new("datetime", || Arc::new(DateTimeNode)) }
