// 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)
//
// `crypto` — hash, hmac, encode/decode. Pure-Rust crates, no openssl.

use async_trait::async_trait;
use base64::Engine as _;
use crate::core::error::NodeError;
use crate::core::types::*;
use crate::engine::{ExecuteCtx, Node, NodeFactory};
use hmac::{Hmac, Mac};
use serde_json::Value;
use sha2::{Digest, Sha256, Sha512};
use std::sync::Arc;

pub struct CryptoNode;

static META: NodeMetadata = NodeMetadata {
    name: "crypto",
    display_name: "Crypto",
    version: &[1],
    group: &["transform"],
    description: "Hashing, HMAC, base64/hex encoding",
    inputs: 1,
    outputs: 1,
    properties: &[],
    credentials: &[],
};

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

    async fn execute(&self, ctx: &mut ExecuteCtx<'_>) -> Result<NodeOutput, NodeError> {
        let action = ctx.node_def.parameters.get("action")
            .and_then(|v| v.as_str())
            .unwrap_or("hash")
            .to_string();
        let algorithm = ctx.node_def.parameters.get("algorithm")
            .and_then(|v| v.as_str())
            .unwrap_or("sha256")
            .to_string();
        let encoding = ctx.node_def.parameters.get("encoding")
            .and_then(|v| v.as_str())
            .unwrap_or("hex")
            .to_string();
        let field = ctx.node_def.parameters.get("output_field")
            .and_then(|v| v.as_str())
            .unwrap_or("digest")
            .to_string();

        let items_in = if ctx.input.is_empty() { vec![NodeItem::default()] } else { ctx.input.clone() };
        let mut out = Vec::with_capacity(items_in.len());
        for item in items_in {
            let value = ctx.param_str("value").unwrap_or_default();
            let bytes = value.as_bytes();
            let raw: Vec<u8> = match action.as_str() {
                "hash" => match algorithm.as_str() {
                    "sha256" => Sha256::digest(bytes).to_vec(),
                    "sha512" => Sha512::digest(bytes).to_vec(),
                    "sha1" => sha1::Sha1::digest(bytes).to_vec(),
                    "md5" => md5::Md5::digest(bytes).to_vec(),
                    other => return Err(NodeError::Other(format!("crypto: unknown hash algorithm `{}`", other))),
                },
                "hmac" => {
                    let secret = ctx.param_str("secret")?;
                    match algorithm.as_str() {
                        "sha256" => {
                            let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes())
                                .map_err(|e| NodeError::Other(format!("crypto: hmac key: {}", e)))?;
                            mac.update(bytes);
                            mac.finalize().into_bytes().to_vec()
                        }
                        "sha512" => {
                            let mut mac = Hmac::<Sha512>::new_from_slice(secret.as_bytes())
                                .map_err(|e| NodeError::Other(format!("crypto: hmac key: {}", e)))?;
                            mac.update(bytes);
                            mac.finalize().into_bytes().to_vec()
                        }
                        other => return Err(NodeError::Other(format!("crypto: unknown hmac algorithm `{}`", other))),
                    }
                }
                "encode" => match encoding.as_str() {
                    "base64" => return Ok(vec![one_field(item, &field, base64::engine::general_purpose::STANDARD.encode(bytes))]),
                    "hex" => return Ok(vec![one_field(item, &field, hex::encode(bytes))]),
                    other => return Err(NodeError::Other(format!("crypto: unknown encoding `{}`", other))),
                },
                "decode" => {
                    let decoded = match encoding.as_str() {
                        "base64" => base64::engine::general_purpose::STANDARD.decode(bytes)
                            .map_err(|e| NodeError::Other(format!("crypto: base64 decode: {}", e)))?,
                        "hex" => hex::decode(bytes)
                            .map_err(|e| NodeError::Other(format!("crypto: hex decode: {}", e)))?,
                        other => return Err(NodeError::Other(format!("crypto: unknown encoding `{}`", other))),
                    };
                    let s = String::from_utf8_lossy(&decoded).into_owned();
                    return Ok(vec![one_field(item, &field, s)]);
                }
                other => return Err(NodeError::Other(format!("crypto: unknown action `{}`", other))),
            };
            let encoded = match encoding.as_str() {
                "base64" => base64::engine::general_purpose::STANDARD.encode(&raw),
                "hex" => hex::encode(&raw),
                other => return Err(NodeError::Other(format!("crypto: unknown encoding `{}`", other))),
            };
            let mut new_json = if item.json.is_object() { item.json.clone() } else { Value::Object(Default::default()) };
            new_json.as_object_mut().unwrap().insert(field.clone(), Value::String(encoded));
            out.push(NodeItem { json: new_json, ..item });
        }
        Ok(vec![out])
    }
}

fn one_field(item: NodeItem, field: &str, value: String) -> Vec<NodeItem> {
    let mut new_json = if item.json.is_object() { item.json.clone() } else { Value::Object(Default::default()) };
    new_json.as_object_mut().unwrap().insert(field.to_string(), Value::String(value));
    vec![NodeItem { json: new_json, ..item }]
}

inventory::submit! { NodeFactory::new("crypto", || Arc::new(CryptoNode)) }
