//! WhatsApp Twilio transport — sends and receives messages via the Twilio API.
use async_trait::async_trait;
use serde::Deserialize;

use crate::chat::{ChatTransport, MessageId};
use crate::slog;

// ── API base URL ─────────────────────────────────────────────────────────

pub(super) const TWILIO_API_BASE: &str = "https://api.twilio.com";

// ── Twilio Transport ────────────────────────────────────────────────────

/// WhatsApp transport that routes through Twilio's REST API.
///
/// Sends messages via `POST {TWILIO_API_BASE}/2010-04-01/Accounts/{account_sid}/Messages.json`
/// using HTTP Basic Auth (Account SID as username, Auth Token as password).
///
/// Inbound messages from Twilio arrive as `application/x-www-form-urlencoded`
/// POST bodies; use [`extract_twilio_text_messages`] to parse them.
pub struct TwilioWhatsAppTransport {
    account_sid: String,
    auth_token: String,
    /// Sender number in E.164 format, e.g. `+14155551234`.
    from_number: String,
    client: reqwest::Client,
    /// Optional base URL override for tests.
    api_base: String,
}

impl TwilioWhatsAppTransport {
    /// Creates a new `TwilioWhatsAppTransport` authenticated with the given Twilio credentials.
    pub fn new(account_sid: String, auth_token: String, from_number: String) -> Self {
        Self {
            account_sid,
            auth_token,
            from_number,
            client: reqwest::Client::new(),
            api_base: TWILIO_API_BASE.to_string(),
        }
    }

    #[cfg(test)]
    pub(crate) fn with_api_base(
        account_sid: String,
        auth_token: String,
        from_number: String,
        api_base: String,
    ) -> Self {
        Self {
            account_sid,
            auth_token,
            from_number,
            client: reqwest::Client::new(),
            api_base,
        }
    }

    /// Send a WhatsApp message via Twilio's Messaging REST API.
    async fn send_text(&self, to: &str, body: &str) -> Result<String, String> {
        let url = format!(
            "{}/2010-04-01/Accounts/{}/Messages.json",
            self.api_base, self.account_sid
        );

        // Twilio expects the WhatsApp number with a "whatsapp:" prefix.
        let from = if self.from_number.starts_with("whatsapp:") {
            self.from_number.clone()
        } else {
            format!("whatsapp:{}", self.from_number)
        };
        let to_wa = if to.starts_with("whatsapp:") {
            to.to_string()
        } else {
            format!("whatsapp:{}", to)
        };

        let params = [
            ("From", from.as_str()),
            ("To", to_wa.as_str()),
            ("Body", body),
        ];

        let resp = self
            .client
            .post(&url)
            .basic_auth(&self.account_sid, Some(&self.auth_token))
            .form(&params)
            .send()
            .await
            .map_err(|e| format!("Twilio API request failed: {e}"))?;

        let status = resp.status();
        let resp_text = resp
            .text()
            .await
            .unwrap_or_else(|_| "<no body>".to_string());

        if !status.is_success() {
            return Err(format!("Twilio API returned {status}: {resp_text}"));
        }

        let parsed: TwilioSendResponse = serde_json::from_str(&resp_text)
            .map_err(|e| format!("Failed to parse Twilio API response: {e} — body: {resp_text}"))?;

        Ok(parsed.sid.unwrap_or_default())
    }
}

#[async_trait]
impl ChatTransport for TwilioWhatsAppTransport {
    async fn send_message(
        &self,
        recipient: &str,
        plain: &str,
        _html: &str,
    ) -> Result<MessageId, String> {
        slog!("[whatsapp/twilio] send_message to {recipient}: {plain:.80}");
        self.send_text(recipient, plain).await
    }

    async fn edit_message(
        &self,
        recipient: &str,
        _original_message_id: &str,
        plain: &str,
        html: &str,
    ) -> Result<(), String> {
        // Twilio does not support message editing — send a new message.
        slog!(
            "[whatsapp/twilio] edit_message — Twilio does not support edits, sending new message"
        );
        self.send_message(recipient, plain, html).await.map(|_| ())
    }

    async fn send_typing(&self, _recipient: &str, _typing: bool) -> Result<(), String> {
        // Twilio WhatsApp API does not expose typing indicators.
        Ok(())
    }
}

// ── Twilio API request/response types ──────────────────────────────────

#[derive(Deserialize)]
struct TwilioSendResponse {
    sid: Option<String>,
}

// ── Twilio webhook types (Twilio → us) ─────────────────────────────────

/// Form-encoded fields from a Twilio WhatsApp inbound webhook POST.
#[derive(Deserialize, Debug)]
pub struct TwilioWebhookForm {
    /// Sender number with `whatsapp:` prefix, e.g. `whatsapp:+15551234567`.
    #[serde(rename = "From")]
    pub from: Option<String>,
    /// Message body text.
    #[serde(rename = "Body")]
    pub body: Option<String>,
}

/// Extract text messages from a Twilio form-encoded webhook body.
///
/// Returns `(sender_phone, message_body)` pairs, with the `whatsapp:` prefix
/// stripped from the sender number.
pub fn extract_twilio_text_messages(bytes: &[u8]) -> Vec<(String, String)> {
    let form: TwilioWebhookForm = match serde_urlencoded::from_bytes(bytes) {
        Ok(f) => f,
        Err(e) => {
            slog!("[whatsapp/twilio] Failed to parse webhook form body: {e}");
            return vec![];
        }
    };

    let from = match form.from {
        Some(f) => f,
        None => return vec![],
    };
    let body = match form.body {
        Some(b) if !b.is_empty() => b,
        _ => return vec![],
    };

    // Strip the "whatsapp:" prefix so the sender is stored as a plain phone number.
    let sender = from.strip_prefix("whatsapp:").unwrap_or(&from).to_string();

    vec![(sender, body)]
}

// ── Tests ───────────────────────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use super::*;

    // ── TwilioWhatsAppTransport tests ─────────────────────────────────

    #[tokio::test]
    async fn twilio_send_message_calls_twilio_api() {
        let mut server = mockito::Server::new_async().await;
        let mock = server
            .mock("POST", "/2010-04-01/Accounts/ACtest/Messages.json")
            .with_body(r#"{"sid": "SMtest123"}"#)
            .create_async()
            .await;

        let transport = TwilioWhatsAppTransport::with_api_base(
            "ACtest".to_string(),
            "authtoken".to_string(),
            "+14155551234".to_string(),
            server.url(),
        );

        let result = transport.send_message("+15551234567", "hello", "").await;
        assert!(result.is_ok(), "unexpected err: {:?}", result.err());
        assert_eq!(result.unwrap(), "SMtest123");
        mock.assert_async().await;
    }

    #[tokio::test]
    async fn twilio_send_message_returns_err_on_api_error() {
        let mut server = mockito::Server::new_async().await;
        server
            .mock("POST", "/2010-04-01/Accounts/ACtest/Messages.json")
            .with_status(401)
            .with_body(r#"{"message": "Unauthorized"}"#)
            .create_async()
            .await;

        let transport = TwilioWhatsAppTransport::with_api_base(
            "ACtest".to_string(),
            "badtoken".to_string(),
            "+14155551234".to_string(),
            server.url(),
        );

        let result = transport.send_message("+15551234567", "hello", "").await;
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("401"));
    }

    #[tokio::test]
    async fn twilio_edit_message_sends_new_message() {
        let mut server = mockito::Server::new_async().await;
        let mock = server
            .mock("POST", "/2010-04-01/Accounts/ACtest/Messages.json")
            .with_body(r#"{"sid": "SMedit456"}"#)
            .create_async()
            .await;

        let transport = TwilioWhatsAppTransport::with_api_base(
            "ACtest".to_string(),
            "authtoken".to_string(),
            "+14155551234".to_string(),
            server.url(),
        );

        let result = transport
            .edit_message("+15551234567", "old-sid", "updated text", "")
            .await;
        assert!(result.is_ok());
        mock.assert_async().await;
    }

    #[tokio::test]
    async fn twilio_send_typing_is_noop() {
        let transport = TwilioWhatsAppTransport::new(
            "ACtest".to_string(),
            "authtoken".to_string(),
            "+14155551234".to_string(),
        );
        assert!(transport.send_typing("+15551234567", true).await.is_ok());
    }

    // ── extract_twilio_text_messages tests ────────────────────────────

    #[test]
    fn extract_twilio_text_messages_parses_valid_form() {
        let body = b"From=whatsapp%3A%2B15551234567&Body=hello+world&To=whatsapp%3A%2B14155551234&MessageSid=SMtest";
        let msgs = extract_twilio_text_messages(body);
        assert_eq!(msgs.len(), 1);
        assert_eq!(msgs[0].0, "+15551234567");
        assert_eq!(msgs[0].1, "hello world");
    }

    #[test]
    fn extract_twilio_text_messages_strips_whatsapp_prefix() {
        let body = b"From=whatsapp%3A%2B15551234567&Body=hi";
        let msgs = extract_twilio_text_messages(body);
        assert_eq!(msgs.len(), 1);
        assert_eq!(msgs[0].0, "+15551234567");
    }

    #[test]
    fn extract_twilio_text_messages_returns_empty_on_missing_from() {
        let body = b"Body=hello";
        let msgs = extract_twilio_text_messages(body);
        assert!(msgs.is_empty());
    }

    #[test]
    fn extract_twilio_text_messages_returns_empty_on_missing_body() {
        let body = b"From=whatsapp%3A%2B15551234567";
        let msgs = extract_twilio_text_messages(body);
        assert!(msgs.is_empty());
    }

    #[test]
    fn extract_twilio_text_messages_returns_empty_on_empty_body() {
        let body = b"From=whatsapp%3A%2B15551234567&Body=";
        let msgs = extract_twilio_text_messages(body);
        assert!(msgs.is_empty());
    }

    #[test]
    fn extract_twilio_text_messages_returns_empty_on_invalid_form() {
        let body = b"not valid form encoded {{{{";
        // serde_urlencoded is lenient, so this might parse or return empty
        // Either way it must not panic.
        let _msgs = extract_twilio_text_messages(body);
    }
}
