//! DiscordTransport — ChatTransport implementation for the Discord Bot API.

use async_trait::async_trait;
use serde::Deserialize;

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

// ── Discord API base URL (overridable for tests) ──────────────────────

const DISCORD_API_BASE: &str = "https://discord.com/api/v10";

// ── DiscordTransport ──────────────────────────────────────────────────

/// Discord Bot API transport.
///
/// Sends messages via `POST {API_BASE}/channels/{channel_id}/messages`,
/// edits via `PATCH {API_BASE}/channels/{channel_id}/messages/{message_id}`,
/// and typing indicators via `POST {API_BASE}/channels/{channel_id}/typing`.
pub struct DiscordTransport {
    bot_token: String,
    client: reqwest::Client,
    /// Optional base URL override for tests.
    api_base: String,
}

impl DiscordTransport {
    /// Creates a new `DiscordTransport` authenticated with the given bot token.
    pub fn new(bot_token: String) -> Self {
        Self {
            bot_token,
            client: reqwest::Client::new(),
            api_base: DISCORD_API_BASE.to_string(),
        }
    }

    /// Creates a `DiscordTransport` pointed at a custom API base URL, for
    /// tests that mock the Discord API instead of hitting discord.com.
    #[cfg(test)]
    pub(crate) fn with_api_base(bot_token: String, api_base: String) -> Self {
        Self {
            bot_token,
            client: reqwest::Client::new(),
            api_base,
        }
    }

    /// Authorization header value: `Bot {token}`.
    fn auth_header(&self) -> String {
        format!("Bot {}", self.bot_token)
    }
}

// ── Discord API response types ────────────────────────────────────────

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

// ── ChatTransport implementation ──────────────────────────────────────

#[async_trait]
impl ChatTransport for DiscordTransport {
    async fn send_message(
        &self,
        channel_id: &str,
        plain: &str,
        _html: &str,
    ) -> Result<MessageId, String> {
        slog!("[discord] send_message to {channel_id}: {plain:.80}");
        let url = format!("{}/channels/{}/messages", self.api_base, channel_id);

        // Discord messages have a 2000-char limit. Truncate if needed.
        let content = if plain.len() > 2000 {
            format!("{}…", truncate_at_char_boundary(plain, 1999))
        } else {
            plain.to_string()
        };

        let payload = serde_json::json!({ "content": content });

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

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

        Ok(msg.id)
    }

    async fn edit_message(
        &self,
        channel_id: &str,
        original_message_id: &str,
        plain: &str,
        _html: &str,
    ) -> Result<(), String> {
        slog!("[discord] edit_message in {channel_id}: id={original_message_id}");
        let url = format!(
            "{}/channels/{}/messages/{}",
            self.api_base, channel_id, original_message_id
        );

        let content = if plain.len() > 2000 {
            format!("{}…", truncate_at_char_boundary(plain, 1999))
        } else {
            plain.to_string()
        };

        let payload = serde_json::json!({ "content": content });

        let resp = self
            .client
            .patch(&url)
            .header("Authorization", self.auth_header())
            .json(&payload)
            .send()
            .await
            .map_err(|e| format!("Discord edit 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!("Discord edit returned {status}: {resp_text}"));
        }

        Ok(())
    }

    async fn send_typing(&self, channel_id: &str, typing: bool) -> Result<(), String> {
        if !typing {
            // Discord doesn't have a "stop typing" API — it times out after ~10s.
            return Ok(());
        }
        let url = format!("{}/channels/{}/typing", self.api_base, channel_id);

        self.client
            .post(&url)
            .header("Authorization", self.auth_header())
            .send()
            .await
            .map_err(|e| format!("Discord typing request failed: {e}"))?;

        Ok(())
    }
}

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

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

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

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

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

    #[tokio::test]
    async fn transport_send_message_handles_api_error() {
        let mut server = mockito::Server::new_async().await;
        server
            .mock("POST", "/channels/bad/messages")
            .with_status(404)
            .with_body(r#"{"message": "Unknown Channel", "code": 10003}"#)
            .create_async()
            .await;

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

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

    #[tokio::test]
    async fn transport_edit_message_calls_patch() {
        let mut server = mockito::Server::new_async().await;
        let mock = server
            .mock("PATCH", "/channels/123456/messages/999888777")
            .match_header("authorization", "Bot test-token")
            .with_body(r#"{"id": "999888777"}"#)
            .create_async()
            .await;

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

        let result = transport
            .edit_message("123456", "999888777", "updated", "")
            .await;
        assert!(result.is_ok());
        mock.assert_async().await;
    }

    #[tokio::test]
    async fn transport_edit_message_handles_error() {
        let mut server = mockito::Server::new_async().await;
        server
            .mock("PATCH", "/channels/123456/messages/bad")
            .with_status(404)
            .with_body(r#"{"message": "Unknown Message", "code": 10008}"#)
            .create_async()
            .await;

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

        let result = transport.edit_message("123456", "bad", "updated", "").await;
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("404"));
    }

    #[tokio::test]
    async fn transport_send_typing_succeeds() {
        let mut server = mockito::Server::new_async().await;
        server
            .mock("POST", "/channels/123456/typing")
            .with_status(204)
            .create_async()
            .await;

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

        assert!(transport.send_typing("123456", true).await.is_ok());
    }

    #[tokio::test]
    async fn transport_send_typing_false_is_noop() {
        let transport = DiscordTransport::new("test-token".to_string());
        assert!(transport.send_typing("123456", false).await.is_ok());
    }

    #[tokio::test]
    async fn transport_handles_http_error() {
        let mut server = mockito::Server::new_async().await;
        server
            .mock("POST", "/channels/123456/messages")
            .with_status(500)
            .with_body("Internal Server Error")
            .create_async()
            .await;

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

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

    // ── ChatTransport trait satisfaction ─────────────────────────────────

    #[test]
    fn discord_transport_satisfies_trait() {
        fn assert_transport<T: ChatTransport>() {}
        assert_transport::<DiscordTransport>();

        let _: Arc<dyn ChatTransport> = Arc::new(DiscordTransport::new("test-token".to_string()));
    }
}
