//! Minimal Discord Gateway WebSocket client.
//!
//! Connects to the Discord Gateway, authenticates with a bot token, maintains
//! the heartbeat keepalive, and dispatches incoming `MESSAGE_CREATE` events to
//! the command handler.

use std::sync::Arc;

use futures::{SinkExt, StreamExt};
use serde::{Deserialize, Serialize};
use tokio_tungstenite::tungstenite::Message as WsMessage;

use crate::slog;

use super::commands::{self, DiscordContext};

// ── Gateway opcodes ─────────────────────────────────────────────────────

const OP_DISPATCH: u8 = 0;
const OP_HEARTBEAT: u8 = 1;
const OP_IDENTIFY: u8 = 2;
const OP_RECONNECT: u8 = 7;
const OP_INVALID_SESSION: u8 = 9;
const OP_HELLO: u8 = 10;
const OP_HEARTBEAT_ACK: u8 = 11;

// ── Gateway intents ─────────────────────────────────────────────────────

/// GUILDS | GUILD_MESSAGES | MESSAGE_CONTENT (privileged)
const GATEWAY_INTENTS: u64 = (1 << 0) | (1 << 9) | (1 << 15);

// ── Gateway payload types ───────────────────────────────────────────────

#[derive(Deserialize, Debug)]
struct GatewayPayload {
    op: u8,
    #[serde(default)]
    d: Option<serde_json::Value>,
    #[serde(default)]
    s: Option<u64>,
    #[serde(default)]
    t: Option<String>,
}

#[derive(Deserialize, Debug)]
struct HelloData {
    heartbeat_interval: u64,
}

#[derive(Deserialize, Debug)]
pub(super) struct MessageCreateData {
    #[serde(default)]
    pub channel_id: String,
    #[serde(default)]
    pub content: String,
    #[serde(default)]
    pub author: Option<MessageAuthor>,
    #[serde(default)]
    pub mentions: Vec<MentionUser>,
}

#[derive(Deserialize, Debug)]
pub(super) struct MessageAuthor {
    pub id: String,
    #[serde(default)]
    pub bot: Option<bool>,
}

#[derive(Deserialize, Debug)]
pub(super) struct MentionUser {
    pub id: String,
}

#[derive(Deserialize, Debug)]
struct ReadyData {
    user: ReadyUser,
}

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

// ── Identify payload ────────────────────────────────────────────────────

#[derive(Serialize)]
struct IdentifyPayload {
    op: u8,
    d: IdentifyData,
}

#[derive(Serialize)]
struct IdentifyData {
    token: String,
    intents: u64,
    properties: IdentifyProperties,
}

#[derive(Serialize)]
struct IdentifyProperties {
    os: &'static str,
    browser: &'static str,
    device: &'static str,
}

#[derive(Serialize)]
struct HeartbeatPayload {
    op: u8,
    d: Option<u64>,
}

// ── Gateway URL ─────────────────────────────────────────────────────────

const GATEWAY_URL: &str = "wss://gateway.discord.gg/?v=10&encoding=json";

// ── Public entry point ──────────────────────────────────────────────────

/// Spawn a background task that connects to the Discord Gateway, maintains
/// the heartbeat, and dispatches incoming messages.
pub fn spawn_gateway(ctx: Arc<DiscordContext>) {
    tokio::spawn(async move {
        loop {
            if let Err(e) = run_gateway(Arc::clone(&ctx)).await {
                slog!("[discord] Gateway connection ended: {e}");
            }
            // Wait before reconnecting.
            slog!("[discord] Reconnecting in 5 seconds…");
            tokio::time::sleep(std::time::Duration::from_secs(5)).await;
        }
    });
}

/// Connect to the Discord Gateway and process events until the connection
/// closes or an unrecoverable error occurs.
async fn run_gateway(ctx: Arc<DiscordContext>) -> Result<(), String> {
    slog!("[discord] Connecting to Discord Gateway…");

    let (ws_stream, _) = tokio_tungstenite::connect_async(GATEWAY_URL)
        .await
        .map_err(|e| format!("Gateway connect failed: {e}"))?;

    slog!("[discord] Gateway WebSocket connected");

    let (mut write, mut read) = ws_stream.split();

    // Wait for Hello (op 10) to get the heartbeat interval.
    let hello = read
        .next()
        .await
        .ok_or("Gateway closed before Hello")?
        .map_err(|e| format!("Gateway read error: {e}"))?;

    let hello_payload: GatewayPayload = parse_ws_message(&hello).ok_or("Failed to parse Hello")?;

    if hello_payload.op != OP_HELLO {
        return Err(format!(
            "Expected Hello (op 10), got op {}",
            hello_payload.op
        ));
    }

    let hello_data: HelloData =
        serde_json::from_value(hello_payload.d.ok_or("Hello missing data")?)
            .map_err(|e| format!("Failed to parse Hello data: {e}"))?;

    let heartbeat_interval = std::time::Duration::from_millis(hello_data.heartbeat_interval);
    slog!(
        "[discord] Heartbeat interval: {}ms",
        hello_data.heartbeat_interval
    );

    // Send Identify.
    let identify = IdentifyPayload {
        op: OP_IDENTIFY,
        d: IdentifyData {
            token: ctx.bot_token.clone(),
            intents: GATEWAY_INTENTS,
            properties: IdentifyProperties {
                os: "linux",
                browser: "huskies",
                device: "huskies",
            },
        },
    };

    let identify_json = serde_json::to_string(&identify)
        .map_err(|e| format!("Failed to serialise Identify: {e}"))?;
    write
        .send(WsMessage::Text(identify_json.into()))
        .await
        .map_err(|e| format!("Failed to send Identify: {e}"))?;

    slog!("[discord] Identify sent, waiting for Ready…");

    // Track sequence number for heartbeats.
    let sequence = Arc::new(std::sync::atomic::AtomicU64::new(0));
    let seq_for_heartbeat = Arc::clone(&sequence);

    // Share the write half between the heartbeat task and the main event loop.
    let write = Arc::new(tokio::sync::Mutex::new(write));
    let write_for_heartbeat = Arc::clone(&write);

    // Spawn heartbeat task.
    let heartbeat_handle = tokio::spawn(async move {
        // Jitter: wait a random fraction of the interval before the first beat.
        let jitter = heartbeat_interval.mul_f64(rand_fraction());
        tokio::time::sleep(jitter).await;

        loop {
            let seq = seq_for_heartbeat.load(std::sync::atomic::Ordering::Relaxed);
            let seq_val = if seq == 0 { None } else { Some(seq) };
            let hb = HeartbeatPayload {
                op: OP_HEARTBEAT,
                d: seq_val,
            };
            if let Ok(json) = serde_json::to_string(&hb) {
                let mut w = write_for_heartbeat.lock().await;
                if w.send(WsMessage::Text(json.into())).await.is_err() {
                    slog!("[discord] Heartbeat send failed, stopping heartbeat");
                    break;
                }
            }
            tokio::time::sleep(heartbeat_interval).await;
        }
    });

    // Track our own bot user ID (set from READY event).
    let mut bot_user_id: Option<String> = None;

    // Main event loop.
    while let Some(msg_result) = read.next().await {
        let msg = match msg_result {
            Ok(m) => m,
            Err(e) => {
                slog!("[discord] Gateway read error: {e}");
                break;
            }
        };

        let payload: GatewayPayload = match parse_ws_message(&msg) {
            Some(p) => p,
            None => continue,
        };

        // Update sequence number.
        if let Some(s) = payload.s {
            sequence.store(s, std::sync::atomic::Ordering::Relaxed);
        }

        match payload.op {
            OP_DISPATCH => {
                let event_name = payload.t.as_deref().unwrap_or("");
                match event_name {
                    "READY" => {
                        if let Some(d) = payload.d
                            && let Ok(ready) = serde_json::from_value::<ReadyData>(d)
                        {
                            bot_user_id = Some(ready.user.id.clone());
                            slog!("[discord] READY — bot user ID: {}", ready.user.id);
                        }
                    }
                    "MESSAGE_CREATE" => {
                        if let Some(d) = payload.d {
                            dispatch_message(Arc::clone(&ctx), d, bot_user_id.clone());
                        }
                    }
                    _ => {}
                }
            }
            OP_HEARTBEAT => {
                // Server requested an immediate heartbeat.
                let seq = sequence.load(std::sync::atomic::Ordering::Relaxed);
                let seq_val = if seq == 0 { None } else { Some(seq) };
                let hb = HeartbeatPayload {
                    op: OP_HEARTBEAT,
                    d: seq_val,
                };
                if let Ok(json) = serde_json::to_string(&hb) {
                    let mut w = write.lock().await;
                    let _ = w.send(WsMessage::Text(json.into())).await;
                }
            }
            OP_HEARTBEAT_ACK => {}
            OP_RECONNECT => {
                slog!("[discord] Gateway requested reconnect");
                break;
            }
            OP_INVALID_SESSION => {
                slog!("[discord] Invalid session — will reconnect");
                // Wait a bit before reconnecting for invalid sessions.
                tokio::time::sleep(std::time::Duration::from_secs(3)).await;
                break;
            }
            _ => {}
        }
    }

    heartbeat_handle.abort();
    slog!("[discord] Gateway event loop ended");
    Ok(())
}

// ── Helpers ─────────────────────────────────────────────────────────────

fn parse_ws_message(msg: &WsMessage) -> Option<GatewayPayload> {
    match msg {
        WsMessage::Text(text) => serde_json::from_str(text).ok(),
        _ => None,
    }
}

/// Filter and dispatch an incoming MESSAGE_CREATE event on a background task.
fn dispatch_message(
    ctx: Arc<DiscordContext>,
    data: serde_json::Value,
    bot_user_id: Option<String>,
) {
    tokio::spawn(async move {
        let msg: MessageCreateData = match serde_json::from_value(data) {
            Ok(m) => m,
            Err(e) => {
                slog!("[discord] Failed to parse MESSAGE_CREATE: {e}");
                return;
            }
        };

        let author = match msg.author.as_ref() {
            Some(a) => a,
            None => return,
        };

        // Ignore bot messages (including our own).
        if author.bot.unwrap_or(false) {
            return;
        }

        // Check if the channel is in our configured channel list.
        if !ctx.channel_ids.contains(&msg.channel_id) {
            return;
        }

        // Check allowed_users (if non-empty, only listed users can interact).
        if !ctx.allowed_users.is_empty() && !ctx.allowed_users.contains(&author.id) {
            return;
        }

        // Check if the bot was mentioned, or if we respond to all messages in
        // configured channels (ambient mode).
        let bot_mentioned = bot_user_id
            .as_ref()
            .is_some_and(|bid| msg.mentions.iter().any(|m| m.id == *bid));

        let in_ambient = ctx
            .services
            .ambient_rooms
            .lock()
            .unwrap()
            .contains(&msg.channel_id);

        if !bot_mentioned && !in_ambient {
            return;
        }

        // Strip the bot mention from the message content if present.
        let content = if let Some(bid) = bot_user_id.as_ref() {
            let mention_pat = format!("<@{bid}>");
            let mention_pat_nick = format!("<@!{bid}>");
            msg.content
                .replace(&mention_pat, "")
                .replace(&mention_pat_nick, "")
                .trim()
                .to_string()
        } else {
            msg.content.clone()
        };

        if content.is_empty() {
            return;
        }

        slog!(
            "[discord] Message from {} in {}: {content:.80}",
            author.id,
            msg.channel_id
        );

        commands::handle_incoming_message(&ctx, &msg.channel_id, &author.id, &content).await;
    });
}

/// Cheap pseudo-random fraction [0.0, 1.0) for heartbeat jitter.
fn rand_fraction() -> f64 {
    let nanos = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap_or_default()
        .subsec_nanos();
    (nanos as f64) / 1_000_000_000.0
}

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

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

    #[test]
    fn parse_hello_payload() {
        let json = r#"{"op": 10, "d": {"heartbeat_interval": 41250}}"#;
        let payload: GatewayPayload = serde_json::from_str(json).unwrap();
        assert_eq!(payload.op, OP_HELLO);
        let hello: HelloData = serde_json::from_value(payload.d.unwrap()).unwrap();
        assert_eq!(hello.heartbeat_interval, 41250);
    }

    #[test]
    fn parse_message_create() {
        let json = r#"{
            "channel_id": "123456",
            "content": "hello bot",
            "author": {"id": "789", "bot": false},
            "mentions": [{"id": "111"}]
        }"#;
        let msg: MessageCreateData = serde_json::from_str(json).unwrap();
        assert_eq!(msg.channel_id, "123456");
        assert_eq!(msg.content, "hello bot");
        assert_eq!(msg.author.as_ref().unwrap().id, "789");
        assert!(!msg.author.as_ref().unwrap().bot.unwrap_or(false));
        assert_eq!(msg.mentions.len(), 1);
        assert_eq!(msg.mentions[0].id, "111");
    }

    #[test]
    fn parse_ready_event() {
        let json = r#"{"user": {"id": "999888"}}"#;
        let ready: ReadyData = serde_json::from_str(json).unwrap();
        assert_eq!(ready.user.id, "999888");
    }

    #[test]
    fn identify_payload_serializes_correctly() {
        let identify = IdentifyPayload {
            op: OP_IDENTIFY,
            d: IdentifyData {
                token: "test-token".to_string(),
                intents: GATEWAY_INTENTS,
                properties: IdentifyProperties {
                    os: "linux",
                    browser: "huskies",
                    device: "huskies",
                },
            },
        };
        let json: serde_json::Value =
            serde_json::from_str(&serde_json::to_string(&identify).unwrap()).unwrap();
        assert_eq!(json["op"], 2);
        assert_eq!(json["d"]["token"], "test-token");
        assert_eq!(json["d"]["intents"], GATEWAY_INTENTS);
        assert_eq!(json["d"]["properties"]["browser"], "huskies");
    }

    #[test]
    fn heartbeat_payload_serializes_with_sequence() {
        let hb = HeartbeatPayload {
            op: OP_HEARTBEAT,
            d: Some(42),
        };
        let json: serde_json::Value =
            serde_json::from_str(&serde_json::to_string(&hb).unwrap()).unwrap();
        assert_eq!(json["op"], 1);
        assert_eq!(json["d"], 42);
    }

    #[test]
    fn heartbeat_payload_serializes_null_sequence() {
        let hb = HeartbeatPayload {
            op: OP_HEARTBEAT,
            d: None,
        };
        let json: serde_json::Value =
            serde_json::from_str(&serde_json::to_string(&hb).unwrap()).unwrap();
        assert_eq!(json["op"], 1);
        assert!(json["d"].is_null());
    }

    #[test]
    fn rand_fraction_in_range() {
        let f = rand_fraction();
        assert!((0.0..1.0).contains(&f));
    }

    #[test]
    fn gateway_intents_correct() {
        // GUILDS (1) | GUILD_MESSAGES (512) | MESSAGE_CONTENT (32768)
        assert_eq!(GATEWAY_INTENTS, 1 | 512 | 32768);
        assert_eq!(GATEWAY_INTENTS, 33281);
    }
}
