// 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)
//
// `rss_feed_read` — fetch + parse an RSS or Atom feed, emit each item.
// Param names match n8n: url, ignoreSSL.

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

pub struct RssFeedReadNode;

static META: NodeMetadata = NodeMetadata {
    name: "rss_feed_read",
    display_name: "RSS Feed Read",
    version: &[1],
    group: &["transform"],
    description: "Fetch and parse an RSS or Atom feed",
    inputs: 1,
    outputs: 1,
    properties: &[
        PropertyDef { name: "url", display_name: "URL", kind: PropertyKind::String, required: true, default: None },
        PropertyDef { name: "ignoreSSL", display_name: "Ignore SSL Issues", kind: PropertyKind::Boolean, required: false, default: Some("false") },
    ],
    credentials: &[],
};

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

    async fn execute(&self, ctx: &mut ExecuteCtx<'_>) -> Result<NodeOutput, NodeError> {
        let url = ctx.param_str("url")?;
        let ignore_ssl = ctx.node_def.parameters.get("ignoreSSL")
            .and_then(|v| v.as_bool()).unwrap_or(false);

        let client = reqwest::Client::builder()
            .danger_accept_invalid_certs(ignore_ssl)
            .user_agent(concat!("r8r/", env!("CARGO_PKG_VERSION")))
            .build()
            .map_err(|e| NodeError::Http(format!("rss: client: {}", e)))?;
        let resp = client.get(&url).send().await
            .map_err(|e| NodeError::Http(format!("rss: {}: {}", url, e)))?;
        let status = resp.status();
        let body = resp.text().await
            .map_err(|e| NodeError::Http(format!("rss: read: {}", e)))?;
        if !status.is_success() {
            return Err(NodeError::Http(format!("rss {}: {}", status, body)));
        }

        let items = parse_feed(&body)?;
        Ok(vec![items.into_iter().map(|j| NodeItem { json: j, ..Default::default() }).collect()])
    }
}

fn parse_feed(xml: &str) -> Result<Vec<Value>, NodeError> {
    let mut reader = Reader::from_str(xml);
    reader.config_mut().trim_text(true);
    let mut buf = Vec::new();
    let mut items: Vec<Value> = Vec::new();
    let mut current: Option<serde_json::Map<String, Value>> = None;
    let mut current_key: Option<String> = None;
    let mut current_text = String::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();
                if name == "item" || name == "entry" {
                    current = Some(serde_json::Map::new());
                } else if current.is_some() {
                    current_key = Some(name.clone());
                    current_text.clear();
                    // Capture link href for Atom
                    if name == "link" {
                        for a in e.attributes().flatten() {
                            if a.key.as_ref() == b"href" {
                                if let Some(map) = current.as_mut() {
                                    let v = String::from_utf8_lossy(&a.value).into_owned();
                                    map.insert("link".into(), Value::String(v));
                                }
                            }
                        }
                    }
                }
            }
            Ok(Event::Text(t)) => {
                if current_key.is_some() {
                    current_text.push_str(&t.unescape().map_err(|e| NodeError::Other(e.to_string()))?);
                }
            }
            Ok(Event::CData(t)) => {
                if current_key.is_some() {
                    current_text.push_str(&String::from_utf8_lossy(t.as_ref()));
                }
            }
            Ok(Event::End(e)) => {
                let name = String::from_utf8_lossy(e.name().as_ref()).into_owned();
                if name == "item" || name == "entry" {
                    if let Some(map) = current.take() {
                        items.push(Value::Object(map));
                    }
                } else if let Some(key) = current_key.take() {
                    if let Some(map) = current.as_mut() {
                        if !current_text.is_empty() {
                            map.insert(key, Value::String(current_text.trim().to_string()));
                        }
                        current_text.clear();
                    }
                }
            }
            Ok(Event::Eof) => break,
            Err(e) => return Err(NodeError::Other(format!("rss xml: {}", e))),
            _ => {}
        }
        buf.clear();
    }
    if items.is_empty() {
        return Ok(vec![json!({"feed_body": xml.to_string(), "warning": "no <item> or <entry> elements found"})]);
    }
    Ok(items)
}

inventory::submit! { NodeFactory::new("rss_feed_read", || Arc::new(RssFeedReadNode)) }
