//! Notification *delivery* — the network half of the notify feature.
//!
//! `openpxe_core::notify` owns the config + persistence; this module
//! turns a `NotifyConfig` + a message into an actual delivery:
//!
//!   - Slack / Discord / Teams → HTTP POST of a provider-shaped JSON
//!     body to the operator's incoming-webhook URL (via `reqwest`).
//!   - SMTP → a TLS email via `lettre`.
//!
//! Every send is best-effort and time-bounded: a flaky webhook must
//! never wedge a PXE boot. Callers fire these from a detached task.

use openpxe_core::{NotifyConfig, NotifyKind};
use std::time::Duration;

/// Hard ceiling on any single delivery so a hung endpoint can't pin a
/// task forever.
const SEND_TIMEOUT: Duration = Duration::from_secs(10);

/// Deliver `body` (with an optional `subject`, used as the email
/// subject / chat bold-line) using the active provider in `cfg`.
/// Returns `Ok(())` on success, or a human-readable error suitable for
/// surfacing in the "Send test" response.
pub async fn send(cfg: &NotifyConfig, subject: &str, body: &str) -> Result<(), String> {
    if !cfg.is_usable() {
        return Err("notifications are not enabled / fully configured".into());
    }
    match cfg.kind {
        NotifyKind::Slack | NotifyKind::Discord | NotifyKind::Teams => {
            send_webhook(cfg, subject, body).await
        }
        NotifyKind::Smtp => send_email(cfg, subject, body).await,
    }
}

async fn send_webhook(cfg: &NotifyConfig, subject: &str, body: &str) -> Result<(), String> {
    // Each chat platform wants a different JSON shape for an incoming
    // webhook. Keep the bodies minimal and plain-text-ish so they
    // render cleanly everywhere.
    let combined = if subject.is_empty() {
        body.to_string()
    } else {
        format!("*{subject}*\n{body}")
    };
    let payload = match cfg.kind {
        NotifyKind::Slack => serde_json::json!({ "text": combined }),
        NotifyKind::Discord => serde_json::json!({ "content": combined }),
        NotifyKind::Teams => serde_json::json!({
            // Legacy MessageCard — the format every Teams "Incoming
            // Webhook" connector still accepts.
            "@type": "MessageCard",
            "@context": "https://schema.org/extensions",
            "summary": if subject.is_empty() { "OpenPXE" } else { subject },
            "title": subject,
            "text": body,
        }),
        NotifyKind::Smtp => unreachable!("smtp handled separately"),
    };

    let client = reqwest::Client::builder()
        .timeout(SEND_TIMEOUT)
        .build()
        .map_err(|e| format!("could not build HTTP client: {e}"))?;
    let resp = client
        .post(&cfg.webhook_url)
        .json(&payload)
        .send()
        .await
        .map_err(|e| format!("webhook POST failed: {e}"))?;
    let status = resp.status();
    if status.is_success() {
        Ok(())
    } else {
        let snippet = resp
            .text()
            .await
            .unwrap_or_default()
            .chars()
            .take(200)
            .collect::<String>();
        Err(format!("webhook returned HTTP {status}: {snippet}"))
    }
}

async fn send_email(cfg: &NotifyConfig, subject: &str, body: &str) -> Result<(), String> {
    use lettre::transport::smtp::authentication::Credentials;
    use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor};

    let from = if cfg.smtp_from.trim().is_empty() {
        cfg.smtp_username.trim()
    } else {
        cfg.smtp_from.trim()
    };
    if from.is_empty() {
        return Err("SMTP requires a From address (or a username to fall back to)".into());
    }

    let email = Message::builder()
        .from(
            from.parse()
                .map_err(|e| format!("invalid From address '{from}': {e}"))?,
        )
        .to(cfg
            .smtp_to
            .trim()
            .parse()
            .map_err(|e| format!("invalid To address '{}': {e}", cfg.smtp_to))?)
        .subject(if subject.is_empty() { "OpenPXE" } else { subject })
        .body(body.to_string())
        .map_err(|e| format!("could not build email: {e}"))?;

    // Implicit TLS (465) vs STARTTLS (587). We never send plaintext.
    let mut builder = if cfg.smtp_implicit_tls {
        AsyncSmtpTransport::<Tokio1Executor>::relay(&cfg.smtp_host)
            .map_err(|e| format!("SMTP relay setup failed: {e}"))?
    } else {
        AsyncSmtpTransport::<Tokio1Executor>::starttls_relay(&cfg.smtp_host)
            .map_err(|e| format!("SMTP STARTTLS setup failed: {e}"))?
    }
    .port(cfg.smtp_port)
    .timeout(Some(SEND_TIMEOUT));

    // Auth is optional — some internal relays accept unauthenticated
    // mail from trusted hosts. Only attach credentials when a username
    // is set.
    if !cfg.smtp_username.trim().is_empty() {
        builder = builder.credentials(Credentials::new(
            cfg.smtp_username.trim().to_string(),
            cfg.smtp_password.clone(),
        ));
    }
    let mailer = builder.build();
    mailer
        .send(email)
        .await
        .map(|_| ())
        .map_err(|e| format!("SMTP send failed: {e}"))
}
