// 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)
//
// `http_request` — generic HTTP client. n8n's HttpRequest node, minus the bloat.

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 HttpRequestNode;

static META: NodeMetadata = NodeMetadata {
    name: "http_request",
    display_name: "HTTP Request",
    version: &[1, 2, 3, 4],
    group: &["transform"],
    description: "Make an HTTP request and receive the response",
    inputs: 1,
    outputs: 1,
    properties: &[],
    credentials: &[],
};

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

    async fn execute(&self, ctx: &mut ExecuteCtx<'_>) -> Result<NodeOutput, NodeError> {
        let method = ctx.param_opt_str("method").unwrap_or_else(|| "GET".into()).to_uppercase();
        let url = ctx.param_str("url")?;
        let body = ctx.node_def.parameters.get("body").cloned();
        let headers = ctx.node_def.parameters.get("headers").cloned()
            .unwrap_or(Value::Object(Default::default()));

        let client = reqwest::Client::builder()
            .user_agent(concat!("r8r/", env!("CARGO_PKG_VERSION")))
            .build()
            .map_err(|e| NodeError::Http(e.to_string()))?;

        let m: reqwest::Method = method.parse()
            .map_err(|e| NodeError::Http(format!("invalid http method: {e}")))?;
        let mut req = client.request(m, &url);

        if let Value::Object(h) = headers {
            for (k, v) in h {
                if let Some(s) = v.as_str() {
                    req = req.header(k, s);
                }
            }
        }

        if let Some(b) = body {
            req = match b {
                Value::String(s) => req.body(s),
                other => req.json(&other),
            };
        }

        let resp = req.send().await.map_err(|e| NodeError::Http(e.to_string()))?;
        let status = resp.status().as_u16();
        let resp_headers: serde_json::Map<String, Value> = resp.headers().iter()
            .map(|(k, v)| (k.to_string(), Value::String(v.to_str().unwrap_or("").to_string())))
            .collect();
        let bytes = resp.bytes().await.map_err(|e| NodeError::Http(e.to_string()))?;
        let body_value: Value = serde_json::from_slice(&bytes)
            .unwrap_or_else(|_| Value::String(String::from_utf8_lossy(&bytes).into_owned()));

        let item = NodeItem {
            json: json!({
                "status": status,
                "headers": Value::Object(resp_headers),
                "body": body_value,
            }),
            ..Default::default()
        };
        Ok(vec![vec![item]])
    }
}

inventory::submit! { NodeFactory::new("http_request", || Arc::new(HttpRequestNode)) }
