claudy 0.2.2

Modern multi-provider launcher for Claude CLI
use anyhow::{Context, Result};
use serde::Deserialize;

use super::buttons::to_inline_keyboard;
use super::normalize::TelegramUpdate;
use crate::domain::channel_events::InteractionButtons;

#[derive(Debug, Deserialize)]
struct TelegramResponse {
    ok: bool,
    result: Option<serde_json::Value>,
    description: Option<String>,
}

#[derive(Debug, Deserialize)]
pub struct TelegramMessage {
    pub message_id: i64,
}

pub struct TelegramApi {
    client: reqwest::Client,
    pub(crate) bot_token: String,
}

impl TelegramApi {
    pub fn new(bot_token: String) -> Self {
        Self {
            client: reqwest::Client::new(),
            bot_token,
        }
    }

    fn url(&self, method: &str) -> String {
        format!("https://api.telegram.org/bot{}/{method}", self.bot_token)
    }

    pub async fn send_message(
        &self,
        chat_id: &str,
        text: &str,
        interaction: Option<&InteractionButtons>,
    ) -> Result<TelegramMessage> {
        let mut body = serde_json::json!({
            "chat_id": chat_id,
            "text": text,
        });

        if let Some(buttons) = interaction {
            body["reply_markup"] = to_inline_keyboard(buttons);
        }

        let resp: TelegramResponse = self
            .client
            .post(self.url("sendMessage"))
            .json(&body)
            .send()
            .await
            .context("telegram sendMessage request failed")?
            .json()
            .await
            .context("telegram sendMessage response parse failed")?;

        if !resp.ok {
            let desc = resp
                .description
                .unwrap_or_else(|| "unknown error".to_string());
            anyhow::bail!("telegram sendMessage failed: {desc}");
        }

        let result = resp.result.context("telegram sendMessage missing result")?;
        let message: TelegramMessage =
            serde_json::from_value(result).context("telegram sendMessage result parse failed")?;

        Ok(message)
    }

    pub async fn edit_message_text(
        &self,
        chat_id: &str,
        message_id: i64,
        text: &str,
        interaction: Option<&InteractionButtons>,
    ) -> Result<()> {
        let mut body = serde_json::json!({
            "chat_id": chat_id,
            "message_id": message_id,
            "text": text,
        });

        match interaction {
            Some(buttons) => {
                body["reply_markup"] = to_inline_keyboard(buttons);
            }
            None => {
                body["reply_markup"] = serde_json::json!({"inline_keyboard": []});
            }
        }

        let resp: TelegramResponse = self
            .client
            .post(self.url("editMessageText"))
            .json(&body)
            .send()
            .await
            .context("telegram editMessageText request failed")?
            .json()
            .await
            .context("telegram editMessageText response parse failed")?;

        if !resp.ok {
            let desc = resp
                .description
                .unwrap_or_else(|| "unknown error".to_string());
            anyhow::bail!("telegram editMessageText failed: {desc}");
        }

        Ok(())
    }

    pub async fn delete_message(&self, chat_id: &str, message_id: i64) -> Result<()> {
        let body = serde_json::json!({
            "chat_id": chat_id,
            "message_id": message_id,
        });

        let resp: TelegramResponse = self
            .client
            .post(self.url("deleteMessage"))
            .json(&body)
            .send()
            .await
            .context("telegram deleteMessage request failed")?
            .json()
            .await
            .context("telegram deleteMessage response parse failed")?;

        if !resp.ok {
            let desc = resp
                .description
                .unwrap_or_else(|| "unknown error".to_string());
            anyhow::bail!("telegram deleteMessage failed: {desc}");
        }

        Ok(())
    }

    pub async fn answer_callback_query(&self, callback_query_id: &str) -> Result<()> {
        let body = serde_json::json!({
            "callback_query_id": callback_query_id,
        });

        let resp: TelegramResponse = self
            .client
            .post(self.url("answerCallbackQuery"))
            .json(&body)
            .send()
            .await
            .context("telegram answerCallbackQuery request failed")?
            .json()
            .await
            .context("telegram answerCallbackQuery response parse failed")?;

        if !resp.ok {
            let desc = resp
                .description
                .unwrap_or_else(|| "unknown error".to_string());
            anyhow::bail!("telegram answerCallbackQuery failed: {desc}");
        }

        Ok(())
    }

    pub async fn send_chat_action(&self, chat_id: &str, action: &str) -> Result<()> {
        let body = serde_json::json!({
            "chat_id": chat_id,
            "action": action,
        });

        let resp: TelegramResponse = self
            .client
            .post(self.url("sendChatAction"))
            .json(&body)
            .send()
            .await
            .context("telegram sendChatAction request failed")?
            .json()
            .await
            .context("telegram sendChatAction response parse failed")?;

        if !resp.ok {
            let desc = resp
                .description
                .unwrap_or_else(|| "unknown error".to_string());
            anyhow::bail!("telegram sendChatAction failed: {desc}");
        }

        Ok(())
    }

    pub async fn get_updates(
        &self,
        offset: Option<i64>,
        timeout: u32,
    ) -> Result<Vec<TelegramUpdate>> {
        let mut body = serde_json::json!({
            "timeout": timeout,
            "allowed_updates": ["message", "callback_query"],
        });
        if let Some(o) = offset {
            body["offset"] = serde_json::json!(o);
        }

        let resp: TelegramResponse = self
            .client
            .post(self.url("getUpdates"))
            .json(&body)
            .send()
            .await
            .context("telegram getUpdates request failed")?
            .json()
            .await
            .context("telegram getUpdates response parse failed")?;

        if !resp.ok {
            let desc = resp
                .description
                .unwrap_or_else(|| "unknown error".to_string());
            anyhow::bail!("telegram getUpdates failed: {desc}");
        }

        let result = resp.result.unwrap_or(serde_json::Value::Null);
        let updates: Vec<TelegramUpdate> =
            serde_json::from_value(result).context("telegram getUpdates result parse failed")?;

        Ok(updates)
    }

    pub async fn set_my_commands(&self, commands: &[(&str, &str)]) -> Result<()> {
        let cmds: Vec<serde_json::Value> = commands
            .iter()
            .map(|(cmd, desc)| serde_json::json!({"command": cmd, "description": desc}))
            .collect();
        let body = serde_json::json!({"commands": cmds});

        let resp: TelegramResponse = self
            .client
            .post(self.url("setMyCommands"))
            .json(&body)
            .send()
            .await
            .context("telegram setMyCommands request failed")?
            .json()
            .await
            .context("telegram setMyCommands response parse failed")?;

        if !resp.ok {
            let desc = resp
                .description
                .unwrap_or_else(|| "unknown error".to_string());
            anyhow::bail!("telegram setMyCommands failed: {desc}");
        }

        Ok(())
    }
}

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

    #[test]
    fn new_creates_api_with_token() {
        let api = TelegramApi::new("123456:ABC-DEF".to_string());
        assert_eq!(api.bot_token, "123456:ABC-DEF");
    }

    #[test]
    fn url_formats_correctly() {
        let api = TelegramApi::new("123456:ABC-DEF".to_string());
        assert_eq!(
            api.url("sendMessage"),
            "https://api.telegram.org/bot123456:ABC-DEF/sendMessage"
        );
        assert_eq!(
            api.url("getMe"),
            "https://api.telegram.org/bot123456:ABC-DEF/getMe"
        );
    }
}