use std::collections::HashMap;
use std::fmt;
use std::sync::atomic::{AtomicI64, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;

use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use futures_util::{SinkExt, StreamExt};
use serde_json::{json, Value};
use tokio::sync::{broadcast, mpsc, oneshot, watch};
use tokio::task::JoinHandle;
use tokio_tungstenite::tungstenite::client::IntoClientRequest;
use tokio_tungstenite::tungstenite::Message;
use tokio_tungstenite::{client_async_tls_with_config, Connector};
use url::Url;

use crate::compression::{decompress_payload, MAX_DECOMPRESSED_SIZE};
use crate::proxy::{connect, ProxyConfig};
use crate::transport::build_tls_config;

const EVENT_CAPACITY: usize = 256;

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CallParams {
    pub token: String,
    pub ws_endpoint: String,
    pub ws_ips: Vec<String>,
    pub wt_endpoint: Option<String>,
    pub calls_api_endpoint: Option<String>,
    pub client_type: Option<String>,
    pub expires_at: Option<i64>,
    pub stun: Option<String>,
    pub turn: Vec<String>,
    pub turn_user: Option<String>,
    pub turn_password: Option<String>,
    pub is_video: bool,
}

pub fn decode_vcp(source: &str) -> Option<CallParams> {
    let (length, encoded) = source.split_once(':')?;
    let raw_length = length.parse::<usize>().ok()?;
    if raw_length == 0 || raw_length > MAX_DECOMPRESSED_SIZE {
        return None;
    }
    let compressed = STANDARD.decode(encoded).ok()?;
    let decompressed = decompress_payload(&compressed).ok()?;
    if decompressed.len() < raw_length {
        return None;
    }
    let value: Value = serde_json::from_slice(&decompressed[..raw_length]).ok()?;
    let object = value.as_object()?;
    Some(CallParams {
        token: object.get("tkn")?.as_str()?.to_string(),
        ws_endpoint: object.get("wse")?.as_str()?.to_string(),
        ws_ips: string_list(object.get("wsip")),
        wt_endpoint: string_field(object.get("wte")),
        calls_api_endpoint: string_field(object.get("vcae")),
        client_type: string_field(object.get("srcp")),
        expires_at: object.get("et").and_then(Value::as_i64),
        stun: string_field(object.get("stne")),
        turn: object
            .get("trne")
            .and_then(Value::as_str)
            .map(|value| {
                value
                    .split(',')
                    .map(str::trim)
                    .filter(|item| !item.is_empty())
                    .map(str::to_string)
                    .collect()
            })
            .unwrap_or_default(),
        turn_user: string_field(object.get("trnu")),
        turn_password: string_field(object.get("trnp")),
        is_video: object.get("iv").and_then(Value::as_bool).unwrap_or(false),
    })
}

#[derive(Debug)]
pub enum CallError {
    Closed,
    Timeout,
    Invalid(String),
    Transport(String),
    Command(String),
}

impl fmt::Display for CallError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Closed => formatter.write_str("call signaling is closed"),
            Self::Timeout => formatter.write_str("call signaling timed out"),
            Self::Invalid(message) => write!(formatter, "invalid call signaling data: {message}"),
            Self::Transport(message) => {
                write!(formatter, "call signaling transport error: {message}")
            }
            Self::Command(message) => write!(formatter, "call signaling command failed: {message}"),
        }
    }
}

impl std::error::Error for CallError {}

pub struct CallSignaling {
    sequence: AtomicI64,
    outgoing: mpsc::UnboundedSender<String>,
    pending: Arc<Mutex<HashMap<i64, oneshot::Sender<Value>>>>,
    notifications: broadcast::Sender<Value>,
    connected: watch::Sender<bool>,
    tasks: Mutex<Vec<JoinHandle<()>>>,
}

impl CallSignaling {
    pub async fn connect(
        source: &str,
        user_agent: &str,
        proxy: Option<&ProxyConfig>,
        insecure_tls: bool,
    ) -> Result<Self, CallError> {
        let url = Url::parse(source).map_err(|error| CallError::Invalid(error.to_string()))?;
        if url.scheme() != "ws" && url.scheme() != "wss" {
            return Err(CallError::Invalid(
                "unsupported WebSocket scheme".to_string(),
            ));
        }
        let host = url
            .host_str()
            .ok_or_else(|| CallError::Invalid("host is missing".to_string()))?;
        let port = url
            .port_or_known_default()
            .ok_or_else(|| CallError::Invalid("port is missing".to_string()))?;
        let tcp = connect(host, port, proxy)
            .await
            .map_err(|error| CallError::Transport(error.to_string()))?;
        let mut request = source
            .into_client_request()
            .map_err(|error| CallError::Invalid(error.to_string()))?;
        request.headers_mut().insert(
            "User-Agent",
            user_agent
                .parse()
                .map_err(|_| CallError::Invalid("user agent".to_string()))?,
        );
        let connector = Connector::Rustls(
            build_tls_config(insecure_tls)
                .map_err(|error| CallError::Transport(error.to_string()))?,
        );
        let (socket, _) = client_async_tls_with_config(request, tcp, None, Some(connector))
            .await
            .map_err(|error| CallError::Transport(error.to_string()))?;
        let (mut writer, mut reader) = socket.split();
        let (outgoing, mut outgoing_rx) = mpsc::unbounded_channel::<String>();
        let (notifications, _) = broadcast::channel(EVENT_CAPACITY);
        let (connected, _) = watch::channel(true);
        let pending: Arc<Mutex<HashMap<i64, oneshot::Sender<Value>>>> =
            Arc::new(Mutex::new(HashMap::new()));

        let writer_connected = connected.clone();
        let writer_task = tokio::spawn(async move {
            while let Some(text) = outgoing_rx.recv().await {
                if writer.send(Message::Text(text.into())).await.is_err() {
                    break;
                }
            }
            writer_connected.send_replace(false);
        });

        let reader_pending = pending.clone();
        let reader_notifications = notifications.clone();
        let reader_outgoing = outgoing.clone();
        let reader_connected = connected.clone();
        let reader_task = tokio::spawn(async move {
            while let Some(message) = reader.next().await {
                let text = match message {
                    Ok(Message::Text(text)) => text.to_string(),
                    Ok(Message::Binary(bytes)) => String::from_utf8_lossy(&bytes).into_owned(),
                    Ok(Message::Close(_)) | Err(_) => break,
                    _ => continue,
                };
                route_message(
                    &text,
                    &reader_pending,
                    &reader_notifications,
                    &reader_outgoing,
                );
            }
            reader_connected.send_replace(false);
            reader_pending.lock().unwrap().clear();
        });

        Ok(Self {
            sequence: AtomicI64::new(0),
            outgoing,
            pending,
            notifications,
            connected,
            tasks: Mutex::new(vec![writer_task, reader_task]),
        })
    }

    pub async fn send_command(&self, command: &str, extra: Value) -> Result<Value, CallError> {
        if !self.is_connected() {
            return Err(CallError::Closed);
        }
        let sequence = self.sequence.fetch_add(1, Ordering::Relaxed) + 1;
        let mut object = extra.as_object().cloned().unwrap_or_default();
        object.insert("command".to_string(), json!(command));
        object.insert("sequence".to_string(), json!(sequence));
        let (sender, receiver) = oneshot::channel();
        self.pending.lock().unwrap().insert(sequence, sender);
        self.outgoing
            .send(Value::Object(object).to_string())
            .map_err(|_| CallError::Closed)?;
        let response = match tokio::time::timeout(Duration::from_secs(15), receiver).await {
            Ok(Ok(value)) => value,
            Ok(Err(_)) => return Err(CallError::Closed),
            Err(_) => {
                self.pending.lock().unwrap().remove(&sequence);
                return Err(CallError::Timeout);
            }
        };
        if response.get("type").and_then(Value::as_str) == Some("error")
            || response.get("error").is_some()
        {
            return Err(CallError::Command(response.to_string()));
        }
        Ok(response)
    }

    pub fn subscribe(&self) -> broadcast::Receiver<Value> {
        self.notifications.subscribe()
    }

    pub fn is_connected(&self) -> bool {
        *self.connected.borrow()
    }

    pub fn close(&self) {
        self.connected.send_replace(false);
        self.pending.lock().unwrap().clear();
        for task in self.tasks.lock().unwrap().drain(..) {
            task.abort();
        }
    }
}

impl Drop for CallSignaling {
    fn drop(&mut self) {
        self.close();
    }
}

fn route_message(
    source: &str,
    pending: &Arc<Mutex<HashMap<i64, oneshot::Sender<Value>>>>,
    notifications: &broadcast::Sender<Value>,
    outgoing: &mpsc::UnboundedSender<String>,
) {
    if source == "ping" {
        let _ = outgoing.send("pong".to_string());
        return;
    }
    let Ok(value) = serde_json::from_str::<Value>(source) else {
        return;
    };
    let kind = value.get("type").and_then(Value::as_str);
    if kind == Some("response") || kind == Some("error") {
        if let Some(sequence) = value.get("sequence").and_then(Value::as_i64) {
            if let Some(sender) = pending.lock().unwrap().remove(&sequence) {
                let _ = sender.send(value.clone());
            }
        }
        if kind == Some("error") {
            let _ = notifications.send(value);
        }
        return;
    }
    if kind == Some("notification") || value.get("notification").is_some() {
        let _ = notifications.send(value);
    }
}

fn string_field(value: Option<&Value>) -> Option<String> {
    value.and_then(Value::as_str).map(str::to_string)
}

fn string_list(value: Option<&Value>) -> Vec<String> {
    value
        .and_then(Value::as_array)
        .map(|items| {
            items
                .iter()
                .filter_map(Value::as_str)
                .map(str::to_string)
                .collect()
        })
        .unwrap_or_default()
}

#[cfg(test)]
mod tests {
    use std::collections::HashMap;
    use std::sync::{Arc, Mutex};
    use std::time::Duration;

    use base64::engine::general_purpose::STANDARD;
    use base64::Engine;
    use futures_util::{SinkExt, StreamExt};
    use serde_json::json;
    use tokio::sync::{broadcast, mpsc, oneshot};
    use tokio_tungstenite::tungstenite::Message;

    use super::{decode_vcp, route_message, CallSignaling};
    use crate::compression::compress_lz4_block;

    #[test]
    fn decodes_synthetic_vcp() {
        let json = br#"{"tkn":"synthetic-token","wse":"wss://example.invalid/ws2","trne":"turn:a.invalid,turn:b.invalid","trnu":"test:42","iv":true}"#;
        let source = format!(
            "{}:{}",
            json.len(),
            STANDARD.encode(compress_lz4_block(json))
        );
        let params = decode_vcp(&source).unwrap();
        assert_eq!(params.token, "synthetic-token");
        assert_eq!(params.turn.len(), 2);
        assert!(params.is_video);
    }

    #[test]
    fn answers_text_keepalive() {
        let pending = Arc::new(Mutex::new(HashMap::new()));
        let (notifications, _) = broadcast::channel(4);
        let (outgoing, mut receiver) = mpsc::unbounded_channel();
        route_message("ping", &pending, &notifications, &outgoing);
        assert_eq!(receiver.try_recv().unwrap(), "pong");
    }

    #[test]
    fn correlates_synthetic_response() {
        let pending = Arc::new(Mutex::new(HashMap::new()));
        let (sender, mut receiver) = oneshot::channel();
        pending.lock().unwrap().insert(7, sender);
        let (notifications, _) = broadcast::channel(4);
        let (outgoing, _) = mpsc::unbounded_channel();
        route_message(
            &json!({"type":"response","sequence":7,"response":"test"}).to_string(),
            &pending,
            &notifications,
            &outgoing,
        );
        assert_eq!(receiver.try_recv().unwrap()["response"], "test");
    }

    #[test]
    fn forwards_synthetic_notification() {
        let pending = Arc::new(Mutex::new(HashMap::new()));
        let (notifications, mut receiver) = broadcast::channel(4);
        let (outgoing, _) = mpsc::unbounded_channel();
        route_message(
            &json!({"type":"notification","notification":"connection"}).to_string(),
            &pending,
            &notifications,
            &outgoing,
        );
        assert_eq!(receiver.try_recv().unwrap()["notification"], "connection");
    }

    #[tokio::test]
    async fn websocket_exchanges_command_notification_and_keepalive() {
        let listener = tokio::net::TcpListener::bind(("127.0.0.1", 0))
            .await
            .unwrap();
        let address = listener.local_addr().unwrap();
        let server = tokio::spawn(async move {
            let (tcp, _) = listener.accept().await.unwrap();
            let mut socket = tokio_tungstenite::accept_async(tcp).await.unwrap();
            let command = socket.next().await.unwrap().unwrap().into_text().unwrap();
            let value: serde_json::Value = serde_json::from_str(&command).unwrap();
            assert_eq!(value["command"], "synthetic-command");
            let sequence = value["sequence"].as_i64().unwrap();
            socket
                .send(Message::Text(
                    json!({"type":"response","sequence":sequence,"response":"synthetic-command"})
                        .to_string()
                        .into(),
                ))
                .await
                .unwrap();
            socket
                .send(Message::Text(
                    json!({"type":"notification","notification":"connection"})
                        .to_string()
                        .into(),
                ))
                .await
                .unwrap();
            socket.send(Message::Text("ping".into())).await.unwrap();
            assert_eq!(
                socket.next().await.unwrap().unwrap().into_text().unwrap(),
                "pong"
            );
        });

        let signaling =
            CallSignaling::connect(&format!("ws://{address}/ws2"), "Synthetic/1.0", None, false)
                .await
                .unwrap();
        let mut notifications = signaling.subscribe();
        let response = signaling
            .send_command("synthetic-command", json!({"value":1}))
            .await
            .unwrap();
        assert_eq!(response["response"], "synthetic-command");
        let notification = tokio::time::timeout(Duration::from_secs(2), notifications.recv())
            .await
            .unwrap()
            .unwrap();
        assert_eq!(notification["notification"], "connection");
        server.await.unwrap();
        signaling.close();
    }
}
