use async_trait::async_trait;
use reqwest::Client;
use serde_json::{json, Value};
use tracing::instrument;

use crate::{
    channel::NotificationChannel,
    config::TelegramConfig,
    error::{ChannelError, Result},
    message::Message,
};

pub struct TelegramChannel {
    name: String,
    config: TelegramConfig,
    client: Client,
}

impl TelegramChannel {
    pub fn new(name: impl Into<String>, config: TelegramConfig, client: Client) -> Self {
        Self {
            name: name.into(),
            config,
            client,
        }
    }
}

/// Escapes the three characters that have meaning in Telegram HTML mode.
fn html_escape(s: &str) -> String {
    // Telegram HTML supports <b>, <i>, <code>, <pre>, <a>.
    // Only &, < and > need escaping.
    let mut out = String::with_capacity(s.len());
    for ch in s.chars() {
        match ch {
            '&' => out.push_str("&amp;"),
            '<' => out.push_str("&lt;"),
            '>' => out.push_str("&gt;"),
            other => out.push(other),
        }
    }
    out
}

/// Builds the Telegram sendMessage JSON body.
pub(crate) fn build_payload(msg: &Message, chat_id: &str) -> Value {
    let mut text = format!(
        "<b>{} {}</b>\n\n{}",
        msg.level.emoji(),
        html_escape(&msg.title),
        html_escape(&msg.body),
    );

    if !msg.metadata.is_empty() {
        text.push('\n');
        for (k, v) in &msg.metadata {
            text.push_str(&format!("\n<b>{}</b>: {}", html_escape(k), html_escape(v)));
        }
    }

    json!({
        "chat_id": chat_id,
        "text": text,
        "parse_mode": "HTML"
    })
}

#[async_trait]
impl NotificationChannel for TelegramChannel {
    fn name(&self) -> &str {
        &self.name
    }

    #[instrument(skip(self, msg), fields(channel = %self.name))]
    async fn send(&self, msg: &Message) -> Result<()> {
        let payload = build_payload(msg, &self.config.chat_id);

        let resp = self
            .client
            .post(self.config.api_url())
            .json(&payload)
            .send()
            .await
            .map_err(|e| ChannelError::HttpError {
                channel: self.name.clone(),
                source: e,
            })?;

        if !resp.status().is_success() {
            let status = resp.status().as_u16();
            let body = resp.text().await.unwrap_or_default();
            return Err(ChannelError::ApiError {
                channel: self.name.clone(),
                status,
                body,
            });
        }

        Ok(())
    }
}

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

    #[test]
    fn html_escape_handles_all_three_special_chars() {
        assert_eq!(html_escape("a < b & c > d"), "a &lt; b &amp; c &gt; d");
    }

    #[test]
    fn html_escape_is_noop_for_plain_text() {
        let plain = "Deploy complete: v1.2.0 to production";
        assert_eq!(html_escape(plain), plain);
    }

    #[test]
    fn payload_uses_html_parse_mode() {
        let msg = Message::info("Test", "Body");
        let payload = build_payload(&msg, "-100123456");
        assert_eq!(payload["parse_mode"], json!("HTML"));
        assert_eq!(payload["chat_id"], json!("-100123456"));
    }

    #[test]
    fn payload_contains_emoji_and_bold_title() {
        let msg = Message::error("Service down", "Health check failed");
        let payload = build_payload(&msg, "-100");
        let text = payload["text"].as_str().unwrap();
        assert!(text.contains("🔴"));
        assert!(text.contains("<b>"));
        assert!(text.contains("Service down"));
    }

    #[test]
    fn payload_escapes_html_in_title_and_body() {
        let msg = Message::warning("a < b", "x & y > z");
        let payload = build_payload(&msg, "-100");
        let text = payload["text"].as_str().unwrap();
        assert!(text.contains("a &lt; b"));
        assert!(text.contains("x &amp; y &gt; z"));
    }

    #[test]
    fn metadata_appended_as_bold_key_value_lines() {
        let msg = Message::info("Test", "Body").with_metadata("env", "prod");
        let payload = build_payload(&msg, "-100");
        let text = payload["text"].as_str().unwrap();
        assert!(text.contains("<b>env</b>: prod"));
    }

    #[test]
    fn api_url_default_base() {
        let cfg = TelegramConfig {
            bot_token: "123:ABC".to_string(),
            chat_id: "-100".to_string(),
            api_base: None,
        };
        assert_eq!(
            cfg.api_url(),
            "https://api.telegram.org/bot123:ABC/sendMessage"
        );
    }

    #[test]
    fn api_url_custom_base_for_testing() {
        let cfg = TelegramConfig {
            bot_token: "123:ABC".to_string(),
            chat_id: "-100".to_string(),
            api_base: Some("http://localhost:8080".to_string()),
        };
        assert_eq!(
            cfg.api_url(),
            "http://localhost:8080/bot123:ABC/sendMessage"
        );
    }
}
