// 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)
//
// `coingecko` — public CoinGecko v3 API. No auth required.
// Param names lifted verbatim from upstream CoinDescription.ts /
// EventDescription.ts: resource (coin/event), operation,
// searchBy, coinId, baseCurrency, baseCurrencies, platformId,
// contractAddress, contractAddresses, quoteCurrency, quoteCurrencies,
// days, date, returnAll, limit, options.

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

pub struct CoinGeckoNode;

static META: NodeMetadata = NodeMetadata {
    name: "coingecko",
    display_name: "CoinGecko",
    version: &[1],
    group: &["output"],
    description: "Consume CoinGecko API",
    inputs: 1,
    outputs: 1,
    properties: &[
        PropertyDef { name: "resource", display_name: "Resource", kind: PropertyKind::Options, required: true, default: Some("coin") },
        PropertyDef { name: "operation", display_name: "Operation", kind: PropertyKind::Options, required: true, default: Some("price") },
        PropertyDef { name: "searchBy", display_name: "Search By", kind: PropertyKind::Options, required: false, default: Some("coinId") },
        PropertyDef { name: "coinId", display_name: "Coin ID", kind: PropertyKind::String, required: false, default: None },
        PropertyDef { name: "baseCurrency", display_name: "Base Currency", kind: PropertyKind::String, required: false, default: None },
        PropertyDef { name: "baseCurrencies", display_name: "Base Currencies", kind: PropertyKind::String, required: false, default: None },
        PropertyDef { name: "quoteCurrency", display_name: "Quote Currency", kind: PropertyKind::String, required: false, default: None },
        PropertyDef { name: "quoteCurrencies", display_name: "Quote Currencies", kind: PropertyKind::String, required: false, default: None },
        PropertyDef { name: "platformId", display_name: "Platform ID", kind: PropertyKind::String, required: false, default: Some("ethereum") },
        PropertyDef { name: "contractAddress", display_name: "Contract Address", kind: PropertyKind::String, required: false, default: None },
        PropertyDef { name: "contractAddresses", display_name: "Contract Addresses", kind: PropertyKind::String, required: false, default: None },
        PropertyDef { name: "days", display_name: "Days", kind: PropertyKind::Options, required: false, default: Some("1") },
        PropertyDef { name: "date", display_name: "Date", kind: PropertyKind::String, required: false, default: None },
        PropertyDef { name: "returnAll", display_name: "Return All", kind: PropertyKind::Boolean, required: false, default: Some("false") },
        PropertyDef { name: "limit", display_name: "Limit", kind: PropertyKind::Number, required: false, default: Some("100") },
        PropertyDef { name: "options", display_name: "Options", kind: PropertyKind::Json, required: false, default: None },
    ],
    credentials: &[],
};

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

    async fn execute(&self, ctx: &mut ExecuteCtx<'_>) -> Result<NodeOutput, NodeError> {
        let base_url = ctx.credential_field("base_url").ok()
            .and_then(|v| v.as_str().map(|s| s.trim_end_matches('/').to_string()))
            .unwrap_or_else(|| "https://api.coingecko.com/api/v3".to_string());

        let resource = ctx.param_opt_str("resource").unwrap_or_else(|| "coin".into());
        let operation = ctx.param_opt_str("operation").unwrap_or_else(|| "price".into());
        let opts = ctx.node_def.parameters.get("options").cloned().unwrap_or(Value::Null);

        let path = match (resource.as_str(), operation.as_str()) {
            ("coin", "price") => {
                let ids = ctx.param_str("baseCurrencies")?;
                let vs = ctx.param_str("quoteCurrencies")?;
                format!("/simple/price?ids={}&vs_currencies={}", ids, vs)
            }
            ("coin", "get") => {
                let id = ctx.param_str("coinId")?;
                format!("/coins/{}", id)
            }
            ("coin", "getAll") => {
                let return_all = ctx.node_def.parameters.get("returnAll")
                    .and_then(|v| v.as_bool()).unwrap_or(false);
                let limit = ctx.node_def.parameters.get("limit")
                    .and_then(|v| v.as_u64()).unwrap_or(100);
                if return_all {
                    "/coins/list".to_string()
                } else {
                    format!("/coins/list?per_page={}", limit)
                }
            }
            ("coin", "history") => {
                let id = ctx.param_str("coinId")?;
                let date = ctx.param_str("date")?;
                format!("/coins/{}/history?date={}", id, date)
            }
            ("coin", "market") => {
                let vs = ctx.param_str("baseCurrency")?;
                let mut qs = vec![format!("vs_currency={}", vs)];
                if let Some(ids) = opts.get("ids").and_then(|v| v.as_str()) {
                    qs.push(format!("ids={}", ids));
                }
                if let Some(cat) = opts.get("category").and_then(|v| v.as_str()) {
                    qs.push(format!("category={}", cat));
                }
                if let Some(order) = opts.get("order").and_then(|v| v.as_str()) {
                    qs.push(format!("order={}", order));
                }
                format!("/coins/markets?{}", qs.join("&"))
            }
            ("coin", "marketChart") => {
                let id = ctx.param_str("coinId")?;
                let vs = ctx.param_str("baseCurrency")?;
                let days = ctx.param_opt_str("days").unwrap_or_else(|| "1".into());
                format!("/coins/{}/market_chart?vs_currency={}&days={}", id, vs, days)
            }
            ("coin", "candlestick") => {
                let id = ctx.param_str("coinId")?;
                let vs = ctx.param_str("baseCurrency")?;
                let days = ctx.param_opt_str("days").unwrap_or_else(|| "1".into());
                format!("/coins/{}/ohlc?vs_currency={}&days={}", id, vs, days)
            }
            ("coin", "ticker") => {
                let id = ctx.param_str("coinId")?;
                format!("/coins/{}/tickers", id)
            }
            ("event", "getAll") => {
                let mut qs = vec![];
                if let Some(c) = opts.get("country_code").and_then(|v| v.as_str()) {
                    qs.push(format!("country_code={}", c));
                }
                if let Some(d) = opts.get("from_date").and_then(|v| v.as_str()) {
                    qs.push(format!("from_date={}", d));
                }
                if let Some(d) = opts.get("to_date").and_then(|v| v.as_str()) {
                    qs.push(format!("to_date={}", d));
                }
                if let Some(t) = opts.get("type").and_then(|v| v.as_str()) {
                    qs.push(format!("type={}", t));
                }
                if let Some(u) = opts.get("upcoming_events_only").and_then(|v| v.as_bool()) {
                    qs.push(format!("upcoming_events_only={}", u));
                }
                let q = if qs.is_empty() { "".to_string() } else { format!("?{}", qs.join("&")) };
                format!("/events{}", q)
            }
            (r, o) => return Err(NodeError::Other(format!("coingecko: {}.{} not implemented", r, o))),
        };

        let url = format!("{}{}", base_url, path);
        let resp = reqwest::Client::new().get(&url)
            .send().await.map_err(|e| NodeError::Http(format!("coingecko: {}", e)))?;
        let status = resp.status();
        let parsed: Value = resp.json().await.unwrap_or(Value::Null);
        if !status.is_success() {
            return Err(NodeError::Http(format!("coingecko {}: {}", status, parsed)));
        }
        Ok(vec![vec![NodeItem { json: parsed, ..Default::default() }]])
    }
}

inventory::submit! { NodeFactory::new("coingecko", || Arc::new(CoinGeckoNode)) }
