// 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)
//
// `supabase` — row CRUD via PostgREST under https://<host>/rest/v1.
// Param names lifted from upstream Supabase.node.ts: resource (row),
// operation (create/delete/get/getAll/update), tableId, dataToSend,
// fieldsUi, filters, returnAll, limit, useCustomSchema, schema.
// Credential `supabaseApi`: { host, serviceRole }.

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

static META: NodeMetadata = NodeMetadata {
    name: "supabase",
    display_name: "Supabase",
    version: &[1],
    group: &["transform"],
    description: "Add, get, delete and update data in Supabase",
    inputs: 1,
    outputs: 1,
    properties: &[
        PropertyDef { name: "useCustomSchema", display_name: "Use Custom Schema", kind: PropertyKind::Boolean, required: false, default: Some("false") },
        PropertyDef { name: "schema", display_name: "Schema", kind: PropertyKind::String, required: false, default: Some("public") },
        PropertyDef { name: "resource", display_name: "Resource", kind: PropertyKind::Options, required: true, default: Some("row") },
        PropertyDef { name: "operation", display_name: "Operation", kind: PropertyKind::Options, required: true, default: Some("getAll") },
        PropertyDef { name: "tableId", display_name: "Table", kind: PropertyKind::String, required: true, default: None },
        PropertyDef { name: "dataToSend", display_name: "Data to Send", kind: PropertyKind::Options, required: false, default: Some("autoMapInputData") },
        PropertyDef { name: "fieldsUi", display_name: "Fields", kind: PropertyKind::Json, required: false, default: None },
        PropertyDef { name: "filters", display_name: "Filters", kind: PropertyKind::Json, 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("50") },
    ],
    credentials: &[CredentialRef { name: "supabaseApi", required: true }],
};

fn build_filter_qs(filters: &Value) -> Vec<(String, String)> {
    let mut qs = Vec::new();
    if let Some(Value::Array(conds)) = filters.get("conditions") {
        for c in conds {
            let key = c.get("keyName").and_then(|v| v.as_str()).unwrap_or("");
            let val = c.get("keyValue").and_then(|v| v.as_str()).unwrap_or("");
            let cond = c.get("condition").and_then(|v| v.as_str()).unwrap_or("eq");
            if !key.is_empty() {
                qs.push((key.to_string(), format!("{}.{}", cond, val)));
            }
        }
    }
    qs
}

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

    async fn execute(&self, ctx: &mut ExecuteCtx<'_>) -> Result<NodeOutput, NodeError> {
        let host = ctx.credential_field("host")?
            .as_str().ok_or_else(|| NodeError::MissingCredential("supabaseApi.host".into()))?
            .trim_end_matches('/')
            .to_string();
        let service_role = ctx.credential_field("serviceRole")?
            .as_str().ok_or_else(|| NodeError::MissingCredential("supabaseApi.serviceRole".into()))?
            .to_string();

        let table = ctx.param_str("tableId")?;
        let operation = ctx.param_opt_str("operation").unwrap_or_else(|| "getAll".into());
        let use_custom = ctx.node_def.parameters.get("useCustomSchema")
            .and_then(|v| v.as_bool()).unwrap_or(false);
        let schema = if use_custom {
            ctx.param_opt_str("schema").unwrap_or_else(|| "public".into())
        } else {
            "public".into()
        };

        let url = format!("{}/rest/v1/{}", host, table);
        let client = reqwest::Client::new();
        let mut req = match operation.as_str() {
            "create" => client.post(&url),
            "delete" => client.delete(&url),
            "get" | "getAll" => client.get(&url),
            "update" => client.patch(&url),
            other => return Err(NodeError::Other(format!("supabase: {} not implemented", other))),
        };
        req = req.bearer_auth(&service_role)
            .header("apikey", &service_role)
            .header("Accept-Profile", &schema)
            .header("Content-Profile", &schema)
            .header("Prefer", "return=representation");

        if matches!(operation.as_str(), "create" | "update") {
            let fields = ctx.node_def.parameters.get("fieldsUi").cloned()
                .unwrap_or(Value::Object(Default::default()));
            req = req.json(&fields);
        }
        let filters = ctx.node_def.parameters.get("filters").cloned().unwrap_or(Value::Null);
        for (k, v) in build_filter_qs(&filters) {
            req = req.query(&[(k.as_str(), v.as_str())]);
        }
        if operation == "getAll" {
            let return_all = ctx.node_def.parameters.get("returnAll")
                .and_then(|v| v.as_bool()).unwrap_or(false);
            if !return_all {
                let limit = ctx.node_def.parameters.get("limit")
                    .and_then(|v| v.as_u64()).unwrap_or(50);
                req = req.query(&[("limit", limit.to_string())]);
            }
        }

        let resp = req.send().await.map_err(|e| NodeError::Http(format!("supabase: {}", e)))?;
        let status = resp.status();
        let parsed: Value = resp.json().await.unwrap_or(Value::Null);
        if !status.is_success() {
            return Err(NodeError::Http(format!("supabase {}: {}", status, parsed)));
        }
        Ok(vec![vec![NodeItem { json: parsed, ..Default::default() }]])
    }
}

inventory::submit! { NodeFactory::new("supabase", || Arc::new(SupabaseNode)) }
