// 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)
//
// `xml` — convert between XML strings and JSON. Operations:
//   parse  — XML string → JSON object via quick-xml
//   build  — JSON object → XML string

use async_trait::async_trait;
use crate::core::error::NodeError;
use crate::core::types::*;
use crate::engine::{ExecuteCtx, Node, NodeFactory};
use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
use quick_xml::Reader;
use quick_xml::Writer;
use serde_json::{json, Map, Value};
use std::sync::Arc;

pub struct XmlNode;

static META: NodeMetadata = NodeMetadata {
    name: "xml",
    display_name: "XML",
    version: &[1, 2],
    group: &["transform"],
    description: "Parse XML to JSON or build XML from JSON",
    inputs: 1,
    outputs: 1,
    properties: &[],
    credentials: &[],
};

#[async_trait]
impl Node for XmlNode {
    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("parse").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() {
                "parse" => {
                    let xml = ctx.param_str("xml").unwrap_or_default();
                    parse_xml(&xml).map_err(|e| NodeError::Other(format!("xml parse: {}", e)))?
                }
                "build" => {
                    let v = ctx.param_value("data").unwrap_or(Value::Null);
                    Value::String(build_xml(&v))
                }
                other => return Err(NodeError::Other(format!("xml: 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("result".into(), result);
            out.push(NodeItem { json: new_json, ..item });
        }
        Ok(vec![out])
    }
}

fn parse_xml(s: &str) -> Result<Value, String> {
    let mut reader = Reader::from_str(s);
    reader.config_mut().trim_text(true);
    let mut stack: Vec<(String, Map<String, Value>)> = vec![(String::new(), Map::new())];
    let mut buf = Vec::new();
    loop {
        match reader.read_event_into(&mut buf) {
            Ok(Event::Start(e)) => {
                let name = String::from_utf8_lossy(e.name().as_ref()).into_owned();
                stack.push((name, Map::new()));
            }
            Ok(Event::End(_)) => {
                let (name, obj) = stack.pop().ok_or("unbalanced XML")?;
                let value = if obj.is_empty() { Value::Null } else { Value::Object(obj) };
                let parent = &mut stack.last_mut().ok_or("xml underflow")?.1;
                merge_into(parent, &name, value);
            }
            Ok(Event::Text(t)) => {
                let txt = t.unescape().map_err(|e| e.to_string())?.into_owned();
                if !txt.is_empty() {
                    let parent = &mut stack.last_mut().ok_or("xml underflow")?.1;
                    parent.insert("$text".into(), Value::String(txt));
                }
            }
            Ok(Event::Empty(e)) => {
                let name = String::from_utf8_lossy(e.name().as_ref()).into_owned();
                let parent = &mut stack.last_mut().ok_or("xml underflow")?.1;
                merge_into(parent, &name, Value::Null);
            }
            Ok(Event::Eof) => break,
            Ok(_) => {}
            Err(e) => return Err(format!("xml: {}", e)),
        }
        buf.clear();
    }
    let (_, root) = stack.pop().ok_or("xml: no root")?;
    Ok(Value::Object(root))
}

fn merge_into(parent: &mut Map<String, Value>, key: &str, value: Value) {
    match parent.remove(key) {
        Some(Value::Array(mut arr)) => { arr.push(value); parent.insert(key.into(), Value::Array(arr)); }
        Some(existing) => { parent.insert(key.into(), Value::Array(vec![existing, value])); }
        None => { parent.insert(key.into(), value); }
    }
}

fn build_xml(v: &Value) -> String {
    let mut writer = Writer::new(Vec::new());
    write_value(&mut writer, "root", v);
    String::from_utf8_lossy(&writer.into_inner()).into_owned()
}

fn write_value<W: std::io::Write>(w: &mut Writer<W>, tag: &str, v: &Value) {
    match v {
        Value::Object(obj) => {
            let _ = w.write_event(Event::Start(BytesStart::new(tag)));
            for (k, vv) in obj { write_value(w, k, vv); }
            let _ = w.write_event(Event::End(BytesEnd::new(tag.to_string())));
        }
        Value::Array(arr) => {
            for vv in arr { write_value(w, tag, vv); }
        }
        Value::Null => {
            let _ = w.write_event(Event::Empty(BytesStart::new(tag)));
        }
        other => {
            let _ = w.write_event(Event::Start(BytesStart::new(tag)));
            let s = match other { Value::String(s) => s.clone(), _ => other.to_string() };
            let _ = w.write_event(Event::Text(BytesText::new(&s)));
            let _ = w.write_event(Event::End(BytesEnd::new(tag.to_string())));
        }
    }
    let _ = json!(null); // keep json! import live
}

inventory::submit! { NodeFactory::new("xml", || Arc::new(XmlNode)) }
