// servidor.rs — Servidor TCP para gr-client
//
// Escucha en el puerto configurado (7777 por defecto) y atiende conexiones
// remotas de gr-client. Todas las operaciones de datos se hacen sobre los
// archivos locales de ~/.gradio, exactamente igual que los procesos locales.
//
// Relay futuro: se conectará a un servidor relay externo como puente para clientes remotos.

use std::fs;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::time::Duration;

use anyhow::Result;
use chrono::{Datelike, NaiveDate};
use serde::{Deserialize, Serialize};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::broadcast;

pub type ClienteConectado = Arc<Mutex<String>>;

// ─── Protocolo (igual que en gr-client/src/protocol.rs) ──────────────────────
// IMPORTANTE: este bloque debe mantenerse sincronizado con
//             radio-player-client/src/protocol.rs

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EntradaRemota {
    pub ruta: String,
    pub dias: String,
    pub inicio: String,
    pub fin: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrackRemoto {
    pub ruta: String,
    pub duracion: String,
    pub titulo: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct EstadoRemoto {
    pub reproduciendo: bool,
    pub pausado: bool,
    pub track_actual: String,
    pub titulo_actual: String,
    pub posicion_secs: f64,
    pub duracion_secs: f64,
    pub upvol: u8,
    pub downvol: u8,
    pub comerciales_activos: bool,
    pub eventos_activos: bool,
    pub num_comerciales: i32,
    pub num_eventos: i32,
    pub num_eventos_espera: i32,
    pub indice_actual: usize,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EntradaDir {
    pub nombre: String,
    pub ruta: String,
    pub es_dir: bool,
    pub es_audio: bool,
    pub tiene_hijos: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResultadoBusqueda {
    pub ruta: String,
    pub nombre: String,
    pub duracion: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BotonRemoto {
    pub indice: u8,
    pub etiqueta: String,
    pub ruta: String,
    pub color: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ItemTanda {
    pub ruta: String,
    pub nombre: String,
    pub duracion_secs: f64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TandaRemota {
    pub hora: u32,
    pub minuto: u32,
    pub items: Vec<ItemTanda>,
    pub duracion_total_secs: f64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "tipo")]
pub enum MsgCliente {
    Auth { token: String },
    Ping,
    ListarDir { ruta: String },
    ObtenerPautaje { tipo_p: String, hora: u8, minuto: u8 },
    GuardarPautaje { tipo_p: String, hora: u8, minuto: u8, entradas: Vec<EntradaRemota> },
    ObtenerParrilla { dia: u8, hora: u8 },
    GuardarParrilla { dia: u8, hora: u8, items: Vec<String> },
    CopiarHoraParrilla { dia_orig: u8, hora_orig: u8, dia_dest: u8, hora_dest: u8 },
    ObtenerBotonera,
    GuardarBotonera { botones: Vec<BotonRemoto> },
    ObtenerPlaylist,
    ObtenerEstado,
    ComandoPlayer { cmd: String },
    SetVolumen { upvol: u8 },
    ReproducirAhora { ruta: String },
    AgregarAlPlaylist { ruta: String, duracion: String, #[serde(default)] al_inicio: bool },
    EliminarDePlaylist { indice: usize },
    MoverEnPlaylist { indice: usize, arriba: bool },
    Buscar { consulta: String, ruta: Option<String> },
    ObtenerReportes { tipo_r: String, fecha: String },
    InfoAudio { ruta: String },
    ActualizarIndice,
    PlaylistPlayAhora { ruta: String, duracion: String },
    StopGeneral,
    VaciarComerciales,
    VaciarEventos,
    VaciarEventosEspera,
    ObtenerCola { tipo_cola: String },
    EliminarDeCola { tipo_cola: String, indice: usize },
    ObtenerProximasTandas,
    ReproducirTanda { hora: u32, minuto: u32, items: Vec<String> },
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "tipo")]
pub enum MsgServidor {
    AuthOk { version: String },
    Pong,
    Error { mensaje: String },
    DirectorioListado { ruta: String, entradas: Vec<EntradaDir> },
    PautajeObtenido { tipo_p: String, hora: u8, minuto: u8, entradas: Vec<EntradaRemota> },
    PautajeGuardado,
    ParrillaObtenida { dia: u8, hora: u8, items: Vec<String> },
    ParrillaGuardada,
    BotoneraObtenida { botones: Vec<BotonRemoto> },
    BotoneraGuardada,
    PlaylistObtenida { tracks: Vec<TrackRemoto> },
    EstadoObtenido { estado: EstadoRemoto },
    InfoAudioObtenida { ruta: String, duracion: String },
    ResultadosBusqueda { resultados: Vec<ResultadoBusqueda> },
    ReportesObtenidos { lineas: Vec<String> },
    PlaylistCambiada { tracks: Vec<TrackRemoto> },
    EstadoCambiado   { estado: EstadoRemoto },
    IndiceActualizado { ok: bool, mensaje: String },
    ColaObtenida { tipo_cola: String, items: Vec<String> },
    ProximasTandasObtenidas { tandas: Vec<TandaRemota> },
    Ok,
}

// ─── Servidor ────────────────────────────────────────────────────────────────

pub struct Servidor {
    puerto: u16,
    token: String,
    cliente_conectado: ClienteConectado,
}

impl Servidor {
    pub fn nuevo(puerto: u16, token: String, cliente_conectado: ClienteConectado) -> Self {
        Servidor { puerto, token, cliente_conectado }
    }

    pub async fn ejecutar(self) -> Result<()> {
        let addr = format!("0.0.0.0:{}", self.puerto);
        let listener = TcpListener::bind(&addr).await?;
        log::info!("gr-client servidor escuchando en {}", addr);

        let (tx_push, _) = broadcast::channel::<String>(256);
        let tx_push = Arc::new(tx_push);

        // Tarea vigilante: monitorea archivos locales y empuja cambios a clientes
        let tx_vigil = tx_push.clone();
        tokio::spawn(async move {
            vigilar_estado(tx_vigil).await;
        });

        loop {
            let (stream, peer_addr) = match listener.accept().await {
                Ok(s) => s,
                Err(e) => {
                    log::warn!("Error aceptando conexión: {}", e);
                    continue;
                }
            };
            log::info!("gr-client: cliente conectado desde {}", peer_addr);
            let rx_push = tx_push.subscribe();
            let token = self.token.clone();
            let cc = self.cliente_conectado.clone();
            tokio::spawn(async move {
                // Conexiones del relay (127.0.0.1) se identifican aparte en relay.rs
                let es_local = peer_addr.ip().is_loopback();
                if !es_local {
                    *cc.lock().unwrap() = peer_addr.ip().to_string();
                }
                if let Err(e) = manejar_cliente(stream, rx_push, token).await {
                    log::warn!("gr-client: error cliente {}: {}", peer_addr, e);
                }
                log::info!("gr-client: cliente {} desconectado", peer_addr);
                if !es_local {
                    cc.lock().unwrap().clear();
                }
            });
        }
    }
}

// Tiempo máximo de inactividad: si no llega ningún mensaje (ni Ping) en este
// plazo, el servidor cierra la conexión para liberar el slot.
const TIMEOUT_INACTIVIDAD: Duration = Duration::from_secs(90);

// ─── Manejo de cliente ────────────────────────────────────────────────────────

async fn manejar_cliente(
    stream: TcpStream,
    mut rx_push: broadcast::Receiver<String>,
    token_esperado: String,
) -> Result<()> {
    let (read_half, mut write_half) = stream.into_split();
    let mut reader = BufReader::new(read_half);
    let mut linea = String::new();

    // Canal interno para enviar respuestas al cliente desde el manejador
    let (tx_resp, mut rx_resp) = tokio::sync::mpsc::unbounded_channel::<String>();

    // Tarea escritora: envía respuestas + push
    let escritor = tokio::spawn(async move {
        loop {
            tokio::select! {
                Some(json) = rx_resp.recv() => {
                    if write_half.write_all(json.as_bytes()).await.is_err() { break; }
                }
                result = rx_push.recv() => {
                    match result {
                        Ok(push) => {
                            if write_half.write_all(push.as_bytes()).await.is_err() { break; }
                        }
                        // Lag en broadcast: el cliente es lento — ignorar mensajes perdidos
                        Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => {
                            log::debug!("gr-client: push lagged {} mensajes, ignorados", n);
                        }
                        Err(_) => break,
                    }
                }
            }
        }
    });

    // Autenticación (con timeout propio para no bloquear el accept)
    linea.clear();
    match tokio::time::timeout(Duration::from_secs(15), reader.read_line(&mut linea)).await {
        Err(_) | Ok(Ok(0)) | Ok(Err(_)) => {
            escritor.abort();
            return Ok(());
        }
        Ok(Ok(_)) => {}
    }
    match serde_json::from_str::<MsgCliente>(linea.trim()) {
        Ok(MsgCliente::Auth { token }) => {
            if !token_esperado.is_empty() && token != token_esperado {
                let resp = serde_json::to_string(&MsgServidor::Error {
                    mensaje: "Token incorrecto".to_string(),
                })? + "\n";
                let _ = tx_resp.send(resp);
                tokio::time::sleep(Duration::from_millis(500)).await;
                escritor.abort();
                return Ok(());
            }
            let resp = serde_json::to_string(&MsgServidor::AuthOk {
                version: env!("CARGO_PKG_VERSION").to_string(),
            })? + "\n";
            let _ = tx_resp.send(resp);
        }
        _ => {
            let resp = serde_json::to_string(&MsgServidor::Error {
                mensaje: "Se esperaba Auth como primer mensaje".to_string(),
            })? + "\n";
            let _ = tx_resp.send(resp);
            tokio::time::sleep(Duration::from_millis(500)).await;
            escritor.abort();
            return Ok(());
        }
    }

    // Bucle principal con timeout de inactividad
    loop {
        linea.clear();
        let resultado = tokio::time::timeout(
            TIMEOUT_INACTIVIDAD,
            reader.read_line(&mut linea),
        ).await;

        match resultado {
            // Timeout de inactividad — cerrar conexión
            Err(_) => {
                log::info!("gr-client: timeout de inactividad ({}s) — cerrando conexión",
                    TIMEOUT_INACTIVIDAD.as_secs());
                break;
            }
            // EOF o error de red
            Ok(Ok(0)) | Ok(Err(_)) => break,
            Ok(Ok(_)) => {
                let trimmed = linea.trim();
                if trimmed.is_empty() { continue; }
                match serde_json::from_str::<MsgCliente>(trimmed) {
                    Ok(MsgCliente::Ping) => {
                        // Responder Pong — reinicia el timer de inactividad implícitamente
                        if let Ok(json) = serde_json::to_string(&MsgServidor::Pong) {
                            let _ = tx_resp.send(json + "\n");
                        }
                    }
                    Ok(msg) => {
                        let respuestas = procesar_mensaje(msg).await;
                        for r in respuestas {
                            if let Ok(json) = serde_json::to_string(&r) {
                                let _ = tx_resp.send(json + "\n");
                            }
                        }
                    }
                    Err(e) => {
                        log::warn!("gr-client: mensaje inválido: {} — {:?}", trimmed, e);
                        let err = MsgServidor::Error { mensaje: format!("JSON inválido: {}", e) };
                        if let Ok(json) = serde_json::to_string(&err) {
                            let _ = tx_resp.send(json + "\n");
                        }
                    }
                }
            }
        }
    }
    escritor.abort();
    Ok(())
}

// ─── Procesamiento de mensajes ────────────────────────────────────────────────

async fn procesar_mensaje(msg: MsgCliente) -> Vec<MsgServidor> {
    match msg {
        MsgCliente::Auth { .. } => vec![MsgServidor::Error {
            mensaje: "Auth duplicado".to_string(),
        }],

        MsgCliente::Ping => vec![MsgServidor::Pong],

        MsgCliente::ListarDir { ruta } => vec![listar_directorio(&ruta)],

        MsgCliente::ObtenerPautaje { tipo_p, hora, minuto } => {
            let entradas = leer_pautaje(&tipo_p, hora, minuto);
            vec![MsgServidor::PautajeObtenido { tipo_p, hora, minuto, entradas }]
        }

        MsgCliente::GuardarPautaje { tipo_p, hora, minuto, entradas } => {
            escribir_pautaje(&tipo_p, hora, minuto, &entradas);
            vec![MsgServidor::PautajeGuardado]
        }

        MsgCliente::ObtenerParrilla { dia, hora } => {
            let items = leer_parrilla(dia, hora);
            vec![MsgServidor::ParrillaObtenida { dia, hora, items }]
        }

        MsgCliente::GuardarParrilla { dia, hora, items } => {
            escribir_parrilla(dia, hora, &items);
            vec![MsgServidor::ParrillaGuardada]
        }

        MsgCliente::CopiarHoraParrilla { dia_orig, hora_orig, dia_dest, hora_dest } => {
            let items = leer_parrilla(dia_orig, hora_orig);
            escribir_parrilla(dia_dest, hora_dest, &items);
            vec![MsgServidor::ParrillaGuardada]
        }

        MsgCliente::ObtenerBotonera => {
            let botones = leer_botonera();
            vec![MsgServidor::BotoneraObtenida { botones }]
        }

        MsgCliente::GuardarBotonera { botones } => {
            escribir_botonera(&botones);
            vec![MsgServidor::BotoneraGuardada]
        }

        MsgCliente::ObtenerPlaylist => {
            let tracks = leer_playlist();
            vec![MsgServidor::PlaylistObtenida { tracks }]
        }

        MsgCliente::ObtenerEstado => {
            let estado = leer_estado();
            vec![MsgServidor::EstadoObtenido { estado }]
        }

        MsgCliente::ComandoPlayer { cmd } => {
            ejecutar_comando_player(&cmd);
            vec![MsgServidor::Ok]
        }

        MsgCliente::SetVolumen { upvol } => {
            let path = gradio_tmp("upvol");
            let _ = fs::write(path, upvol.to_string());
            vec![MsgServidor::Ok]
        }

        MsgCliente::ReproducirAhora { ruta } => {
            if !grpautaje::audio_probe::is_playable(Path::new(&ruta)) {
                log::warn!("[srv] ReproducirAhora rechazado, audio inválido: {}", ruta);
                return vec![MsgServidor::Error { mensaje: "audio inválido o corrupto".into() }];
            }
            let path = gradio_tmp("botonera_now");
            let _ = fs::write(path, &ruta);
            vec![MsgServidor::Ok]
        }

        MsgCliente::PlaylistPlayAhora { ruta, duracion } => {
            if !grpautaje::audio_probe::is_playable(Path::new(&ruta)) {
                log::warn!("[srv] PlaylistPlayAhora rechazado, audio inválido: {}", ruta);
                return vec![MsgServidor::Error { mensaje: "audio inválido o corrupto".into() }];
            }
            let path = gradio_tmp("play_now");
            let content = format!("{}\t{}", ruta, duracion);
            let _ = fs::write(path, content);
            vec![MsgServidor::Ok]
        }

        MsgCliente::AgregarAlPlaylist { ruta, duracion, al_inicio } => {
            if !grpautaje::audio_probe::is_playable(Path::new(&ruta)) {
                log::warn!("[srv] AgregarAlPlaylist rechazado, audio inválido: {}", ruta);
                return vec![MsgServidor::Error { mensaje: "audio inválido o corrupto".into() }];
            }
            agregar_al_playlist(&ruta, &duracion, al_inicio);
            vec![MsgServidor::Ok]
        }

        MsgCliente::EliminarDePlaylist { indice } => {
            eliminar_de_playlist(indice);
            vec![MsgServidor::Ok]
        }

        MsgCliente::MoverEnPlaylist { indice, arriba } => {
            mover_en_playlist(indice, arriba);
            vec![MsgServidor::Ok]
        }

        MsgCliente::Buscar { consulta, ruta } => {
            let resultados = buscar_audio(&consulta, ruta.as_deref()).await;
            vec![MsgServidor::ResultadosBusqueda { resultados }]
        }

        MsgCliente::ObtenerReportes { tipo_r, fecha } => {
            let lineas = generar_reporte(&tipo_r, &fecha);
            vec![MsgServidor::ReportesObtenidos { lineas }]
        }

        MsgCliente::InfoAudio { ruta } => {
            let duracion = obtener_duracion_audio(&ruta).await;
            vec![MsgServidor::InfoAudioObtenida { ruta, duracion }]
        }
        MsgCliente::StopGeneral => {
            let tmp = gradio_home().join("tmp");
            let _ = fs::write(tmp.join("cmd_stop_general"), "1");
            vec![MsgServidor::Ok]
        }

        MsgCliente::VaciarComerciales => {
            let _ = fs::write(gradio_home().join("tmp").join("comercialeslist4"), "");
            let items = leer_cola("comerciales");
            vec![MsgServidor::ColaObtenida { tipo_cola: "comerciales".into(), items }]
        }

        MsgCliente::VaciarEventos => {
            let _ = fs::write(gradio_home().join("tmp").join("eventoslist"), "");
            let items = leer_cola("eventos");
            vec![MsgServidor::ColaObtenida { tipo_cola: "eventos".into(), items }]
        }

        MsgCliente::VaciarEventosEspera => {
            let _ = fs::write(gradio_home().join("tmp").join("eventos-esperalist"), "");
            let items = leer_cola("eventos_espera");
            vec![MsgServidor::ColaObtenida { tipo_cola: "eventos_espera".into(), items }]
        }

        MsgCliente::ObtenerCola { tipo_cola } => {
            let items = leer_cola(&tipo_cola);
            vec![MsgServidor::ColaObtenida { tipo_cola, items }]
        }

        MsgCliente::EliminarDeCola { tipo_cola, indice } => {
            eliminar_de_cola(&tipo_cola, indice);
            let items = leer_cola(&tipo_cola);
            vec![MsgServidor::ColaObtenida { tipo_cola, items }]
        }

        MsgCliente::ObtenerProximasTandas => {
            let tandas = obtener_proximas_tandas();
            vec![MsgServidor::ProximasTandasObtenidas { tandas }]
        }

        MsgCliente::ReproducirTanda { hora, minuto, items } => {
            reproducir_tanda(hora, minuto, &items);
            let cola = leer_cola("comerciales");
            vec![MsgServidor::ColaObtenida { tipo_cola: "comerciales".into(), items: cola }]
        }

        MsgCliente::ActualizarIndice => {
            // Usa una base de datos propia en ~/.gradio/data/locatedb
            // para no necesitar sudo. Indexa el HOME del usuario.
            let db_path = gradio_home().join("locatedb");
            let home    = dirs::home_dir().unwrap_or_else(|| PathBuf::from("/"));
            let resultado = tokio::process::Command::new("updatedb")
                .arg("--output").arg(&db_path)
                .arg("--localpaths").arg(&home)
                .output()
                .await;
            match resultado {
                Ok(out) if out.status.success() => {
                    log::info!("updatedb completado en {:?}", db_path);
                    vec![MsgServidor::IndiceActualizado { ok: true, mensaje: "Índice actualizado correctamente".to_string() }]
                }
                Ok(out) => {
                    let err = String::from_utf8_lossy(&out.stderr).trim().to_string();
                    let err = if err.is_empty() {
                        format!("exit code {}", out.status.code().unwrap_or(-1))
                    } else { err };
                    log::warn!("updatedb falló: {}", err);
                    vec![MsgServidor::IndiceActualizado { ok: false, mensaje: format!("Error updatedb: {}", err) }]
                }
                Err(e) => {
                    log::warn!("No se pudo ejecutar updatedb: {}", e);
                    vec![MsgServidor::IndiceActualizado { ok: false, mensaje: format!("No se pudo ejecutar updatedb: {}", e) }]
                }
            }
        }
    }
}

// ─── Vigilante de estado ──────────────────────────────────────────────────────

async fn vigilar_estado(tx: Arc<broadcast::Sender<String>>) {
    let playlist_path = gradio_tmp("playlist4");
    let estado_path   = gradio_tmp("estado.json");

    let mut ultima_playlist = std::time::SystemTime::UNIX_EPOCH;
    let mut ultimo_estado   = std::time::SystemTime::UNIX_EPOCH;

    loop {
        tokio::time::sleep(Duration::from_millis(500)).await;

        // Monitorear playlist4
        if let Ok(meta) = fs::metadata(&playlist_path) {
            if let Ok(modificado) = meta.modified() {
                if modificado > ultima_playlist {
                    ultima_playlist = modificado;
                    let tracks = leer_playlist();
                    if let Ok(json) = serde_json::to_string(&MsgServidor::PlaylistCambiada { tracks }) {
                        let _ = tx.send(json + "\n");
                    }
                }
            }
        }

        // Monitorear estado.json
        if let Ok(meta) = fs::metadata(&estado_path) {
            if let Ok(modificado) = meta.modified() {
                if modificado > ultimo_estado {
                    ultimo_estado = modificado;
                    let estado = leer_estado();
                    if let Ok(json) = serde_json::to_string(&MsgServidor::EstadoCambiado { estado }) {
                        let _ = tx.send(json + "\n");
                    }
                }
            }
        }
    }
}

// ─── Helpers de archivos ──────────────────────────────────────────────────────

fn gradio_home() -> PathBuf {
    dirs::home_dir()
        .unwrap_or_else(|| PathBuf::from("/root"))
        .join(".gradio/data")
}

fn gradio_tmp(nombre: &str) -> PathBuf {
    gradio_home().join("tmp").join(nombre)
}

fn leer_f64(path: &Path) -> f64 {
    fs::read_to_string(path)
        .ok()
        .and_then(|s| s.trim().parse::<f64>().ok())
        .unwrap_or(0.0)
}

// ─── Playlist ─────────────────────────────────────────────────────────────────

fn leer_playlist() -> Vec<TrackRemoto> {
    let path = gradio_tmp("playlist4");
    let content = fs::read_to_string(&path).unwrap_or_default();
    content
        .lines()
        .filter(|l| !l.trim().is_empty())
        .filter_map(|line| {
            let mut parts = line.splitn(2, '\t');
            let ruta = parts.next()?.trim().to_string();
            let duracion = parts.next().unwrap_or("").trim().to_string();
            let titulo = PathBuf::from(&ruta)
                .file_stem()
                .and_then(|s| s.to_str())
                .unwrap_or("")
                .to_string();
            Some(TrackRemoto { ruta, duracion, titulo })
        })
        .collect()
}

fn agregar_al_playlist(ruta: &str, duracion: &str, al_inicio: bool) {
    let path = gradio_tmp("playlist4");
    let linea = format!("{}\t{}\n", ruta, duracion);
    if al_inicio {
        let existente = fs::read_to_string(&path).unwrap_or_default();
        let _ = fs::write(&path, format!("{}{}", linea, existente));
    } else {
        use std::io::Write;
        if let Ok(mut f) = fs::OpenOptions::new().append(true).create(true).open(path) {
            let _ = f.write_all(linea.as_bytes());
        }
    }
}

fn eliminar_de_playlist(indice: usize) {
    let path = gradio_tmp("playlist4");
    let content = fs::read_to_string(&path).unwrap_or_default();
    let mut lineas: Vec<&str> = content.lines().collect();
    if indice < lineas.len() {
        lineas.remove(indice);
    }
    let nuevo = lineas.join("\n") + "\n";
    let _ = fs::write(path, nuevo);
}

fn mover_en_playlist(indice: usize, arriba: bool) {
    let path = gradio_tmp("playlist4");
    let content = fs::read_to_string(&path).unwrap_or_default();
    let mut lineas: Vec<String> = content.lines().map(|s| s.to_string()).collect();
    if arriba && indice > 0 {
        lineas.swap(indice, indice - 1);
    } else if !arriba && indice + 1 < lineas.len() {
        lineas.swap(indice, indice + 1);
    }
    let nuevo = lineas.join("\n") + "\n";
    let _ = fs::write(path, nuevo);
}

// ─── Estado del reproductor ───────────────────────────────────────────────────

fn leer_estado() -> EstadoRemoto {
    // estado.json es escrito por radio-player cada vez que cambia
    let path = gradio_tmp("estado.json");
    if let Ok(data) = fs::read_to_string(&path) {
        if let Ok(estado) = serde_json::from_str::<EstadoRemoto>(&data) {
            return estado;
        }
    }
    // Fallback: construir desde archivos individuales
    let upvol = leer_f64(&gradio_tmp("upvol")).clamp(0.0, 100.0) as u8;
    let downvol = leer_f64(&gradio_tmp("downvol")).clamp(0.0, 100.0) as u8;
    EstadoRemoto { upvol, downvol, ..Default::default() }
}

fn ejecutar_comando_player(cmd: &str) {
    // Los comandos se ejecutan escribiendo archivos de señal
    let base = gradio_tmp("");
    match cmd {
        "play"        => { let _ = fs::write(base.join("cmd_play"), "1"); }
        "pause"       => { let _ = fs::write(base.join("cmd_pause"), "1"); }
        "stop"        => { let _ = fs::write(base.join("cmd_stop"), "1"); }
        "stop_after"  => { let _ = fs::write(base.join("cmd_stop_after"), "1"); }
        "siguiente"   => { let _ = fs::write(base.join("cmd_siguiente"), "1"); }
        _ => {}
    }
}

// ─── Pautaje (.com) ───────────────────────────────────────────────────────────

fn directorio_pautaje(tipo_p: &str) -> PathBuf {
    let carpeta = match tipo_p {
        "eventos"        => "eventos",
        "eventos-espera" => "eventos-espera",
        _                => "comerciales",
    };
    gradio_home().join(carpeta)
}

fn ruta_pautaje(tipo_p: &str, hora: u8, minuto: u8) -> PathBuf {
    directorio_pautaje(tipo_p)
        .join(hora.to_string())
        .join(format!("{}.com", minuto))
}

fn leer_pautaje(tipo_p: &str, hora: u8, minuto: u8) -> Vec<EntradaRemota> {
    let path = ruta_pautaje(tipo_p, hora, minuto);
    let content = fs::read_to_string(&path).unwrap_or_default();
    content
        .lines()
        .filter(|l| !l.trim().is_empty() && !l.starts_with('#'))
        .filter_map(|line| {
            let parts: Vec<&str> = line.splitn(4, '|').collect();
            if parts.len() < 4 { return None; }
            Some(EntradaRemota {
                ruta: parts[0].trim().to_string(),
                dias: parts[1].trim().to_string(),
                inicio: parts[2].trim().to_string(),
                fin: parts[3].trim().trim_end_matches('\r').to_string(),
            })
        })
        .collect()
}

fn escribir_pautaje(tipo_p: &str, hora: u8, minuto: u8, entradas: &[EntradaRemota]) {
    let path = ruta_pautaje(tipo_p, hora, minuto);
    if let Some(padre) = path.parent() {
        let _ = fs::create_dir_all(padre);
    }
    let contenido: String = entradas
        .iter()
        .map(|e| format!("{}|{}|{}|{}\n", e.ruta, e.dias, e.inicio, e.fin))
        .collect();
    let _ = fs::write(path, contenido);
}

// ─── Parrilla (.mus) ──────────────────────────────────────────────────────────

fn ruta_parrilla(dia: u8, hora: u8) -> PathBuf {
    gradio_home()
        .join("parrilla")
        .join(dia.to_string())
        .join(format!("{}-{}.mus", hora, hora + 1))
}

fn leer_parrilla(dia: u8, hora: u8) -> Vec<String> {
    let path = ruta_parrilla(dia, hora);
    let content = fs::read_to_string(&path).unwrap_or_default();
    content
        .lines()
        .filter(|l| !l.trim().is_empty() && !l.starts_with('#'))
        .map(|l| l.trim_end_matches('\r').to_string())
        .collect()
}

fn escribir_parrilla(dia: u8, hora: u8, items: &[String]) {
    let path = ruta_parrilla(dia, hora);
    if let Some(padre) = path.parent() {
        let _ = fs::create_dir_all(padre);
    }
    let contenido: String = items.iter().map(|s| format!("{}\n", s)).collect();
    let _ = fs::write(path, contenido);
}

// ─── Botonera ─────────────────────────────────────────────────────────────────

fn ruta_botonera_remote() -> PathBuf {
    gradio_home().join("botonera-remote.json")
}

fn ruta_botonera_config() -> PathBuf {
    gradio_home().join("botonera").join("config.json")
}

fn leer_botonera() -> Vec<BotonRemoto> {
    // Preferir versión guardada desde el cliente remoto
    let path_remote = ruta_botonera_remote();
    if let Ok(data) = fs::read_to_string(&path_remote) {
        if let Ok(botones) = serde_json::from_str::<Vec<BotonRemoto>>(&data) {
            return botones;
        }
    }
    // Leer desde config.json (formato de tabs del player) y aplanar
    let path_config = ruta_botonera_config();
    if let Ok(data) = fs::read_to_string(&path_config) {
        if let Ok(v) = serde_json::from_str::<serde_json::Value>(&data) {
            let mut resultado: Vec<BotonRemoto> = Vec::new();
            if let Some(tabs) = v["tabs"].as_array() {
                for tab in tabs {
                    if let Some(botones) = tab["botones"].as_array() {
                        for boton in botones {
                            if boton.is_null() { continue; }
                            let ruta = boton["ruta"].as_str().unwrap_or("").to_string();
                            let nombre = boton["nombre"].as_str().unwrap_or("").to_string();
                            if ruta.is_empty() { continue; }
                            let indice = resultado.len() as u8;
                            resultado.push(BotonRemoto {
                                indice,
                                etiqueta: nombre,
                                ruta,
                                color: String::new(),
                            });
                        }
                    }
                }
            }
            return resultado;
        }
    }
    Vec::new()
}

fn escribir_botonera(botones: &[BotonRemoto]) {
    let path = ruta_botonera_remote();
    if let Ok(json) = serde_json::to_string_pretty(botones) {
        let _ = fs::write(path, json);
    }
}

// ─── Directorio ───────────────────────────────────────────────────────────────

fn listar_directorio(ruta: &str) -> MsgServidor {
    let path = Path::new(ruta);
    if !path.is_dir() {
        return MsgServidor::Error {
            mensaje: format!("No es un directorio: {}", ruta),
        };
    }

    let mut entradas: Vec<EntradaDir> = Vec::new();
    let Ok(dir) = fs::read_dir(path) else {
        return MsgServidor::Error {
            mensaje: format!("No se pudo leer: {}", ruta),
        };
    };

    let mut dirs: Vec<EntradaDir>  = Vec::new();
    let mut files: Vec<EntradaDir> = Vec::new();

    for entry in dir.flatten() {
        let nombre = entry.file_name().to_string_lossy().to_string();
        if nombre.starts_with('.') { continue; }
        let ruta_hijo = entry.path().to_string_lossy().to_string();
        let es_dir    = entry.file_type().map(|t| t.is_dir()).unwrap_or(false);
        let es_audio  = if !es_dir { es_archivo_audio(&nombre) } else { false };
        let tiene_hijos = if es_dir {
            fs::read_dir(&entry.path()).map(|mut d| d.next().is_some()).unwrap_or(false)
        } else { false };

        let e = EntradaDir { nombre, ruta: ruta_hijo, es_dir, es_audio, tiene_hijos };
        if es_dir { dirs.push(e); } else if es_audio { files.push(e); }
    }

    dirs.sort_by(|a, b| a.nombre.to_lowercase().cmp(&b.nombre.to_lowercase()));
    files.sort_by(|a, b| a.nombre.to_lowercase().cmp(&b.nombre.to_lowercase()));
    entradas.extend(dirs);
    entradas.extend(files);

    MsgServidor::DirectorioListado { ruta: ruta.to_string(), entradas }
}

fn es_archivo_audio(nombre: &str) -> bool {
    let lower = nombre.to_lowercase();
    matches!(
        lower.rsplit('.').next().unwrap_or(""),
        "mp3" | "wav" | "ogg" | "flac" | "aac" | "m4a" | "opus" | "wma" | "mp2" | "aiff"
    )
}

// ─── Búsqueda ─────────────────────────────────────────────────────────────────

async fn buscar_audio(consulta: &str, ruta_base: Option<&str>) -> Vec<ResultadoBusqueda> {
    // 1. `locate` (rápido) — usa el índice propio (~/.gradio/data/locatedb) si existe,
    //    si no el índice del sistema (plocate/mlocate), exactamente igual que
    //    search_with_locate() en gr_buscador.rs: sin acotar por carpetas, solo
    //    se descartan rutas que ya no existen en disco (índice desactualizado).
    if let Some(resultados) = buscar_con_locate(consulta, ruta_base).await {
        if !resultados.is_empty() {
            return resultados;
        }
    }

    // 2. Fallback si `locate` no está disponible: búsqueda recursiva en filesystem,
    //    acotada a las carpetas reales de la biblioteca (derivadas de la parrilla)
    //    para no recorrer TODO el home — backups viejos, carpetas de empaquetado
    //    con copias repetidas de los mismos audios, etc.
    let roots = if ruta_base.is_none() { busqueda_roots() } else { Vec::new() };
    let bases: Vec<PathBuf> = match ruta_base {
        Some(b) => vec![PathBuf::from(b)],
        None if !roots.is_empty() => roots,
        None => vec![dirs::home_dir().unwrap_or_else(|| PathBuf::from("/"))],
    };

    let consulta_lower = consulta.to_lowercase();
    let mut resultados = Vec::new();
    for base in &bases {
        buscar_recursivo(base, &consulta_lower, &mut resultados, 0);
    }
    let mut vistos: std::collections::HashSet<String> = std::collections::HashSet::new();
    resultados.retain(|r| vistos.insert(r.ruta.clone()));
    resultados.sort_by(|a, b| a.nombre.cmp(&b.nombre));
    resultados.truncate(500);
    resultados
}

/// Busca con el comando `locate` (índice propio si existe, si no el del sistema).
/// `None` si el binario `locate` no está disponible; `Some(vec![])` si no hubo hits.
async fn buscar_con_locate(consulta: &str, ruta_base: Option<&str>) -> Option<Vec<ResultadoBusqueda>> {
    tokio::process::Command::new("locate").arg("--version").output().await.ok()?;

    let db_propia = gradio_home().join("locatedb");
    let mut cmd = tokio::process::Command::new("locate");
    cmd.arg("-i").arg("--");
    if db_propia.exists() {
        cmd.arg("-d").arg(&db_propia);
    }
    cmd.arg(consulta);

    let out = cmd.output().await.ok()?;
    let texto = String::from_utf8_lossy(&out.stdout);
    let consulta_lower = consulta.to_lowercase();
    let mut vistos: std::collections::HashSet<String> = std::collections::HashSet::new();
    let mut resultados: Vec<ResultadoBusqueda> = texto
        .lines()
        .filter(|l| {
            let lower = l.to_lowercase();
            lower.contains(&consulta_lower) && es_archivo_audio(l)
        })
        .filter(|l| Path::new(l).exists()) // descarta entradas del índice ya obsoletas
        .filter(|l| ruta_base.map(|b| l.starts_with(b)).unwrap_or(true))
        .filter(|l| vistos.insert(l.to_string()))
        .map(|l| {
            let path = Path::new(l);
            ResultadoBusqueda {
                ruta: l.to_string(),
                nombre: path.file_name()
                    .map(|n| n.to_string_lossy().to_string())
                    .unwrap_or_default(),
                duracion: String::new(),
            }
        })
        .collect();
    resultados.sort_by(|a, b| a.nombre.cmp(&b.nombre));
    resultados.truncate(500);
    Some(resultados)
}

/// Carpetas reales de la biblioteca de audio, derivadas de las rutas usadas
/// en la parrilla (`~/.gradio/data/parrilla/*/*.mus`). Si no hay parrilla
/// configurada aún, usa carpetas típicas por defecto. Mismo criterio que
/// `search_roots_fallback()` en gr_buscador.rs — deben mantenerse alineados.
fn busqueda_roots() -> Vec<PathBuf> {
    let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("/"));
    let mut roots: Vec<PathBuf> = Vec::new();

    let parrilla_dir = gradio_home().join("parrilla");
    if parrilla_dir.exists() {
        collect_mus_dirs(&parrilla_dir, &mut roots);
    }

    if roots.is_empty() {
        for candidate in &["Musica", "Music", "G Radio", "001 radio"] {
            let p = home.join(candidate);
            if p.exists() { roots.push(p); }
        }
    }

    roots.sort();
    roots.dedup();
    roots
}

/// Extrae carpetas de origen referenciadas en archivos .mus de la parrilla.
fn collect_mus_dirs(dir: &Path, roots: &mut Vec<PathBuf>) {
    let Ok(entries) = fs::read_dir(dir) else { return };
    for entry in entries.flatten() {
        let path = entry.path();
        if path.is_dir() {
            collect_mus_dirs(&path, roots);
        } else if path.extension().and_then(|e| e.to_str()) == Some("mus") {
            if let Ok(content) = fs::read_to_string(&path) {
                for line in content.lines() {
                    let raw = line.trim()
                        .trim_end_matches('/')
                        .trim_end_matches('*')
                        .trim_end_matches('/');
                    let p = PathBuf::from(raw);
                    if p.is_dir() {
                        roots.push(p);
                    } else if let Some(parent) = p.parent() {
                        if parent.exists() {
                            roots.push(parent.to_path_buf());
                        }
                    }
                }
            }
        }
    }
}

fn buscar_recursivo(dir: &Path, consulta: &str, resultados: &mut Vec<ResultadoBusqueda>, depth: u32) {
    if depth > 8 { return; }
    let Ok(entries) = fs::read_dir(dir) else { return; };
    for entry in entries.flatten() {
        let nombre = entry.file_name().to_string_lossy().to_string();
        if nombre.starts_with('.') { continue; }
        let path = entry.path();
        if path.is_dir() {
            buscar_recursivo(&path, consulta, resultados, depth + 1);
        } else if es_archivo_audio(&nombre) && nombre.to_lowercase().contains(consulta) {
            resultados.push(ResultadoBusqueda {
                ruta: path.to_string_lossy().to_string(),
                nombre,
                duracion: String::new(), // se obtiene con InfoAudio si se necesita
            });
        }
    }
}

// ─── Duración de audio ────────────────────────────────────────────────────────

async fn obtener_duracion_audio(ruta: &str) -> String {
    let output = tokio::process::Command::new("ffprobe")
        .args([
            "-v", "error",
            "-show_entries", "format=duration",
            "-of", "default=noprint_wrappers=1:nokey=1",
            ruta,
        ])
        .output()
        .await;

    match output {
        Ok(o) if o.status.success() => {
            let secs: f64 = String::from_utf8_lossy(&o.stdout)
                .trim()
                .parse()
                .unwrap_or(0.0);
            let s = secs as u64;
            format!("{:02}:{:02}:{:05.3}", s / 3600, (s % 3600) / 60, secs % 60.0)
        }
        _ => String::new(),
    }
}

// ─── Reportes ─────────────────────────────────────────────────────────────────

fn generar_reporte(tipo_r: &str, fecha: &str) -> Vec<String> {
    match tipo_r {
        "playlist" => {
            leer_playlist()
                .into_iter()
                .enumerate()
                .map(|(i, t)| format!("{:3}. {} [{}]", i + 1, t.titulo, t.duracion))
                .collect()
        }
        "parrilla"    => generar_reporte_parrilla(fecha),
        "comerciales" => listar_pautaje_activo("comerciales", fecha),
        "eventos"     => listar_pautaje_activo("eventos", fecha),
        "emitidos"    => leer_log_emitidos(fecha),
        _ => vec![format!("Tipo de reporte desconocido: {}", tipo_r)],
    }
}

fn generar_reporte_parrilla(fecha: &str) -> Vec<String> {
    let dia = match NaiveDate::parse_from_str(fecha, "%Y%m%d") {
        Ok(d) => d.weekday().number_from_monday() as u8, // 1=Lun..7=Dom
        Err(_) => return vec![format!("Fecha inválida: {}", fecha)],
    };
    let nombre_dia = match dia {
        1 => "Lunes", 2 => "Martes", 3 => "Miércoles",
        4 => "Jueves", 5 => "Viernes", 6 => "Sábado", _ => "Domingo",
    };
    let mut lineas = Vec::new();
    lineas.push(format!("Parrilla — {} ({})", nombre_dia, fecha));
    let mut tiene_datos = false;
    for hora in 0u8..24 {
        let items = leer_parrilla(dia, hora);
        if !items.is_empty() {
            tiene_datos = true;
            lineas.push(format!("── {:02}:00–{:02}:00 ({} items)", hora, hora + 1, items.len()));
            for item in items {
                let etiqueta = if item.ends_with("/*") {
                    format!("  [carpeta] {}", item.trim_end_matches("/*"))
                } else {
                    format!("  {}", item.rsplit('/').next().unwrap_or(&item))
                };
                lineas.push(etiqueta);
            }
        }
    }
    if !tiene_datos {
        lineas.push(format!("Sin parrilla programada para el {}.", nombre_dia));
    }
    lineas
}

fn listar_pautaje_activo(tipo_p: &str, _fecha: &str) -> Vec<String> {
    let mut lineas = Vec::new();
    for hora in 0u8..24 {
        for minuto in [0u8, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55] {
            let entradas = leer_pautaje(tipo_p, hora, minuto);
            if !entradas.is_empty() {
                lineas.push(format!("── {:02}:{:02} ({} entradas)", hora, minuto, entradas.len()));
                for e in entradas {
                    lineas.push(format!("   {} | {} | {} - {}", e.ruta, e.dias, e.inicio, e.fin));
                }
            }
        }
    }
    if lineas.is_empty() {
        lineas.push(format!("No hay pautaje de {} programado.", tipo_p));
    }
    lineas
}

fn leer_log_emitidos(fecha: &str) -> Vec<String> {
    // Buscar log de emisión en ~/.gradio/logs/{fecha}.log
    let path = gradio_home().join("logs").join(format!("{}.log", fecha));
    if let Ok(content) = fs::read_to_string(&path) {
        return content.lines().map(|l| l.to_string()).collect();
    }
    vec![format!("No hay log para la fecha {}.", fecha)]
}

// ─── Inicialización desde main.rs ────────────────────────────────────────────

/// Escribe el estado actual del reproductor en estado.json.
/// Llamado por el timer de build_ui cada 2 segundos.
pub fn escribir_estado_json(estado: &EstadoRemoto) {
    let path = gradio_tmp("estado.json");
    if let Some(padre) = path.parent() {
        let _ = fs::create_dir_all(padre);
    }
    if let Ok(json) = serde_json::to_string(estado) {
        let _ = fs::write(path, json);
    }
}

// ─── Colas activas ────────────────────────────────────────────────────────────

fn ruta_cola(tipo: &str) -> PathBuf {
    let nombre = match tipo {
        "eventos"        => "eventoslist",
        "eventos_espera" => "eventos-esperalist",
        _                => "comercialeslist4",
    };
    gradio_home().join("tmp").join(nombre)
}

fn leer_cola(tipo: &str) -> Vec<String> {
    fs::read_to_string(ruta_cola(tipo))
        .unwrap_or_default()
        .lines()
        .filter(|l| !l.trim().is_empty())
        .map(|l| l.trim().to_string())
        .collect()
}

fn eliminar_de_cola(tipo: &str, indice: usize) {
    let path = ruta_cola(tipo);
    let content = fs::read_to_string(&path).unwrap_or_default();
    let mut lineas: Vec<&str> = content.lines().filter(|l| !l.trim().is_empty()).collect();
    if indice < lineas.len() {
        lineas.remove(indice);
    }
    let nuevo = if lineas.is_empty() {
        String::new()
    } else {
        lineas.join("\n") + "\n"
    };
    let _ = fs::write(path, nuevo);
}

// ─── Próximas tandas de comerciales (igual que el panel del vivo) ─────────────

/// Reutiliza el escaneo de horario de main.rs (mismo criterio: hasta 4 tandas
/// futuras con comerciales, respetando played_breaks, máscara de días y rango
/// de fechas) para exponerlo al cliente remoto.
fn obtener_proximas_tandas() -> Vec<TandaRemota> {
    let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("/"));
    crate::scan_upcoming_breaks(&home, 4)
        .into_iter()
        .map(|br| TandaRemota {
            hora: br.hour,
            minuto: br.minute,
            items: br.items.into_iter().map(|(ruta, nombre, duracion_secs, _file_line)| {
                ItemTanda { ruta, nombre, duracion_secs }
            }).collect(),
            duracion_total_secs: br.total_dur,
        })
        .collect()
}

/// Reproduce una tanda concreta: reemplaza la cola activa de comerciales con
/// los ítems recibidos (los mismos que se le mostraron al cliente), la marca
/// como reproducida en played_breaks para que el scheduler no la repita, y
/// dispara la señal IPC que el loop de radio-player usa para arrancarla
/// (idéntico al botón ▶ de "Próximas tandas" en el vivo).
fn reproducir_tanda(hora: u32, minuto: u32, items: &[String]) {
    let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("/"));
    let comlist = home.join(".gradio/data/tmp/comercialeslist4");
    if let Ok(mut f) = fs::OpenOptions::new().write(true).create(true).truncate(true).open(&comlist) {
        use std::io::Write;
        for path in items {
            let _ = writeln!(f, "{}", path);
        }
    }
    crate::mark_break_played(hora, minuto, &home);
    let _ = fs::write(home.join(".gradio/data/tmp/cmd_play_tanda"), "1");
}

/// Lanza el servidor en un hilo separado con su propio runtime tokio.
/// Se llama desde `main()` de radio-player antes de `app.run()`.
pub fn iniciar(puerto: u16, token: String, cliente_conectado: ClienteConectado) {
    std::thread::Builder::new()
        .name("gr-client-server".to_string())
        .spawn(move || {
            let rt = tokio::runtime::Builder::new_current_thread()
                .enable_all()
                .build()
                .expect("gr-client: no se pudo crear runtime tokio");
            rt.block_on(async move {
                let srv = Servidor::nuevo(puerto, token, cliente_conectado);
                if let Err(e) = srv.ejecutar().await {
                    log::error!("gr-client servidor: {}", e);
                }
            });
        })
        .expect("gr-client: no se pudo crear hilo servidor");
}
