// 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)
//
// `aws_s3` — minimal S3 client (list / get-object / put-object) with
// hand-rolled SigV4 signing. No aws-sdk-s3 dep.
// Param names: resource (file/bucket/folder), operation, bucketName,
// fileKey, fileName, fileContent, prefix, options.
// Credential `s3`: { endpoint, region, accessKeyId, secretAccessKey,
// forcePathStyle, ignoreSSLIssues }.

use async_trait::async_trait;
use crate::core::error::NodeError;
use crate::core::types::*;
use crate::engine::{ExecuteCtx, Node, NodeFactory};
use chrono::Utc;
use hmac::{Hmac, Mac};
use sha2::{Digest, Sha256};
use serde_json::{json, Value};
use std::sync::Arc;

type HmacSha256 = Hmac<Sha256>;

pub struct AwsS3Node;
pub struct S3Node;

const PROPS: &[PropertyDef] = &[
    PropertyDef { name: "resource", display_name: "Resource", kind: PropertyKind::Options, required: true, default: Some("file") },
    PropertyDef { name: "operation", display_name: "Operation", kind: PropertyKind::Options, required: true, default: Some("getAll") },
    PropertyDef { name: "bucketName", display_name: "Bucket Name", kind: PropertyKind::String, required: true, default: None },
    PropertyDef { name: "fileKey", display_name: "File Key", kind: PropertyKind::String, required: false, default: None },
    PropertyDef { name: "fileName", display_name: "File Name", kind: PropertyKind::String, required: false, default: None },
    PropertyDef { name: "fileContent", display_name: "File Content", kind: PropertyKind::String, required: false, default: None },
    PropertyDef { name: "prefix", display_name: "Prefix", kind: PropertyKind::String, required: false, default: None },
    PropertyDef { name: "options", display_name: "Options", kind: PropertyKind::Json, required: false, default: None },
];

const CREDS: &[CredentialRef] = &[CredentialRef { name: "s3", required: true }];

static META_AWS: NodeMetadata = NodeMetadata {
    name: "awss3",
    display_name: "AWS S3",
    version: &[1],
    group: &["output"],
    description: "Upload, list, and download S3 objects",
    inputs: 1,
    outputs: 1,
    properties: PROPS,
    credentials: CREDS,
};

static META_S3: NodeMetadata = NodeMetadata {
    name: "s3",
    display_name: "S3",
    version: &[1],
    group: &["output"],
    description: "Sends data to any S3-compatible service (Minio, Digital Ocean Spaces, etc.)",
    inputs: 1,
    outputs: 1,
    properties: PROPS,
    credentials: CREDS,
};

fn hmac(key: &[u8], data: &[u8]) -> Vec<u8> {
    let mut m = HmacSha256::new_from_slice(key).expect("hmac key");
    m.update(data);
    m.finalize().into_bytes().to_vec()
}

fn sha256_hex(data: &[u8]) -> String {
    let mut h = Sha256::new();
    h.update(data);
    hex::encode(h.finalize())
}

fn sign(secret: &str, date: &str, region: &str, service: &str) -> Vec<u8> {
    let k = format!("AWS4{}", secret);
    let kdate = hmac(k.as_bytes(), date.as_bytes());
    let kregion = hmac(&kdate, region.as_bytes());
    let kservice = hmac(&kregion, service.as_bytes());
    hmac(&kservice, b"aws4_request")
}

async fn execute_s3(ctx: &mut ExecuteCtx<'_>) -> Result<NodeOutput, NodeError> {
        let access_key = ctx.credential_field("accessKeyId")?
            .as_str().ok_or_else(|| NodeError::MissingCredential("s3.accessKeyId".into()))?
            .to_string();
        let secret_key = ctx.credential_field("secretAccessKey")?
            .as_str().ok_or_else(|| NodeError::MissingCredential("s3.secretAccessKey".into()))?
            .to_string();
        let region = ctx.credential_field("region").ok()
            .and_then(|v| v.as_str().map(|s| s.to_string()))
            .unwrap_or_else(|| "us-east-1".into());
        let endpoint = ctx.credential_field("endpoint").ok()
            .and_then(|v| v.as_str().map(|s| s.trim_end_matches('/').to_string()))
            .unwrap_or_else(|| format!("https://s3.{}.amazonaws.com", region));

        let resource = ctx.param_opt_str("resource").unwrap_or_else(|| "file".into());
        let operation = ctx.param_opt_str("operation").unwrap_or_else(|| "getAll".into());
        let bucket = ctx.param_str("bucketName")?;

        let (method, path, qs, payload): (&str, String, Vec<(String, String)>, Vec<u8>) = match (resource.as_str(), operation.as_str()) {
            ("file", "getAll") | ("bucket", "getAll") => {
                let mut qs = vec![("list-type".to_string(), "2".to_string())];
                if let Some(p) = ctx.param_opt_str("prefix") {
                    qs.push(("prefix".to_string(), p));
                }
                ("GET", format!("/{}", bucket), qs, vec![])
            }
            ("file", "download") | ("file", "get") => {
                let key = ctx.param_str("fileKey")?;
                ("GET", format!("/{}/{}", bucket, key), vec![], vec![])
            }
            ("file", "upload") => {
                let key = ctx.param_str("fileName")?;
                let content = ctx.param_opt_str("fileContent").unwrap_or_default();
                ("PUT", format!("/{}/{}", bucket, key), vec![], content.into_bytes())
            }
            ("file", "delete") => {
                let key = ctx.param_str("fileKey")?;
                ("DELETE", format!("/{}/{}", bucket, key), vec![], vec![])
            }
            (r, o) => return Err(NodeError::Other(format!("aws_s3: {}.{} not implemented", r, o))),
        };

        let now = Utc::now();
        let amz_date = now.format("%Y%m%dT%H%M%SZ").to_string();
        let date_stamp = now.format("%Y%m%d").to_string();
        let host = endpoint.trim_start_matches("http://").trim_start_matches("https://").to_string();

        let mut qs_sorted = qs.clone();
        qs_sorted.sort();
        let canonical_qs = qs_sorted.iter()
            .map(|(k, v)| format!("{}={}", urlencoding::encode(k), urlencoding::encode(v)))
            .collect::<Vec<_>>()
            .join("&");
        let payload_hash = sha256_hex(&payload);
        let canonical_headers = format!("host:{}\nx-amz-content-sha256:{}\nx-amz-date:{}\n", host, payload_hash, amz_date);
        let signed_headers = "host;x-amz-content-sha256;x-amz-date";
        let canonical_request = format!(
            "{}\n{}\n{}\n{}\n{}\n{}",
            method, path, canonical_qs, canonical_headers, signed_headers, payload_hash
        );
        let credential_scope = format!("{}/{}/s3/aws4_request", date_stamp, region);
        let string_to_sign = format!(
            "AWS4-HMAC-SHA256\n{}\n{}\n{}",
            amz_date, credential_scope, sha256_hex(canonical_request.as_bytes())
        );
        let signing_key = sign(&secret_key, &date_stamp, &region, "s3");
        let signature = hex::encode(hmac(&signing_key, string_to_sign.as_bytes()));
        let auth = format!(
            "AWS4-HMAC-SHA256 Credential={}/{}, SignedHeaders={}, Signature={}",
            access_key, credential_scope, signed_headers, signature
        );

        let url = if canonical_qs.is_empty() {
            format!("{}{}", endpoint, path)
        } else {
            format!("{}{}?{}", endpoint, path, canonical_qs)
        };
        let mut req = reqwest::Client::new()
            .request(method.parse().unwrap(), &url)
            .header("Authorization", &auth)
            .header("x-amz-date", &amz_date)
            .header("x-amz-content-sha256", &payload_hash)
            .header("Host", &host);
        if !payload.is_empty() {
            req = req.body(payload);
        }
        let resp = req.send().await.map_err(|e| NodeError::Http(format!("aws_s3: {}", e)))?;
        let status = resp.status();
        let body_text = resp.text().await.unwrap_or_default();
        if !status.is_success() {
            return Err(NodeError::Http(format!("aws_s3 {}: {}", status, body_text)));
        }
        let parsed: Value = serde_json::from_str(&body_text)
            .unwrap_or_else(|_| json!({ "raw": body_text }));
        Ok(vec![vec![NodeItem { json: parsed, ..Default::default() }]])
}

#[async_trait]
impl Node for AwsS3Node {
    fn metadata(&self) -> &'static NodeMetadata { &META_AWS }
    async fn execute(&self, ctx: &mut ExecuteCtx<'_>) -> Result<NodeOutput, NodeError> {
        execute_s3(ctx).await
    }
}

#[async_trait]
impl Node for S3Node {
    fn metadata(&self) -> &'static NodeMetadata { &META_S3 }
    async fn execute(&self, ctx: &mut ExecuteCtx<'_>) -> Result<NodeOutput, NodeError> {
        execute_s3(ctx).await
    }
}

inventory::submit! { NodeFactory::new("awss3", || Arc::new(AwsS3Node)) }
inventory::submit! { NodeFactory::new("s3", || Arc::new(S3Node)) }
