// 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)
//
// `email_send` — send an email via SMTP.
// Credential: smtp = { host, port?, user?, password?, starttls?, stub? }
// stub = true skips the real SMTP transport and uses lettre's
// AsyncStubTransport — captures the message without dialing out. Lets
// the test suite exercise the full message-build path without an SMTP
// fixture daemon.

use async_trait::async_trait;
use crate::core::error::NodeError;
use crate::core::types::*;
use crate::engine::{ExecuteCtx, Node, NodeFactory};
use lettre::message::header::ContentType;
use lettre::transport::smtp::authentication::Credentials;
use lettre::transport::stub::AsyncStubTransport;
use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor};
use serde_json::json;
use std::sync::Arc;

pub struct EmailSendNode;

static META: NodeMetadata = NodeMetadata {
    name: "email_send",
    display_name: "Send Email",
    version: &[1, 2],
    group: &["output"],
    description: "Send an email via SMTP",
    inputs: 1,
    outputs: 1,
    properties: &[],
    credentials: &[CredentialRef { name: "smtp", required: true }],
};

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

    async fn execute(&self, ctx: &mut ExecuteCtx<'_>) -> Result<NodeOutput, NodeError> {
        let stub_mode = ctx.credential_field("stub").ok()
            .and_then(|v| v.as_bool()).unwrap_or(false);

        let from = ctx.param_str("from")?;
        let to = ctx.param_str("to")?;
        let subject = ctx.param_str("subject")?;
        let body = ctx.param_str("body")?;
        let html = ctx.node_def.parameters.get("html")
            .and_then(|v| v.as_bool()).unwrap_or(false);

        let from_addr = from.parse().map_err(|e| NodeError::Other(format!("email: from: {}", e)))?;
        let to_addr = to.parse().map_err(|e| NodeError::Other(format!("email: to: {}", e)))?;

        let mut builder = Message::builder()
            .from(from_addr)
            .to(to_addr)
            .subject(&subject);
        builder = builder.header(if html { ContentType::TEXT_HTML } else { ContentType::TEXT_PLAIN });
        let msg = builder.body(body)
            .map_err(|e| NodeError::Other(format!("email: build: {}", e)))?;

        if stub_mode {
            let transport = AsyncStubTransport::new_ok();
            transport.send(msg).await
                .map_err(|e| NodeError::Other(format!("email: stub: {}", e)))?;
            return Ok(vec![vec![NodeItem {
                json: json!({ "code": "stub-ok", "message": ["captured by stub transport, no SMTP dialed"], "stub": true }),
                ..Default::default()
            }]]);
        }

        let host = ctx.credential_field("host")?
            .as_str()
            .ok_or_else(|| NodeError::MissingCredential("smtp.host".into()))?
            .to_string();
        let port = ctx.credential_field("port").ok()
            .and_then(|v| v.as_u64()).map(|n| n as u16).unwrap_or(587);
        let starttls = ctx.credential_field("starttls").ok()
            .and_then(|v| v.as_bool()).unwrap_or(true);
        let user = ctx.credential_field("user").ok()
            .and_then(|v| v.as_str().map(|s| s.to_string()));
        let password = ctx.credential_field("password").ok()
            .and_then(|v| v.as_str().map(|s| s.to_string()));

        let mut transport_builder = if starttls {
            AsyncSmtpTransport::<Tokio1Executor>::starttls_relay(&host)
                .map_err(|e| NodeError::Other(format!("email: tls: {}", e)))?
        } else {
            AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous(&host)
        };
        transport_builder = transport_builder.port(port);
        if let (Some(u), Some(p)) = (user, password) {
            transport_builder = transport_builder.credentials(Credentials::new(u, p));
        }
        let transport = transport_builder.build();
        let resp = transport.send(msg).await
            .map_err(|e| NodeError::Other(format!("email: send: {}", e)))?;

        Ok(vec![vec![NodeItem {
            json: json!({ "code": resp.code().to_string(), "message": resp.message().collect::<Vec<_>>() }),
            ..Default::default()
        }]])
    }
}

inventory::submit! { NodeFactory::new("email_send", || Arc::new(EmailSendNode)) }
