//! WhatsApp Business API integration.
//!
//! Provides:
//! - [`WhatsAppTransport`] — a [`ChatTransport`] that sends messages via the
//!   Meta Graph API (`graph.facebook.com/v21.0/{phone_number_id}/messages`).
//! - [`MessagingWindowTracker`] — tracks the 24-hour messaging window per user.
//! - [`webhook_verify`] / [`webhook_receive`] — Poem handlers for the WhatsApp
//!   webhook (GET verification handshake + POST incoming messages).

/// Incoming message command dispatch (e.g. `!status`, `!help`).
pub mod commands;
/// WhatsApp message formatting helpers.
pub mod format;
/// Conversation history and 24-hour messaging-window tracking.
pub mod history;
/// Meta Graph API transport implementation.
pub mod meta;
/// Twilio REST API transport implementation.
pub mod twilio;
/// HMAC signature verification for Meta and Twilio webhooks.
pub mod verify;

pub use history::{MessagingWindowTracker, WhatsAppConversationHistory, load_whatsapp_history};
pub use meta::WhatsAppTransport;
pub use twilio::{TwilioWhatsAppTransport, extract_twilio_text_messages};

use serde::Deserialize;
use std::sync::Arc;

use crate::chat::ChatTransport;
use crate::services::Services;
use crate::slog;
use poem::{Request, Response, handler, http::StatusCode, web::Query};

// ── Webhook types (Meta → us) ───────────────────────────────────────────

/// Top-level webhook payload from Meta.
#[derive(Deserialize, Debug)]
pub struct WebhookPayload {
    #[serde(default)]
    pub entry: Vec<WebhookEntry>,
}

/// One entry in the top-level `entry` array of a Meta webhook payload.
#[derive(Deserialize, Debug)]
pub struct WebhookEntry {
    #[serde(default)]
    pub changes: Vec<WebhookChange>,
}

/// One change event within a [`WebhookEntry`].
#[derive(Deserialize, Debug)]
pub struct WebhookChange {
    pub value: Option<WebhookValue>,
}

/// The `value` object inside a [`WebhookChange`], containing messages and metadata.
#[derive(Deserialize, Debug)]
pub struct WebhookValue {
    #[serde(default)]
    pub messages: Vec<WebhookMessage>,
    #[allow(dead_code)] // Present in Meta webhook JSON, kept for deserialization
    pub metadata: Option<WebhookMetadata>,
}

/// Phone-number metadata attached to a [`WebhookValue`].
#[derive(Deserialize, Debug)]
pub struct WebhookMetadata {
    #[allow(dead_code)]
    pub phone_number_id: Option<String>,
}

/// A single inbound WhatsApp message from the Meta webhook.
#[derive(Deserialize, Debug)]
pub struct WebhookMessage {
    pub from: Option<String>,
    pub r#type: Option<String>,
    pub text: Option<WebhookText>,
}

/// The `text` body of a [`WebhookMessage`].
#[derive(Deserialize, Debug)]
pub struct WebhookText {
    pub body: Option<String>,
}

/// Extract text messages from a webhook payload.
///
/// Returns `(sender_phone, message_body)` pairs.
pub fn extract_text_messages(payload: &WebhookPayload) -> Vec<(String, String)> {
    let mut messages = Vec::new();
    for entry in &payload.entry {
        for change in &entry.changes {
            if let Some(value) = &change.value {
                for msg in &value.messages {
                    if msg.r#type.as_deref() == Some("text")
                        && let (Some(from), Some(text)) = (&msg.from, &msg.text)
                        && let Some(body) = &text.body
                    {
                        messages.push((from.clone(), body.clone()));
                    }
                }
            }
        }
    }
    messages
}

/// Query parameters for the webhook verification GET request.
#[derive(Deserialize)]
pub struct VerifyQuery {
    #[serde(rename = "hub.mode")]
    pub hub_mode: Option<String>,
    #[serde(rename = "hub.verify_token")]
    pub hub_verify_token: Option<String>,
    #[serde(rename = "hub.challenge")]
    pub hub_challenge: Option<String>,
}

/// Shared context for webhook handlers, injected via Poem's `Data` extractor.
pub struct WhatsAppWebhookContext {
    /// Shared services bundle (project root, agent pool, bot identity, permissions).
    pub services: Arc<Services>,
    pub verify_token: String,
    /// Active provider: `"meta"` (Meta Graph API) or `"twilio"` (Twilio REST API).
    pub provider: String,
    pub transport: Arc<dyn ChatTransport>,
    /// Per-sender conversation history for LLM passthrough.
    pub history: WhatsAppConversationHistory,
    /// Maximum number of conversation entries to keep per sender.
    pub history_size: usize,
    /// Tracks the 24-hour messaging window per user phone number.
    pub window_tracker: Arc<MessagingWindowTracker>,
    /// Phone numbers allowed to send messages to the bot.
    /// When empty, all numbers are allowed (backwards compatible).
    pub allowed_phones: Vec<String>,
    /// Meta app secret for `X-Hub-Signature-256` HMAC verification.
    /// When non-empty, every inbound POST is verified against this secret.
    /// When empty, signature verification is skipped.
    pub app_secret: String,
    /// Twilio Auth Token for `X-Twilio-Signature` HMAC-SHA1 verification.
    /// When non-empty, every inbound Twilio POST is verified.
    /// When empty, Twilio signature verification is skipped.
    pub twilio_auth_token: String,
}

/// GET /webhook/whatsapp — webhook verification.
///
/// For Meta: responds to the `hub.mode=subscribe` challenge handshake.
/// For Twilio: Twilio does not send GET verification; always returns 200 OK.
#[handler]
pub async fn webhook_verify(
    Query(q): Query<VerifyQuery>,
    ctx: poem::web::Data<&Arc<WhatsAppWebhookContext>>,
) -> Response {
    // Twilio does not use a GET challenge; just acknowledge.
    if ctx.provider == "twilio" {
        return Response::builder().status(StatusCode::OK).body("ok");
    }

    // Meta verification handshake.
    if q.hub_mode.as_deref() == Some("subscribe")
        && q.hub_verify_token.as_deref() == Some(&ctx.verify_token)
        && let Some(challenge) = q.hub_challenge
    {
        slog!("[whatsapp] Webhook verification succeeded");
        return Response::builder().status(StatusCode::OK).body(challenge);
    }
    slog!("[whatsapp] Webhook verification failed");
    Response::builder()
        .status(StatusCode::FORBIDDEN)
        .body("Verification failed")
}

/// POST /webhook/whatsapp — receive incoming messages.
///
/// Dispatches to the appropriate parser based on the configured provider:
/// - `"meta"`: parses Meta's JSON `WebhookPayload`.
/// - `"twilio"`: parses Twilio's `application/x-www-form-urlencoded` body.
///
/// Both providers expect a `200 OK` response, even on parse errors.
///
/// For the `"meta"` provider, the `X-Hub-Signature-256` header is verified
/// against the configured app secret (HMAC-SHA256).  For Twilio, the
/// `X-Twilio-Signature` header is verified against the auth token.
/// Requests with a missing or invalid signature are rejected with `403 Forbidden`.
#[handler]
pub async fn webhook_receive(
    req: &Request,
    body: poem::Body,
    ctx: poem::web::Data<&Arc<WhatsAppWebhookContext>>,
) -> Response {
    let bytes = match body.into_bytes().await {
        Ok(b) => b,
        Err(e) => {
            slog!("[whatsapp] Failed to read webhook body: {e}");
            return Response::builder()
                .status(StatusCode::BAD_REQUEST)
                .body("Bad request");
        }
    };

    // Verify HMAC-SHA256 signature for Meta webhooks when an app secret is configured.
    if ctx.provider != "twilio" && !ctx.app_secret.is_empty() {
        let signature = req.header("X-Hub-Signature-256").unwrap_or("");
        if signature.is_empty() {
            slog!("[whatsapp] Missing X-Hub-Signature-256 header; rejecting request");
            return Response::builder()
                .status(StatusCode::FORBIDDEN)
                .body("Missing signature");
        }
        if !verify::verify_meta_signature(&ctx.app_secret, &bytes, signature) {
            slog!("[whatsapp] X-Hub-Signature-256 verification failed; rejecting request");
            return Response::builder()
                .status(StatusCode::FORBIDDEN)
                .body("Invalid signature");
        }
    }

    // Verify HMAC-SHA1 X-Twilio-Signature for Twilio webhooks.
    if ctx.provider == "twilio" && !ctx.twilio_auth_token.is_empty() {
        let signature = req.header("X-Twilio-Signature").unwrap_or("");
        if signature.is_empty() {
            slog!("[whatsapp/twilio] Missing X-Twilio-Signature header; rejecting request");
            return Response::builder()
                .status(StatusCode::FORBIDDEN)
                .body("Missing X-Twilio-Signature");
        }
        let url = reconstruct_request_url(req);
        if !verify::verify_twilio_signature(&ctx.twilio_auth_token, &url, &bytes, signature) {
            slog!("[whatsapp/twilio] X-Twilio-Signature verification failed; rejecting request");
            return Response::builder()
                .status(StatusCode::FORBIDDEN)
                .body("Invalid signature");
        }
    }

    let messages = if ctx.provider == "twilio" {
        let msgs = extract_twilio_text_messages(&bytes);
        if msgs.is_empty() {
            slog!("[whatsapp/twilio] No text messages in webhook body; ignoring");
        }
        msgs
    } else {
        let payload: WebhookPayload = match serde_json::from_slice(&bytes) {
            Ok(p) => p,
            Err(e) => {
                slog!("[whatsapp] Failed to parse webhook payload: {e}");
                // Meta expects 200 even on parse errors to avoid retries.
                return Response::builder().status(StatusCode::OK).body("ok");
            }
        };
        let msgs = extract_text_messages(&payload);
        if msgs.is_empty() {
            // Status updates, read receipts, etc. — acknowledge silently.
            return Response::builder().status(StatusCode::OK).body("ok");
        }
        msgs
    };

    if messages.is_empty() {
        return Response::builder().status(StatusCode::OK).body("ok");
    }

    let ctx = Arc::clone(*ctx);
    tokio::spawn(async move {
        for (sender, text) in messages {
            slog!("[whatsapp] Message from {sender}: {text}");
            commands::handle_incoming_message(&ctx, &sender, &text).await;
        }
    });

    Response::builder().status(StatusCode::OK).body("ok")
}

/// Reconstruct the full public URL of a webhook request.
///
/// Uses `X-Forwarded-Proto` (or falls back to `"https"`) and `Host` to build
/// the scheme + authority, then appends the request path and any query string.
/// This matches the URL Twilio used when it signed the request.
fn reconstruct_request_url(req: &Request) -> String {
    let scheme = req.header("x-forwarded-proto").unwrap_or("https");
    let host = req.header("host").unwrap_or("");
    let uri = req.uri();
    let path = uri.path();
    match uri.query() {
        Some(q) => format!("{scheme}://{host}{path}?{q}"),
        None => format!("{scheme}://{host}{path}"),
    }
}

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

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

    // ── webhook_receive signature-verification handler tests ──────────

    use crate::chat::transport::whatsapp::history::MessagingWindowTracker;
    use poem::EndpointExt;
    use std::fmt::Write as FmtWrite;
    use std::sync::Arc;

    struct NullTransport;

    #[async_trait::async_trait]
    impl crate::chat::ChatTransport for NullTransport {
        async fn send_message(
            &self,
            _: &str,
            _: &str,
            _: &str,
        ) -> Result<crate::chat::MessageId, String> {
            Ok(String::new())
        }
        async fn edit_message(&self, _: &str, _: &str, _: &str, _: &str) -> Result<(), String> {
            Ok(())
        }
        async fn send_typing(&self, _: &str, _: bool) -> Result<(), String> {
            Ok(())
        }
    }

    fn make_ctx(
        provider: &str,
        app_secret: &str,
        twilio_auth_token: &str,
    ) -> Arc<WhatsAppWebhookContext> {
        let tmp = tempfile::tempdir().unwrap();
        let services =
            crate::services::Services::new_test(tmp.path().to_path_buf(), "Bot".to_string());
        Arc::new(WhatsAppWebhookContext {
            services,
            verify_token: "tok".to_string(),
            provider: provider.to_string(),
            transport: Arc::new(NullTransport),
            history: Arc::new(tokio::sync::Mutex::new(Default::default())),
            history_size: 20,
            window_tracker: Arc::new(MessagingWindowTracker::new()),
            allowed_phones: vec![],
            app_secret: app_secret.to_string(),
            twilio_auth_token: twilio_auth_token.to_string(),
        })
    }

    fn handler_app(
        provider: &str,
        app_secret: &str,
        twilio_auth_token: &str,
    ) -> impl poem::Endpoint {
        let ctx = make_ctx(provider, app_secret, twilio_auth_token);
        poem::Route::new()
            .at("/webhook", poem::post(webhook_receive))
            .data(ctx)
    }

    fn make_meta_sig(secret: &str, body: &[u8]) -> String {
        use hmac::{Hmac, KeyInit, Mac};
        use sha2::Sha256;
        let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes()).unwrap();
        mac.update(body);
        let result = mac.finalize().into_bytes();
        let mut hex = String::from("sha256=");
        for b in &result {
            write!(hex, "{b:02x}").unwrap();
        }
        hex
    }

    #[tokio::test]
    async fn meta_missing_signature_returns_403() {
        let app = handler_app("meta", "secret", "");
        let cli = poem::test::TestClient::new(app);
        let resp = cli
            .post("/webhook")
            .body(b"{\"entry\":[]}".to_vec())
            .send()
            .await;
        assert_eq!(resp.0.status(), poem::http::StatusCode::FORBIDDEN);
    }

    #[tokio::test]
    async fn meta_invalid_signature_returns_403() {
        let app = handler_app("meta", "secret", "");
        let cli = poem::test::TestClient::new(app);
        let resp = cli
            .post("/webhook")
            .header(
                "X-Hub-Signature-256",
                "sha256=0000000000000000000000000000000000000000000000000000000000000000",
            )
            .body(b"{\"entry\":[]}".to_vec())
            .send()
            .await;
        assert_eq!(resp.0.status(), poem::http::StatusCode::FORBIDDEN);
    }

    #[tokio::test]
    async fn meta_valid_signature_passes() {
        let body = b"{\"entry\":[]}";
        let sig = make_meta_sig("secret", body);
        let app = handler_app("meta", "secret", "");
        let cli = poem::test::TestClient::new(app);
        let resp = cli
            .post("/webhook")
            .header("X-Hub-Signature-256", sig)
            .body(body.to_vec())
            .send()
            .await;
        assert_eq!(resp.0.status(), poem::http::StatusCode::OK);
    }

    #[tokio::test]
    async fn meta_no_secret_configured_skips_verification() {
        // When app_secret is empty, signature checking is disabled (backwards-compat).
        let app = handler_app("meta", "", "");
        let cli = poem::test::TestClient::new(app);
        let resp = cli
            .post("/webhook")
            .body(b"{\"entry\":[]}".to_vec())
            .send()
            .await;
        assert_eq!(resp.0.status(), poem::http::StatusCode::OK);
    }

    #[tokio::test]
    async fn twilio_missing_signature_returns_403() {
        let app = handler_app("twilio", "", "twilio-token");
        let cli = poem::test::TestClient::new(app);
        let resp = cli
            .post("/webhook")
            .body(b"Body=hello&From=whatsapp%3A%2B15551234567".to_vec())
            .send()
            .await;
        assert_eq!(resp.0.status(), poem::http::StatusCode::FORBIDDEN);
    }

    #[tokio::test]
    async fn twilio_invalid_signature_returns_403() {
        let app = handler_app("twilio", "", "twilio-token");
        let cli = poem::test::TestClient::new(app);
        let resp = cli
            .post("/webhook")
            .header("X-Twilio-Signature", "invalidsig==")
            .body(b"Body=hello&From=whatsapp%3A%2B15551234567".to_vec())
            .send()
            .await;
        assert_eq!(resp.0.status(), poem::http::StatusCode::FORBIDDEN);
    }

    #[tokio::test]
    async fn twilio_no_auth_token_configured_skips_verification() {
        // When twilio_auth_token is empty, signature checking is disabled (backwards-compat).
        let app = handler_app("twilio", "", "");
        let cli = poem::test::TestClient::new(app);
        let resp = cli
            .post("/webhook")
            .body(b"Body=hello&From=whatsapp%3A%2B15551234567".to_vec())
            .send()
            .await;
        assert_eq!(resp.0.status(), poem::http::StatusCode::OK);
    }

    // ── Existing webhook / transport tests ────────────────────────────

    #[test]
    fn extract_text_messages_parses_valid_payload() {
        let json = r#"{
            "entry": [{
                "changes": [{
                    "value": {
                        "messages": [{
                            "from": "15551234567",
                            "type": "text",
                            "text": { "body": "help" }
                        }],
                        "metadata": { "phone_number_id": "123456" }
                    }
                }]
            }]
        }"#;
        let payload: WebhookPayload = serde_json::from_str(json).unwrap();
        let msgs = extract_text_messages(&payload);
        assert_eq!(msgs.len(), 1);
        assert_eq!(msgs[0].0, "15551234567");
        assert_eq!(msgs[0].1, "help");
    }

    #[test]
    fn extract_text_messages_ignores_non_text() {
        let json = r#"{
            "entry": [{
                "changes": [{
                    "value": {
                        "messages": [{
                            "from": "15551234567",
                            "type": "image",
                            "image": { "id": "img123" }
                        }],
                        "metadata": { "phone_number_id": "123456" }
                    }
                }]
            }]
        }"#;
        let payload: WebhookPayload = serde_json::from_str(json).unwrap();
        let msgs = extract_text_messages(&payload);
        assert!(msgs.is_empty());
    }

    #[test]
    fn extract_text_messages_handles_empty_payload() {
        let json = r#"{ "entry": [] }"#;
        let payload: WebhookPayload = serde_json::from_str(json).unwrap();
        let msgs = extract_text_messages(&payload);
        assert!(msgs.is_empty());
    }

    #[test]
    fn extract_text_messages_handles_multiple_messages() {
        let json = r#"{
            "entry": [{
                "changes": [{
                    "value": {
                        "messages": [
                            { "from": "111", "type": "text", "text": { "body": "status" } },
                            { "from": "222", "type": "text", "text": { "body": "help" } }
                        ],
                        "metadata": { "phone_number_id": "123456" }
                    }
                }]
            }]
        }"#;
        let payload: WebhookPayload = serde_json::from_str(json).unwrap();
        let msgs = extract_text_messages(&payload);
        assert_eq!(msgs.len(), 2);
        assert_eq!(msgs[0].1, "status");
        assert_eq!(msgs[1].1, "help");
    }
}
