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

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

pub struct SlackChannel {
    name: String,
    config: SlackConfig,
    client: Client,
}

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

/// Builds the Slack webhook JSON payload from a message.
///
/// Extracted as a free function so payload shape can be unit-tested without
/// mocking HTTP.
pub(crate) fn build_payload(
    msg: &Message,
    channel_override: Option<&str>,
    username_override: Option<&str>,
) -> Value {
    let fields: Vec<Value> = msg
        .metadata
        .iter()
        .map(|(k, v)| {
            json!({
                "title": k,
                "value": v,
                "short": true
            })
        })
        .collect();

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

    if let Some(ch) = channel_override {
        payload["channel"] = json!(ch);
    }
    if let Some(u) = username_override {
        payload["username"] = json!(u);
    }

    payload
}

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

        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_color() {
        let msg = Message::error("Deploy failed", "Rollback triggered");
        let payload = build_payload(&msg, None, None);
        assert_eq!(
            payload["attachments"][0]["color"].as_str().unwrap(),
            "#cc0000"
        );
    }

    #[test]
    fn metadata_maps_to_fields_array() {
        let msg = Message::info("Test", "Body")
            .with_metadata("env", "production")
            .with_metadata("version", "1.2.0");
        let payload = build_payload(&msg, None, None);
        let fields = payload["attachments"][0]["fields"].as_array().unwrap();
        assert_eq!(fields.len(), 2);
    }

    #[test]
    fn channel_override_appears_at_top_level() {
        let msg = Message::info("Test", "Body");
        let payload = build_payload(&msg, Some("#alerts"), Some("vapora-bot"));
        assert_eq!(payload["channel"], json!("#alerts"));
        assert_eq!(payload["username"], json!("vapora-bot"));
    }

    #[test]
    fn no_overrides_leaves_keys_absent() {
        let msg = Message::info("Test", "Body");
        let payload = build_payload(&msg, None, None);
        assert!(payload.get("channel").is_none());
        assert!(payload.get("username").is_none());
    }
}
