// 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)
//
// `customer_io` — segments / campaigns / customers via Customer.io REST.
// Param names: resource (segment/campaign/customer/event), operation,
// segmentId, customerIds, campaignId, period, customerId, eventName, data.
// Credential `customerIoApi`: { trackingApiKey, trackingSiteId,
// appApiKey, region }.

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

static META: NodeMetadata = NodeMetadata {
    name: "customer_io",
    display_name: "Customer.io",
    version: &[1],
    group: &["output"],
    description: "Manage Customer.io segments, campaigns, and customers",
    inputs: 1,
    outputs: 1,
    properties: &[
        PropertyDef { name: "resource", display_name: "Resource", kind: PropertyKind::Options, required: true, default: Some("customer") },
        PropertyDef { name: "operation", display_name: "Operation", kind: PropertyKind::Options, required: true, default: Some("upsert") },
        PropertyDef { name: "segmentId", display_name: "Segment ID", kind: PropertyKind::String, required: false, default: None },
        PropertyDef { name: "customerIds", display_name: "Customer IDs", kind: PropertyKind::String, required: false, default: None },
        PropertyDef { name: "campaignId", display_name: "Campaign ID", kind: PropertyKind::String, required: false, default: None },
        PropertyDef { name: "period", display_name: "Period", kind: PropertyKind::String, required: false, default: None },
        PropertyDef { name: "customerId", display_name: "Customer ID", kind: PropertyKind::String, required: false, default: None },
        PropertyDef { name: "eventName", display_name: "Event Name", kind: PropertyKind::String, required: false, default: None },
        PropertyDef { name: "data", display_name: "Data", kind: PropertyKind::Json, required: false, default: None },
    ],
    credentials: &[CredentialRef { name: "customerIoApi", required: true }],
};

fn track_base(region: &str) -> String {
    match region {
        "EU region" | "EU" => "https://track-eu.customer.io".to_string(),
        _ => "https://track.customer.io".to_string(),
    }
}
fn beta_base(region: &str) -> String {
    match region {
        "EU region" | "EU" => "https://api-eu.customer.io".to_string(),
        _ => "https://api.customer.io".to_string(),
    }
}

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

    async fn execute(&self, ctx: &mut ExecuteCtx<'_>) -> Result<NodeOutput, NodeError> {
        let region = ctx.credential_field("region").ok()
            .and_then(|v| v.as_str().map(|s| s.to_string()))
            .unwrap_or_else(|| "Global region".to_string());
        let tracking_key = ctx.credential_field("trackingApiKey").ok()
            .and_then(|v| v.as_str().map(|s| s.to_string()));
        let tracking_site = ctx.credential_field("trackingSiteId").ok()
            .and_then(|v| v.as_str().map(|s| s.to_string()));
        let app_key = ctx.credential_field("appApiKey").ok()
            .and_then(|v| v.as_str().map(|s| s.to_string()));
        let base_override = ctx.credential_field("base_url").ok()
            .and_then(|v| v.as_str().map(|s| s.trim_end_matches('/').to_string()));
        let track_url = base_override.clone().unwrap_or_else(|| track_base(&region));
        let beta_url = base_override.unwrap_or_else(|| beta_base(&region));

        let resource = ctx.param_opt_str("resource").unwrap_or_else(|| "customer".into());
        let operation = ctx.param_opt_str("operation").unwrap_or_else(|| "upsert".into());

        match (resource.as_str(), operation.as_str()) {
            ("customer", "upsert") => {
                let id = ctx.param_str("customerId")?;
                let data = ctx.node_def.parameters.get("data").cloned()
                    .unwrap_or(Value::Object(Default::default()));
                let url = format!("{}/api/v1/customers/{}", track_url, id);
                let (key, secret) = (
                    tracking_site.ok_or_else(|| NodeError::MissingCredential("customerIoApi.trackingSiteId".into()))?,
                    tracking_key.ok_or_else(|| NodeError::MissingCredential("customerIoApi.trackingApiKey".into()))?,
                );
                let resp = reqwest::Client::new().put(&url)
                    .basic_auth(&key, Some(&secret))
                    .json(&data)
                    .send().await.map_err(|e| NodeError::Http(format!("customer_io: {}", e)))?;
                response_to_item(resp).await
            }
            ("event", "track") => {
                let id = ctx.param_str("customerId")?;
                let event_name = ctx.param_str("eventName")?;
                let data = ctx.node_def.parameters.get("data").cloned()
                    .unwrap_or(Value::Object(Default::default()));
                let url = format!("{}/api/v1/customers/{}/events", track_url, id);
                let (key, secret) = (
                    tracking_site.ok_or_else(|| NodeError::MissingCredential("customerIoApi.trackingSiteId".into()))?,
                    tracking_key.ok_or_else(|| NodeError::MissingCredential("customerIoApi.trackingApiKey".into()))?,
                );
                let payload = json!({ "name": event_name, "data": data });
                let resp = reqwest::Client::new().post(&url)
                    .basic_auth(&key, Some(&secret))
                    .json(&payload)
                    .send().await.map_err(|e| NodeError::Http(format!("customer_io: {}", e)))?;
                response_to_item(resp).await
            }
            ("segment", "addCustomers") | ("segment", "removeCustomers") => {
                let segment_id = ctx.param_str("segmentId")?;
                let ids: Vec<String> = ctx.param_str("customerIds")?.split(',').map(|s| s.trim().to_string()).collect();
                let action = if operation == "addCustomers" { "add_customers" } else { "remove_customers" };
                let url = format!("{}/api/v1/segments/{}/{}", track_url, segment_id, action);
                let (key, secret) = (
                    tracking_site.ok_or_else(|| NodeError::MissingCredential("customerIoApi.trackingSiteId".into()))?,
                    tracking_key.ok_or_else(|| NodeError::MissingCredential("customerIoApi.trackingApiKey".into()))?,
                );
                let resp = reqwest::Client::new().post(&url)
                    .basic_auth(&key, Some(&secret))
                    .json(&json!({ "ids": ids }))
                    .send().await.map_err(|e| NodeError::Http(format!("customer_io: {}", e)))?;
                response_to_item(resp).await
            }
            ("campaign", "get") | ("campaign", "getAll") | ("campaign", "getMetrics") => {
                let app_key = app_key.ok_or_else(|| NodeError::MissingCredential("customerIoApi.appApiKey".into()))?;
                let url = match operation.as_str() {
                    "getAll" => format!("{}/v1/api/campaigns", beta_url),
                    "get" => {
                        let id = ctx.param_str("campaignId")?;
                        format!("{}/v1/api/campaigns/{}", beta_url, id)
                    }
                    _ => {
                        let id = ctx.param_str("campaignId")?;
                        let period = ctx.param_opt_str("period").unwrap_or_else(|| "days".into());
                        format!("{}/v1/api/campaigns/{}/metrics?period={}", beta_url, id, period)
                    }
                };
                let resp = reqwest::Client::new().get(&url).bearer_auth(&app_key)
                    .send().await.map_err(|e| NodeError::Http(format!("customer_io: {}", e)))?;
                response_to_item(resp).await
            }
            (r, o) => Err(NodeError::Other(format!("customer_io: {}.{} not implemented", r, o))),
        }
    }
}

async fn response_to_item(resp: reqwest::Response) -> Result<NodeOutput, NodeError> {
    let status = resp.status();
    let parsed: Value = resp.json().await.unwrap_or(Value::Null);
    if !status.is_success() {
        return Err(NodeError::Http(format!("customer_io {}: {}", status, parsed)));
    }
    Ok(vec![vec![NodeItem { json: parsed, ..Default::default() }]])
}

inventory::submit! { NodeFactory::new("customer_io", || Arc::new(CustomerIoNode)) }
