// relay.rs — Módulo de relay internet para gr-client
//
// Mantiene una conexión WebSocket persistente a un servidor relay externo
// registrada con el ID de 8 dígitos configurado. Cuando gr-client se conecta
// al relay con ese ID, este módulo hace bridge entre el WS del relay y el
// servidor TCP local de servidor.rs, de forma transparente.
//
// La URL del relay se toma de la variable de entorno GRADIO_RELAY_URL.
// Si no está definida, se usa DEFAULT_RELAY_URL. Para autohospedar el relay,
// definir GRADIO_RELAY_URL=wss://mi-servidor.ejemplo/ws antes de lanzar.
//
// Flujo:
//   1. Se conecta a la URL configurada
//   2. Envía {"tipo":"register","id":"XXXXXXXX"}
//   3. Recibe {"tipo":"registered","id":"XXXXXXXX"}
//   4. Espera frames WS del relay (originados en gr-client)
//   5. Cada frame WS → línea JSON → TCP servidor.rs local
//      Cada línea TCP → frame WS → relay → gr-client
//   6. Si detecta un frame Auth, abre una conexión TCP fresca
//   7. Reconecta con backoff exponencial si la WS cae

use std::sync::Arc;
use std::time::Duration;

use anyhow::Result;
use futures_util::{SinkExt, StreamExt};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::TcpStream;
use tokio_tungstenite::{connect_async, tungstenite::Message};

use crate::servidor::ClienteConectado;

const DEFAULT_RELAY_URL: &str = "wss://relay.example.com/ws";

// Tiempo máximo para que un envío individual al relay complete. Cubre el caso
// de una conexión "zombi": el TCP sigue ESTAB a nivel de SO (sin RST/FIN, p.ej.
// tras un corte de red silencioso o cambio de IP pública del router) pero los
// bytes ya no llegan al otro lado. Sin este timeout, un solo .send() colgado
// para siempre bloqueaba el único punto de escritura compartido y el relay
// terminaba desregistrando el ID por falta de Pong — sin que este lado se
// enterara nunca, porque nada devolvía error.
const SEND_TIMEOUT: Duration = Duration::from_secs(15);

fn relay_url() -> String {
    std::env::var("GRADIO_RELAY_URL").unwrap_or_else(|_| DEFAULT_RELAY_URL.to_string())
}

/// Lanza el hilo de relay. No bloquea — retorna inmediatamente.
/// `id` — 8 dígitos numéricos, `puerto_local` — puerto del servidor TCP local.
pub fn iniciar(id: String, puerto_local: u16, cliente_conectado: ClienteConectado) {
    std::thread::Builder::new()
        .name("gradio-relay".to_string())
        .spawn(move || {
            let rt = tokio::runtime::Builder::new_current_thread()
                .enable_all()
                .build()
                .expect("relay: no se pudo crear runtime tokio");
            rt.block_on(bucle_relay(id, puerto_local, cliente_conectado));
        })
        .expect("relay: no se pudo crear hilo");
}

// ─── Bucle de reconexión ──────────────────────────────────────────────────────

async fn bucle_relay(id: String, puerto_local: u16, cliente_conectado: ClienteConectado) {
    loop {
        log::info!("relay: conectando con ID={}", id);
        match conectar_y_operar(&id, puerto_local, cliente_conectado.clone()).await {
            Ok(()) => {
                log::info!("relay: sesión cerrada, reconectando en 1s...");
                tokio::time::sleep(Duration::from_secs(1)).await;
            }
            Err(e) => {
                log::warn!("relay: {e}. Reintentando en 30s...");
                tokio::time::sleep(Duration::from_secs(30)).await;
            }
        }
    }
}

// ─── Conexión principal ───────────────────────────────────────────────────────

async fn conectar_y_operar(id: &str, puerto_local: u16, cliente_conectado: ClienteConectado) -> Result<()> {
    // connect_async maneja TLS automáticamente para wss:// (feature native-tls)
    let url = relay_url();
    log::info!("relay: usando URL {}", url);
    let (ws, _) = connect_async(url).await?;

    let (ws_sink, mut ws_rx) = ws.split();

    // ── Tarea de escritura única ──────────────────────────────────────────────
    // Todo envío al relay pasa por este canal en vez de golpear el sink
    // directamente desde varios sitios. Cada .send() real tiene SEND_TIMEOUT:
    // si se cuelga (conexión zombi), la tarea avisa por `murio_tx` y termina,
    // en lugar de quedar sosteniendo el punto de escritura para siempre.
    let (out_tx, mut out_rx) = tokio::sync::mpsc::unbounded_channel::<Message>();
    let (murio_tx, mut murio_rx) = tokio::sync::mpsc::unbounded_channel::<()>();
    tokio::spawn(async move {
        let mut ws_sink = ws_sink;
        while let Some(msg) = out_rx.recv().await {
            match tokio::time::timeout(SEND_TIMEOUT, ws_sink.send(msg)).await {
                Ok(Ok(())) => {}
                Ok(Err(e)) => {
                    log::warn!("relay: error enviando al relay: {e}");
                    let _ = murio_tx.send(());
                    break;
                }
                Err(_) => {
                    log::warn!("relay: envío al relay colgado >{}s (conexión zombi)", SEND_TIMEOUT.as_secs());
                    let _ = murio_tx.send(());
                    break;
                }
            }
        }
    });

    // ── Handshake de registro ─────────────────────────────────────────────────
    let msg = format!(r#"{{"tipo":"register","id":"{}"}}"#, id);
    out_tx.send(Message::Text(msg.into()))
        .map_err(|_| anyhow::anyhow!("relay: canal de escritura cerrado antes de registrar"))?;

    let confirmacion = tokio::time::timeout(SEND_TIMEOUT, ws_rx.next()).await
        .map_err(|_| anyhow::anyhow!("timeout esperando confirmación de registro"))?
        .ok_or_else(|| anyhow::anyhow!("relay cerró antes de confirmar registro"))??;

    match &confirmacion {
        Message::Text(t) => {
            let v: serde_json::Value = serde_json::from_str(t)
                .map_err(|_| anyhow::anyhow!("respuesta de registro no es JSON: {t}"))?;
            if v["tipo"].as_str() != Some("registered") {
                return Err(anyhow::anyhow!("respuesta inesperada: {t}"));
            }
            log::info!("relay: registrado con ID={id}");
        }
        _ => return Err(anyhow::anyhow!("respuesta de registro no es texto")),
    }

    // ── Canal TCP→WS ──────────────────────────────────────────────────────────
    let (tcp_to_ws_tx, mut tcp_to_ws_rx) =
        tokio::sync::mpsc::unbounded_channel::<String>();

    let out_tx_fwd = out_tx.clone();
    tokio::spawn(async move {
        while let Some(linea) = tcp_to_ws_rx.recv().await {
            let texto = linea.trim_end_matches(['\n', '\r']).to_string();
            if texto.is_empty() { continue; }
            if out_tx_fwd.send(Message::Text(texto.into())).is_err() {
                break;
            }
        }
    });

    // ── Estado del bridge ─────────────────────────────────────────────────────
    let mut tcp_write: Option<tokio::net::tcp::OwnedWriteHalf> = None;
    // Epoch: incrementado cada vez que se abre un TCP nuevo.
    // Evita que el signal tcp_caido de una sesión anterior mate la actual.
    let epoch_actual = Arc::new(std::sync::atomic::AtomicU64::new(0));
    let (tcp_caido_tx, mut tcp_caido_rx) =
        tokio::sync::mpsc::unbounded_channel::<u64>(); // lleva el epoch

    // Timer de ping WS — detecta clientes caídos sin cerrar la conexión limpiamente
    let mut ping_interval = tokio::time::interval(Duration::from_secs(45));
    ping_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);

    // Timer de ping TCP local — evita el timeout de inactividad de servidor.rs (90s)
    // cuando el cliente Android está conectado pero sin interacción.
    let mut ping_tcp_interval = tokio::time::interval(Duration::from_secs(60));
    ping_tcp_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);

    // ── Bucle principal ───────────────────────────────────────────────────────
    loop {
        tokio::select! {
            // Ping periódico al relay para mantener la WS viva ante NAT/firewall
            _ = ping_interval.tick() => {
                if out_tx.send(Message::Ping(vec![].into())).is_err() {
                    return Ok(()); // canal de escritura cerrado → reconectar
                }
            }
            // La tarea de escritura murió (envío colgado o error real) → reconectar
            Some(()) = murio_rx.recv() => {
                return Err(anyhow::anyhow!(
                    "escritura al relay sin respuesta por más de {}s (conexión zombi)",
                    SEND_TIMEOUT.as_secs()
                ));
            }
            // Ping periódico al servidor TCP local — reinicia el timer de
            // inactividad de servidor.rs (90s) cuando el cliente está idle.
            _ = ping_tcp_interval.tick() => {
                if let Some(ref mut tw) = tcp_write {
                    if tw.write_all(b"{\"tipo\":\"Ping\"}\n").await.is_err() {
                        tcp_write = None;
                    }
                }
            }
            // TCP al servidor local se cerró — si el epoch coincide, reconectar WS para
            // garantizar estado limpio en el relay antes del próximo cliente.
            Some(ep) = tcp_caido_rx.recv() => {
                if ep == epoch_actual.load(std::sync::atomic::Ordering::Relaxed) {
                    log::info!("relay: cliente desconectado (ep={}), reconectando WS...", ep);
                    cliente_conectado.lock().unwrap().clear();
                    return Ok(()); // fuerza re-registro limpio con el relay
                } else {
                    log::debug!("relay: tcp_caido obsoleto ignorado (ep={} vs actual={})", ep,
                        epoch_actual.load(std::sync::atomic::Ordering::Relaxed));
                }
            }

            // Frame WS del relay (viene de gr-client)
            resultado = ws_rx.next() => {
                let frame = match resultado {
                    Some(Ok(f))  => f,
                    Some(Err(e)) => return Err(e.into()),
                    None         => return Ok(()),   // relay cerró
                };

                match frame {
                    Message::Close(_) => return Ok(()),
                    Message::Ping(d)  => {
                        let _ = out_tx.send(Message::Pong(d));
                    }
                    Message::Text(texto) => {
                        // Filtrar mensajes de control del relay (no son del cliente Android)
                        let tipo_frame = serde_json::from_str::<serde_json::Value>(&texto)
                            .ok()
                            .and_then(|v| v["tipo"].as_str().map(|t| t.to_string()))
                            .unwrap_or_default();
                        match tipo_frame.as_str() {
                            "connected" | "disconnected" | "registered" => {
                                log::debug!("relay: mensaje de control ignorado: {tipo_frame}");
                                continue;
                            }
                            "error" => {
                                log::warn!("relay: error del relay: {texto}");
                                return Ok(()); // reconectar
                            }
                            _ => {}
                        }

                        // Si el frame es Auth → nuevo cliente → abrir TCP fresco
                        let es_auth = tipo_frame == "Auth";

                        if es_auth {
                            // Incrementar epoch antes de reemplazar la conexión
                            let nuevo_ep = epoch_actual.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1;
                            log::debug!("relay: nuevo cliente (Auth) — TCP fresco (ep={})", nuevo_ep);
                            tcp_write = None;  // cierra write half anterior → envía EOF a servidor.rs
                            *cliente_conectado.lock().unwrap() = "Internet".to_string();
                        }

                        // Si no hay TCP activo, abrir uno al servidor local
                        if tcp_write.is_none() {
                            match TcpStream::connect(
                                format!("127.0.0.1:{puerto_local}")
                            ).await {
                                Ok(tcp) => {
                                    let ep = epoch_actual.load(std::sync::atomic::Ordering::Relaxed);
                                    let (rh, wh) = tcp.into_split();
                                    tcp_write = Some(wh);
                                    let tx_d = tcp_to_ws_tx.clone();
                                    let tx_c = tcp_caido_tx.clone();
                                    tokio::spawn(async move {
                                        let mut reader = BufReader::new(rh);
                                        let mut linea  = String::new();
                                        loop {
                                            linea.clear();
                                            match reader.read_line(&mut linea).await {
                                                Ok(0) | Err(_) => break,
                                                Ok(_) => {
                                                    if tx_d.send(linea.clone()).is_err() { break; }
                                                }
                                            }
                                        }
                                        let _ = tx_c.send(ep); // envía el epoch de esta sesión
                                    });
                                    log::debug!("relay: TCP a 127.0.0.1:{puerto_local} abierto (ep={})", ep);
                                }
                                Err(e) => {
                                    log::warn!("relay: no se pudo conectar a servidor local: {e}");
                                    continue;
                                }
                            }
                        }

                        // Reenviar al servidor local
                        if let Some(ref mut tw) = tcp_write {
                            let linea = format!("{texto}\n");
                            if tw.write_all(linea.as_bytes()).await.is_err() {
                                tcp_write = None;
                            }
                        }
                    }
                    _ => {} // Binary, Pong — ignorar
                }
            }
        }
    }
}
