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

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

pub struct DiscordChannel {
    name: String,
    config: DiscordConfig,
    client: Client,
}

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

/// Builds the Discord webhook JSON payload from a message.
pub(crate) fn build_payload(
    msg: &Message,
    username_override: Option<&str>,
    avatar_url: Option<&str>,
) -> Value {
    let fields: Vec<Value> = msg
        .metadata
        .iter()
        .map(|(k, v)| {
            json!({
                "name": k,
                "value": v,
                "inline": true
            })
        })
        .collect();

    let mut payload = json!({
        "embeds": [{
            "title": msg.title,
            "description": msg.body,
            "color": msg.level.discord_color(),
            "fields": fields,
            "footer": { "text": "vapora" }
        }]
    });

    if let Some(u) = username_override {
        payload["username"] = json!(u);
    }
    if let Some(av) = avatar_url {
        payload["avatar_url"] = json!(av);
    }

    payload
}

#[async_trait]
impl NotificationChannel for DiscordChannel {
    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.username.as_deref(),
            self.config.avatar_url.as_deref(),
        );

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

        // Discord returns 204 No Content on success.
        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 error_message_uses_red_discord_color() {
        let msg = Message::error("Service down", "Health check failed");
        let payload = build_payload(&msg, None, None);
        assert_eq!(
            payload["embeds"][0]["color"].as_u64().unwrap(),
            0xcc0000_u64
        );
    }

    #[test]
    fn success_message_uses_green_discord_color() {
        let msg = Message::success("Deploy complete", "v1.2.0");
        let payload = build_payload(&msg, None, None);
        assert_eq!(
            payload["embeds"][0]["color"].as_u64().unwrap(),
            0x36a64f_u64
        );
    }

    #[test]
    fn metadata_maps_to_inline_fields() {
        let msg = Message::info("Test", "Body").with_metadata("region", "eu-west-1");
        let payload = build_payload(&msg, None, None);
        let fields = payload["embeds"][0]["fields"].as_array().unwrap();
        assert_eq!(fields.len(), 1);
        assert_eq!(fields[0]["inline"], json!(true));
    }

    #[test]
    fn username_and_avatar_appear_at_top_level() {
        let msg = Message::info("Test", "Body");
        let payload = build_payload(
            &msg,
            Some("vapora-bot"),
            Some("https://example.com/avatar.png"),
        );
        assert_eq!(payload["username"], json!("vapora-bot"));
        assert_eq!(
            payload["avatar_url"],
            json!("https://example.com/avatar.png")
        );
    }
}
