//! WhatsApp Meta (Cloud API) transport — sends and receives messages via the Meta Graph API.
use async_trait::async_trait;
use serde::{Deserialize, Serialize};

use super::history::MessagingWindowTracker;
use crate::chat::{ChatTransport, MessageId};
use crate::slog;

// ── API base URLs (overridable for tests) ────────────────────────────────

pub(super) const GRAPH_API_BASE: &str = "https://graph.facebook.com/v21.0";

/// Graph API error code indicating the 24-hour messaging window has elapsed.
///
/// When Meta returns this code the caller must fall back to an approved message
/// template instead of free-form text.
pub(super) const ERROR_CODE_OUTSIDE_WINDOW: i64 = 131047;

/// Sentinel error string returned by [`WhatsAppTransport::send_text`] when the
/// Graph API reports that the 24-hour messaging window has expired.
pub(super) const OUTSIDE_WINDOW_ERR: &str = "OUTSIDE_MESSAGING_WINDOW";

// ── WhatsApp Transport ──────────────────────────────────────────────────

/// Real WhatsApp Business API transport.
///
/// Sends text messages via `POST {GRAPH_API_BASE}/{phone_number_id}/messages`.
/// Falls back to approved notification templates when the 24-hour window has
/// elapsed (Meta error 131047).
pub struct WhatsAppTransport {
    phone_number_id: String,
    access_token: String,
    client: reqwest::Client,
    /// Name of the approved Meta message template used for notifications
    /// outside the 24-hour messaging window.
    #[allow(dead_code)] // Used by Meta provider path (send_template_notification)
    notification_template_name: String,
    /// Optional base URL override for tests.
    api_base: String,
}

impl WhatsAppTransport {
    /// Creates a new `WhatsAppTransport` authenticated with the given Meta Cloud API credentials.
    pub fn new(
        phone_number_id: String,
        access_token: String,
        notification_template_name: String,
    ) -> Self {
        Self {
            phone_number_id,
            access_token,
            client: reqwest::Client::new(),
            notification_template_name,
            api_base: GRAPH_API_BASE.to_string(),
        }
    }

    #[cfg(test)]
    pub(crate) fn with_api_base(
        phone_number_id: String,
        access_token: String,
        api_base: String,
    ) -> Self {
        Self {
            phone_number_id,
            access_token,
            client: reqwest::Client::new(),
            notification_template_name: "pipeline_notification".to_string(),
            api_base,
        }
    }

    /// Send a free-form text message to a WhatsApp user via the Graph API.
    ///
    /// Returns [`OUTSIDE_WINDOW_ERR`] if the API responds with error code
    /// 131047 (messaging window expired).  All other errors are returned as
    /// descriptive strings.
    async fn send_text(&self, to: &str, body: &str) -> Result<String, String> {
        let url = format!("{}/{}/messages", self.api_base, self.phone_number_id);

        let payload = GraphSendMessage {
            messaging_product: "whatsapp",
            to,
            r#type: "text",
            text: GraphTextBody { body },
        };

        let resp = self
            .client
            .post(&url)
            .bearer_auth(&self.access_token)
            .json(&payload)
            .send()
            .await
            .map_err(|e| format!("WhatsApp 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() {
            // Check for 'outside messaging window' (code 131047).  Return a
            // distinct sentinel so callers can fall back to a template without
            // crashing.
            if let Ok(err_body) = serde_json::from_str::<GraphApiErrorResponse>(&resp_text)
                && err_body.error.as_ref().and_then(|e| e.code) == Some(ERROR_CODE_OUTSIDE_WINDOW)
            {
                slog!(
                    "[whatsapp] Outside 24-hour messaging window for {to}; \
                     template required (error 131047)"
                );
                return Err(OUTSIDE_WINDOW_ERR.to_string());
            }
            return Err(format!("WhatsApp API returned {status}: {resp_text}"));
        }

        // Extract the message ID from the response.
        let parsed: GraphSendResponse = serde_json::from_str(&resp_text).map_err(|e| {
            format!("Failed to parse WhatsApp API response: {e} — body: {resp_text}")
        })?;

        let msg_id = parsed
            .messages
            .first()
            .map(|m| m.id.clone())
            .unwrap_or_default();

        Ok(msg_id)
    }

    /// Send an approved template notification message.
    ///
    /// Used when the 24-hour window has expired and free-form text is not
    /// permitted.  The template must already be approved in the Meta Business
    /// Manager under the name configured in `bot.toml`
    /// (`whatsapp_notification_template`, default `pipeline_notification`).
    ///
    /// The template body is expected to accept two positional parameters:
    /// `{{1}}` = story name, `{{2}}` = pipeline stage.
    #[allow(dead_code)] // Meta provider path — template fallback for expired 24h window
    pub async fn send_template_notification(
        &self,
        to: &str,
        story_name: &str,
        stage: &str,
    ) -> Result<String, String> {
        let url = format!("{}/{}/messages", self.api_base, self.phone_number_id);

        let payload = GraphTemplateMessage {
            messaging_product: "whatsapp",
            to,
            r#type: "template",
            template: GraphTemplate {
                name: &self.notification_template_name,
                language: GraphLanguage { code: "en_US" },
                components: vec![GraphTemplateComponent {
                    r#type: "body",
                    parameters: vec![
                        GraphTemplateParameter {
                            r#type: "text",
                            text: story_name.to_string(),
                        },
                        GraphTemplateParameter {
                            r#type: "text",
                            text: stage.to_string(),
                        },
                    ],
                }],
            },
        };

        let resp = self
            .client
            .post(&url)
            .bearer_auth(&self.access_token)
            .json(&payload)
            .send()
            .await
            .map_err(|e| format!("WhatsApp template 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!(
                "WhatsApp template API returned {status}: {resp_text}"
            ));
        }

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

        let msg_id = parsed
            .messages
            .first()
            .map(|m| m.id.clone())
            .unwrap_or_default();

        Ok(msg_id)
    }

    /// Send a pipeline notification, respecting the 24-hour messaging window.
    ///
    /// - Within the window: sends a free-form text message.
    /// - Outside the window (or if the API returns 131047): sends an approved
    ///   template message instead.
    ///
    /// This method never crashes on a messaging-window error — it always
    /// attempts the template fallback and logs what happened.
    #[allow(dead_code)] // Meta provider path — window-aware notification dispatch
    pub async fn send_notification(
        &self,
        to: &str,
        tracker: &MessagingWindowTracker,
        story_name: &str,
        stage: &str,
    ) -> Result<String, String> {
        if tracker.is_within_window(to) {
            let text = format!("Story '{story_name}' has moved to {stage}.");
            match self.send_text(to, &text).await {
                Ok(id) => return Ok(id),
                Err(ref e) if e == OUTSIDE_WINDOW_ERR => {
                    // Window expired between our check and the API call —
                    // fall through to the template path.
                    slog!(
                        "[whatsapp] Window expired mid-flight for {to}; \
                         falling back to template"
                    );
                }
                Err(e) => return Err(e),
            }
        }

        // Outside window — use the approved template.
        slog!("[whatsapp] Sending template notification to {to} (outside 24h window)");
        self.send_template_notification(to, story_name, stage).await
    }
}

#[async_trait]
impl ChatTransport for WhatsAppTransport {
    async fn send_message(
        &self,
        recipient: &str,
        plain: &str,
        _html: &str,
    ) -> Result<MessageId, String> {
        slog!("[whatsapp] send_message to {recipient}: {plain:.80}");
        match self.send_text(recipient, plain).await {
            Ok(id) => Ok(id),
            Err(ref e) if e == OUTSIDE_WINDOW_ERR => {
                // Graceful degradation: log and surface a meaningful error
                // rather than crashing.  Callers sending command responses
                // should normally be within the window; this handles the edge
                // case where processing was delayed.
                slog!(
                    "[whatsapp] Cannot send to {recipient}: outside 24h window \
                     (message dropped)"
                );
                Err(format!(
                    "Outside 24-hour messaging window for {recipient}; \
                     send a message to the bot first to re-open the window"
                ))
            }
            Err(e) => Err(e),
        }
    }

    async fn edit_message(
        &self,
        recipient: &str,
        _original_message_id: &str,
        plain: &str,
        html: &str,
    ) -> Result<(), String> {
        // WhatsApp does not support message editing — send a new message.
        slog!("[whatsapp] edit_message — WhatsApp 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> {
        // WhatsApp Business API does not expose typing indicators.
        Ok(())
    }
}

// ── Graph API request/response types ────────────────────────────────────

#[derive(Serialize)]
struct GraphSendMessage<'a> {
    messaging_product: &'a str,
    to: &'a str,
    r#type: &'a str,
    text: GraphTextBody<'a>,
}

#[derive(Serialize)]
struct GraphTextBody<'a> {
    body: &'a str,
}

#[derive(Deserialize)]
struct GraphSendResponse {
    #[serde(default)]
    messages: Vec<GraphMessageId>,
}

#[derive(Deserialize)]
struct GraphMessageId {
    id: String,
}

// ── Graph API error response types ─────────────────────────────────────

#[derive(Deserialize)]
struct GraphApiErrorResponse {
    error: Option<GraphApiError>,
}

#[derive(Deserialize)]
struct GraphApiError {
    code: Option<i64>,
    #[allow(dead_code)]
    message: Option<String>,
}

// ── Template message types ──────────────────────────────────────────────

#[allow(dead_code)] // Meta provider path — template message types
#[derive(Serialize)]
struct GraphTemplateMessage<'a> {
    messaging_product: &'a str,
    to: &'a str,
    r#type: &'a str,
    template: GraphTemplate<'a>,
}

#[allow(dead_code)]
#[derive(Serialize)]
struct GraphTemplate<'a> {
    name: &'a str,
    language: GraphLanguage,
    components: Vec<GraphTemplateComponent>,
}

#[allow(dead_code)]
#[derive(Serialize)]
struct GraphLanguage {
    code: &'static str,
}

#[allow(dead_code)]
#[derive(Serialize)]
struct GraphTemplateComponent {
    r#type: &'static str,
    parameters: Vec<GraphTemplateParameter>,
}

#[allow(dead_code)]
#[derive(Serialize)]
struct GraphTemplateParameter {
    r#type: &'static str,
    text: String,
}

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

#[cfg(test)]
mod tests {
    use super::*;
    use crate::chat::transport::whatsapp::history::MessagingWindowTracker;

    // ── send_text error handling ───────────────────────────────────────

    #[tokio::test]
    async fn send_text_handles_131047_outside_window_error() {
        let mut server = mockito::Server::new_async().await;
        server
            .mock("POST", "/123456/messages")
            .with_status(400)
            .with_body(
                r#"{"error":{"message":"More than 24 hours have passed","type":"OAuthException","code":131047}}"#,
            )
            .create_async()
            .await;

        let transport = WhatsAppTransport::with_api_base(
            "123456".to_string(),
            "test-token".to_string(),
            server.url(),
        );

        let result = transport.send_text("15551234567", "hello").await;
        assert!(result.is_err());
        assert_eq!(result.unwrap_err(), OUTSIDE_WINDOW_ERR);
    }

    #[tokio::test]
    async fn send_message_handles_outside_window_gracefully() {
        let mut server = mockito::Server::new_async().await;
        server
            .mock("POST", "/123456/messages")
            .with_status(400)
            .with_body(
                r#"{"error":{"message":"More than 24 hours have passed","type":"OAuthException","code":131047}}"#,
            )
            .create_async()
            .await;

        let transport = WhatsAppTransport::with_api_base(
            "123456".to_string(),
            "test-token".to_string(),
            server.url(),
        );

        // send_message must not panic — it returns Err with a human-readable message.
        let result = transport.send_message("15551234567", "hello", "").await;
        assert!(result.is_err());
        let msg = result.unwrap_err();
        assert!(
            msg.contains("24-hour messaging window"),
            "unexpected: {msg}"
        );
    }

    // ── send_template_notification ────────────────────────────────────

    #[tokio::test]
    async fn send_template_notification_calls_graph_api() {
        let mut server = mockito::Server::new_async().await;
        let mock = server
            .mock("POST", "/123456/messages")
            .match_header("authorization", "Bearer test-token")
            .match_body(mockito::Matcher::PartialJsonString(
                r#"{"type":"template"}"#.to_string(),
            ))
            .with_body(r#"{"messages": [{"id": "wamid.tpl123"}]}"#)
            .create_async()
            .await;

        let transport = WhatsAppTransport::with_api_base(
            "123456".to_string(),
            "test-token".to_string(),
            server.url(),
        );

        let result = transport
            .send_template_notification("15551234567", "my-story", "done")
            .await;
        assert!(result.is_ok(), "unexpected err: {:?}", result.err());
        assert_eq!(result.unwrap(), "wamid.tpl123");
        mock.assert_async().await;
    }

    #[tokio::test]
    async fn send_notification_uses_text_within_window() {
        let mut server = mockito::Server::new_async().await;
        let mock = server
            .mock("POST", "/123456/messages")
            .match_body(mockito::Matcher::PartialJsonString(
                r#"{"type":"text"}"#.to_string(),
            ))
            .with_body(r#"{"messages": [{"id": "wamid.txt1"}]}"#)
            .create_async()
            .await;

        let transport = WhatsAppTransport::with_api_base(
            "123456".to_string(),
            "test-token".to_string(),
            server.url(),
        );
        let tracker = MessagingWindowTracker::new();
        tracker.record_message("15551234567");

        let result = transport
            .send_notification("15551234567", &tracker, "my-story", "done")
            .await;
        assert!(result.is_ok());
        mock.assert_async().await;
    }

    #[tokio::test]
    async fn send_notification_uses_template_outside_window() {
        let mut server = mockito::Server::new_async().await;
        let mock = server
            .mock("POST", "/123456/messages")
            .match_body(mockito::Matcher::PartialJsonString(
                r#"{"type":"template"}"#.to_string(),
            ))
            .with_body(r#"{"messages": [{"id": "wamid.tpl2"}]}"#)
            .create_async()
            .await;

        let transport = WhatsAppTransport::with_api_base(
            "123456".to_string(),
            "test-token".to_string(),
            server.url(),
        );
        // No record_message call — user is outside the window.
        let tracker = MessagingWindowTracker::new();

        let result = transport
            .send_notification("15551234567", &tracker, "my-story", "done")
            .await;
        assert!(result.is_ok());
        mock.assert_async().await;
    }

    #[tokio::test]
    async fn transport_send_message_calls_graph_api() {
        let mut server = mockito::Server::new_async().await;
        let mock = server
            .mock("POST", "/123456/messages")
            .match_header("authorization", "Bearer test-token")
            .with_body(r#"{"messages": [{"id": "wamid.abc123"}]}"#)
            .create_async()
            .await;

        let transport = WhatsAppTransport::with_api_base(
            "123456".to_string(),
            "test-token".to_string(),
            server.url(),
        );

        let result = transport
            .send_message("15551234567", "hello", "<p>hello</p>")
            .await;
        assert!(result.is_ok());
        assert_eq!(result.unwrap(), "wamid.abc123");
        mock.assert_async().await;
    }

    #[tokio::test]
    async fn transport_edit_sends_new_message() {
        let mut server = mockito::Server::new_async().await;
        let mock = server
            .mock("POST", "/123456/messages")
            .with_body(r#"{"messages": [{"id": "wamid.xyz"}]}"#)
            .create_async()
            .await;

        let transport = WhatsAppTransport::with_api_base(
            "123456".to_string(),
            "test-token".to_string(),
            server.url(),
        );

        let result = transport
            .edit_message("15551234567", "old-msg-id", "updated", "<p>updated</p>")
            .await;
        assert!(result.is_ok());
        mock.assert_async().await;
    }

    #[tokio::test]
    async fn transport_send_typing_succeeds() {
        let transport =
            WhatsAppTransport::new("123".to_string(), "tok".to_string(), "tpl".to_string());
        assert!(transport.send_typing("room1", true).await.is_ok());
        assert!(transport.send_typing("room1", false).await.is_ok());
    }

    #[tokio::test]
    async fn transport_handles_api_error() {
        let mut server = mockito::Server::new_async().await;
        server
            .mock("POST", "/123456/messages")
            .with_status(401)
            .with_body(r#"{"error": {"message": "Invalid token"}}"#)
            .create_async()
            .await;

        let transport = WhatsAppTransport::with_api_base(
            "123456".to_string(),
            "bad-token".to_string(),
            server.url(),
        );

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