// gr_buscador.rs — Buscador de audio para G Radio Player
//
// Busca recursivamente en las carpetas de música archivos que contengan
// el texto ingresado. Los resultados se muestran en una tabla con:
//   Tema Encontrado | Tiempo | Ruta
//
// Doble clic → agrega al final de playlist4
// Arrastre   → el operador ubica el archivo en la lista de main.rs

use gtk4 as gtk;
use gtk::prelude::*;
use gtk::{
    Application, ApplicationWindow, Box as GtkBox, Button, Entry,
    Label, ListBox, ListBoxRow, Orientation, ScrolledWindow,
    GestureClick, CssProvider, STYLE_PROVIDER_PRIORITY_APPLICATION,
};
use gdk4::ContentProvider;

const ICONO_BUSCADOR: &[u8] = include_bytes!("../assets/busqueda.png");
const ICONO_UP:        &[u8] = include_bytes!("../assets/up.png");
const ICONO_DOWN:      &[u8] = include_bytes!("../assets/down.png");
use glib;
use gstreamer;
use gstreamer::prelude::{
    ElementExt, ElementExtManual, GstBinExtManual, PadExt,
};
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::{Arc, Mutex};
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::thread;

use grpautaje::i18n::tr;
use grpautaje::skin;

const APP_ID: &str = "com.gradio.buscador";

fn is_audio(p: &Path) -> bool {
    p.extension()
        .and_then(|e| e.to_str())
        .map(|e| matches!(e.to_ascii_lowercase().as_str(),
            "mp3" | "wav" | "ogg" | "flac" | "m4a" | "mp4" | "mkv" | "avi"))
        .unwrap_or(false)
}

/// Busca archivos de audio que contengan `query` (case-insensitive).
///
/// Estrategia (en orden de preferencia):
/// 1. `locate -i <query>` — usa el índice del sistema, instantáneo.
/// 2. Si locate no está disponible, búsqueda recursiva desde las carpetas
///    de la parrilla y directorios habituales de música.
fn search_files(_roots: &[PathBuf], query: &str) -> Vec<PathBuf> {
    // Intentar locate primero
    if let Ok(results) = search_with_locate(query) {
        if !results.is_empty() {
            return results;
        }
    }
    // Fallback: búsqueda recursiva
    let roots = search_roots_fallback();
    let query_lower = query.to_lowercase();
    let mut results = Vec::new();
    for root in &roots {
        search_recursive(root, &query_lower, &mut results);
    }
    results.sort();
    results
}

/// Usa `locate -i <query>` y filtra solo archivos de audio existentes.
/// Prioriza la base de datos propia (~/.gradio/data/locatedb) si existe,
/// luego la del sistema como fallback.
fn search_with_locate(query: &str) -> Result<Vec<PathBuf>, ()> {
    let check = Command::new("locate").arg("--version").output();
    if check.is_err() { return Err(()); }

    // Base de datos propia (generada con "⟳ Índice", no requiere sudo)
    let db_propia = dirs::home_dir()
        .unwrap_or_else(|| std::path::PathBuf::from("/"))
        .join(".gradio/data/locatedb");

    let mut cmd = Command::new("locate");
    cmd.arg("-i").arg("--");
    if db_propia.exists() {
        cmd.arg("-d").arg(&db_propia);
    }
    cmd.arg(query);

    let out = cmd.output().map_err(|_| ())?;

    let stdout = String::from_utf8_lossy(&out.stdout);
    let results: Vec<PathBuf> = stdout
        .lines()
        .map(|l| PathBuf::from(l.trim()))
        .filter(|p| p.exists() && is_audio(p))
        .collect();

    Ok(results)
}

/// Carpetas de búsqueda para el fallback recursivo.
/// Lee las rutas de los archivos .mus de la parrilla; si no hay, usa defaults.
fn search_roots_fallback() -> Vec<PathBuf> {
    let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("/"));
    let mut roots: Vec<PathBuf> = Vec::new();

    let parrilla_dir = home.join(".gradio/data/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
}

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 search_recursive(dir: &Path, query: &str, results: &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() {
            search_recursive(&path, query, results);
        } else if is_audio(&path) {
            let name = path.file_stem()
                .and_then(|s| s.to_str())
                .unwrap_or("")
                .to_lowercase();
            if name.contains(query) {
                results.push(path);
            }
        }
    }
}

/// Obtiene la duración de un archivo con ffprobe
fn get_duration(path: &Path) -> String {
    let out = Command::new("ffprobe")
        .args(["-v", "error",
               "-show_entries", "format=duration",
               "-of", "default=noprint_wrappers=1:nokey=1",
               path.to_str().unwrap_or("")])
        .output();
    match out {
        Ok(o) => {
            let secs = String::from_utf8_lossy(&o.stdout)
                .trim()
                .parse::<f64>()
                .unwrap_or(0.0);
            let total = secs as u64;
            format!("{:02}:{:02}:{:02}", total / 3600, (total % 3600) / 60, total % 60)
        }
        Err(_) => "00:00:00".to_string(),
    }
}

/// Escribe path + duración al archivo play_now para reproducción inmediata con crossfade.
fn write_play_now(path: &Path, duration: &str) {
    let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("/"));
    let play_now_path = home.join(".gradio/data/tmp/play_now");
    let content = format!("{}\t{}", path.display(), duration);
    let _ = fs::write(&play_now_path, &content);
    eprintln!("[buscador] play_now → {}", content);
}

/// Agrega una ruta al final de playlist4 en formato TAB
fn append_to_playlist(path: &Path) {
    if !grpautaje::audio_probe::is_playable(path) {
        eprintln!("[buscador] audio inválido, no agregado: {}", path.display());
        return;
    }
    let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("/"));
    let playlist_path = home.join(".gradio/data/tmp/playlist4");

    let dur = get_duration(path);
    let title = path.file_stem()
        .and_then(|s| s.to_str())
        .unwrap_or("Audio")
        .to_string();

    // Formato: /ruta/archivo.mp3\tMM:SS.000
    // Convertir HH:MM:SS a MM:SS.000
    let parts: Vec<&str> = dur.splitn(3, ':').collect();
    let mm_ss = if parts.len() == 3 {
        let h: u64 = parts[0].parse().unwrap_or(0);
        let m: u64 = parts[1].parse().unwrap_or(0);
        let s: u64 = parts[2].parse().unwrap_or(0);
        format!("{:02}:{:02}.000", h * 60 + m, s)
    } else {
        "00:00.000".to_string()
    };

    let line = format!("{}\t{}\n", path.display(), mm_ss);
    if let Ok(mut f) = fs::OpenOptions::new()
        .create(true).append(true)
        .open(&playlist_path)
    {
        use std::io::Write;
        let _ = f.write_all(line.as_bytes());
    }
    eprintln!("[buscador] Agregado a playlist: {} ({})", title, mm_ss);
}

/// Inserta una ruta como PRIMERA línea de playlist4 en formato TAB.
fn prepend_to_playlist(path: &Path) {
    if !grpautaje::audio_probe::is_playable(path) {
        eprintln!("[buscador] audio inválido, no insertado: {}", path.display());
        return;
    }
    let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("/"));
    let playlist_path = home.join(".gradio/data/tmp/playlist4");

    let dur = get_duration(path);
    let title = path.file_stem()
        .and_then(|s| s.to_str())
        .unwrap_or("Audio")
        .to_string();

    // HH:MM:SS → MM:SS.000
    let parts: Vec<&str> = dur.splitn(3, ':').collect();
    let mm_ss = if parts.len() == 3 {
        let h: u64 = parts[0].parse().unwrap_or(0);
        let m: u64 = parts[1].parse().unwrap_or(0);
        let s: u64 = parts[2].parse().unwrap_or(0);
        format!("{:02}:{:02}.000", h * 60 + m, s)
    } else {
        "00:00.000".to_string()
    };

    let new_line = format!("{}\t{}\n", path.display(), mm_ss);
    let existing = fs::read_to_string(&playlist_path).unwrap_or_default();
    let _ = fs::write(&playlist_path, format!("{}{}", new_line, existing));
    eprintln!("[buscador] Insertado al inicio: {} ({})", title, mm_ss);
}

/// Helper local: botón con ícono PNG escalado.
fn icon_button(png_bytes: &[u8], tooltip: &str, size: i32) -> Button {
    let btn = Button::new();
    btn.set_tooltip_text(Some(tooltip));
    btn.set_size_request(size, size);
    btn.set_hexpand(false);
    btn.set_vexpand(false);

    let loader = gdk4::gdk_pixbuf::PixbufLoader::new();
    loader.write(png_bytes).unwrap_or(());
    loader.close().unwrap_or(());
    if let Some(pixbuf) = loader.pixbuf() {
        let icon_size = (size - 10).max(16);
        if let Some(scaled) = pixbuf.scale_simple(
            icon_size, icon_size,
            gdk4::gdk_pixbuf::InterpType::Bilinear,
        ) {
            let texture = gdk4::Texture::for_pixbuf(&scaled);
            let image = gtk4::Image::from_paintable(Some(&texture));
            btn.set_child(Some(&image));
        }
    }
    btn
}

const APP_CSS: &str = r#"
/* ── Forzar tema oscuro en todos los widgets ── */
window, .background {
    background-color: #1e1e1e;
    color: #e0e0e0;
}
box, scrolledwindow, viewport {
    background-color: #1e1e1e;
    color: #e0e0e0;
}
listbox {
    background-color: #1e1e1e;
    color: #e0e0e0;
}
listbox row {
    background-color: #1e1e1e;
    color: #e0e0e0;
}
label { color: #e0e0e0; }
separator {
    background-color: #444444;
    min-width: 1px;
    min-height: 1px;
}

.toolbar {
    background-color: #2a2a2a;
    padding: 4px 8px;
    border-bottom: 1px solid #444;
}

.search-entry {
    background: #333333;
    color: #ffffff;
    border: 1px solid #555555;
    border-radius: 4px;
    padding: 4px 8px;
    font-size: 13px;
    min-width: 300px;
}

.search-entry:focus {
    border-color: #4a90d9;
}

.header-row {
    background-color: #2a3a4a;
    padding: 4px 8px;
    border-bottom: 1px solid #555;
}

.header-label {
    color: #aaccee;
    font-weight: bold;
    font-size: 14px;
}

.result-row {
    padding: 3px 8px;
    border-bottom: 1px solid #2a2a2a;
    background-color: #1e1e1e;
}

.result-row:nth-child(even) {
    background-color: #252525;
}

.result-row:hover {
    background-color: #2a4a6a;
}

.result-row:selected {
    background-color: #1a6496;
}

.col-title {
    color: #e0e0e0;
    font-size: 15px;
    min-width: 280px;
}

.col-time {
    color: #88cc88;
    font-size: 15px;
    min-width: 80px;
}

.col-path {
    color: #888888;
    font-size: 13px;
}

/* Botón play-now — igual que queue-idx-btn del playlist */
button.btn-play-now,
button.btn-play-now * {
    all: unset;
}
button.btn-play-now {
    background: #1a6496;
    color: #ffffff;
    font-size: 12px;
    font-weight: bold;
    min-width: 28px;
    min-height: 20px;
    padding: 2px 6px;
    border-radius: 3px;
    margin-right: 4px;
}
button.btn-play-now:hover {
    background: #2980b9;
}
button.btn-play-now:active {
    background: #145074;
}

/* Botones inline up/down: insertar al inicio / agregar al final */
button.btn-pos,
button.btn-pos * { all: unset; }
button.btn-pos {
    background: #2a3a4a;
    border: 1px solid #3a5a7a;
    border-radius: 3px;
    min-width: 28px;
    min-height: 24px;
    margin-left: 2px;
    padding: 2px;
}
button.btn-pos:hover  { background: #3a6a9a; }
button.btn-pos:active { background: #1a4a7a; }


.status-bar {
    background-color: #2a2a2a;
    padding: 3px 8px;
    border-top: 1px solid #444;
    color: #888888;
    font-size: 11px;
}

.btn-toolbar {
    background: #3a3a3a;
    color: #e0e0e0;
    border: 1px solid #555;
    border-radius: 4px;
    padding: 4px 10px;
    font-size: 12px;
    min-width: 32px;
    min-height: 32px;
}

.btn-toolbar:hover {
    background: #4a90d9;
    color: #ffffff;
}
"#;

fn main() -> glib::ExitCode {
    // i18n: leer locale del config compartido si está, si no autodetectar
    let locale_cfg = read_locale_from_config();
    grpautaje::i18n::init(locale_cfg.as_deref());

    let app = Application::builder()
        .application_id(APP_ID)
        .build();

    app.connect_activate(|app| {
        if let Some(w) = app.active_window() { w.present(); return; }
        build_ui(app);
    });
    app.run()
}

/// Lee la línea 17 (locale) del archivo gradio_config si existe.
fn read_locale_from_config() -> Option<String> {
    let home = dirs::home_dir()?;
    let path = home.join(".gradio/data/gradio_config");
    let content = std::fs::read_to_string(path).ok()?;
    let line = content.lines().nth(16)?.trim();
    if line.is_empty() { None } else { Some(line.to_string()) }
}

fn build_ui(app: &Application) {
    // Solicitar tema oscuro al sistema
    if let Some(settings) = gtk4::Settings::default() {
        settings.set_gtk_application_prefer_dark_theme(true);
    }

    // CSS con prioridad máxima para pisar el tema del sistema
    let provider = CssProvider::new();
    provider.load_from_data(APP_CSS);
    if let Some(display) = gdk4::Display::default() {
        gtk::style_context_add_provider_for_display(
            &display, &provider,
            gtk4::STYLE_PROVIDER_PRIORITY_USER,
        );
        skin::aplicar_css_extra(&display);
    }

    let window = ApplicationWindow::builder()
        .application(app)
        .title(tr("buscador.win_title"))
        .default_width(900)
        .default_height(500)
        .build();

    // Resultados compartidos entre búsqueda en hilo y UI
    let results: Rc<RefCell<Vec<PathBuf>>> = Rc::new(RefCell::new(Vec::new()));
    let searching = Rc::new(RefCell::new(false));

    // ── Layout principal ──────────────────────────────────────────────────────
    let vbox = GtkBox::new(Orientation::Vertical, 0);

    // ── Barra de herramientas ─────────────────────────────────────────────────
    let toolbar = GtkBox::new(Orientation::Horizontal, 6);
    toolbar.add_css_class("toolbar");
    toolbar.set_margin_start(4);
    toolbar.set_margin_end(4);
    toolbar.set_margin_top(4);
    toolbar.set_margin_bottom(4);

    // Botón lupa / buscar
    let btn_search = Button::new();
    btn_search.add_css_class("btn-toolbar");
    btn_search.set_tooltip_text(Some(tr("buscador.btn_search_tip")));
    {
        // Ícono lupa simple con label
        let lbl = Label::new(Some("🔍"));
        btn_search.set_child(Some(&lbl));
    }

    // Entry de búsqueda
    let entry = Entry::new();
    entry.add_css_class("search-entry");
    entry.set_placeholder_text(Some(tr("buscador.entry_ph")));
    entry.set_hexpand(true);

    // Botón limpiar
    let btn_clear = Button::with_label("✕");
    btn_clear.add_css_class("btn-toolbar");
    btn_clear.set_tooltip_text(Some(tr("buscador.btn_clear")));

    // Botón actualizar índice (updatedb)
    let btn_updatedb = Button::with_label(tr("buscador.btn_index"));
    btn_updatedb.add_css_class("btn-toolbar");
    btn_updatedb.set_tooltip_text(Some(tr("buscador.btn_index_tip")));

    toolbar.append(&btn_search);
    toolbar.append(&btn_clear);
    toolbar.append(&entry);
    toolbar.append(&btn_updatedb);

    // ── Cabecera de columnas ──────────────────────────────────────────────────
    let header = GtkBox::new(Orientation::Horizontal, 0);
    header.add_css_class("header-row");

    let h_num   = Label::new(Some("  #"));
    let h_title = Label::new(Some(tr("buscador.col_tema")));
    let h_time  = Label::new(Some(tr("buscador.col_tiempo")));
    let h_path  = Label::new(Some(tr("buscador.col_ruta")));

    for (lbl, width) in [(&h_num, 40), (&h_title, 280), (&h_time, 90), (&h_path, -1)] {
        lbl.add_css_class("header-label");
        lbl.set_halign(gtk::Align::Start);
        lbl.set_margin_start(4);
        if width > 0 { lbl.set_size_request(width, -1); }
        else { lbl.set_hexpand(true); }
        header.append(lbl);
    }

    // ── Lista de resultados ───────────────────────────────────────────────────
    let listbox = ListBox::new();
    listbox.set_selection_mode(gtk::SelectionMode::Single);

    let scroll = ScrolledWindow::new();
    scroll.set_vexpand(true);
    scroll.set_child(Some(&listbox));

    // ── Barra de estado ───────────────────────────────────────────────────────
    let status_bar = GtkBox::new(Orientation::Horizontal, 0);
    status_bar.add_css_class("status-bar");
    let status_lbl = Label::new(Some(tr("buscador.status_ready")));
    status_lbl.set_halign(gtk::Align::Start);
    status_lbl.set_margin_start(8);
    status_bar.append(&status_lbl);

    vbox.append(&toolbar);
    vbox.append(&header);
    vbox.append(&scroll);
    vbox.append(&status_bar);
    window.set_child(Some(&vbox));

    // ── Función de búsqueda ───────────────────────────────────────────────────
    let do_search = {
        let entry       = entry.clone();
        let listbox     = listbox.clone();
        let status_lbl  = status_lbl.clone();
        let results     = results.clone();
        let searching   = searching.clone();

        move || {
            let query = entry.text().to_string();
            if query.trim().is_empty() { return; }

            if *searching.borrow() { return; }
            *searching.borrow_mut() = true;

            // Limpiar lista
            while let Some(ch) = listbox.first_child() { listbox.remove(&ch); }
            status_lbl.set_text(&tr("buscador.status_buscando").replace("{q}", &query));

            let query_clone = query.clone();

            // Buscar en hilo separado para no bloquear UI
            let (tx, rx) = std::sync::mpsc::channel::<Vec<PathBuf>>();
            thread::spawn(move || {
                let found = search_files(&[], &query_clone);
                let _ = tx.send(found);
            });

            // Polling con timeout_add para recibir resultado
            let listbox_c   = listbox.clone();
            let status_c    = status_lbl.clone();
            let results_c   = results.clone();
            let searching_c = searching.clone();

            glib::timeout_add_local(std::time::Duration::from_millis(100), move || {
                match rx.try_recv() {
                    Ok(found) => {
                        *results_c.borrow_mut() = found.clone();
                        *searching_c.borrow_mut() = false;

                        let count = found.len();
                        status_c.set_text(
                            &tr("buscador.status_resultados")
                                .replace("{n}", &count.to_string())
                                .replace("{q}", &query)
                        );

                        for (i, path) in found.iter().enumerate() {
                            let row = build_result_row(i + 1, path);
                            listbox_c.append(&row);
                        }

                        glib::ControlFlow::Break
                    }
                    Err(std::sync::mpsc::TryRecvError::Empty) => glib::ControlFlow::Continue,
                    Err(_) => {
                        *searching_c.borrow_mut() = false;
                        glib::ControlFlow::Break
                    }
                }
            });
        }
    };

    // Activar con Enter en el entry
    let do_search_entry = do_search.clone();
    entry.connect_activate(move |_| do_search_entry());

    // Activar con botón lupa
    let do_search_btn = do_search.clone();
    btn_search.connect_clicked(move |_| do_search_btn());

    // Limpiar
    let entry_clear  = entry.clone();
    let listbox_clear = listbox.clone();
    let status_clear = status_lbl.clone();
    btn_clear.connect_clicked(move |_| {
        entry_clear.set_text("");
        while let Some(ch) = listbox_clear.first_child() { listbox_clear.remove(&ch); }
        status_clear.set_text(tr("buscador.status_listo"));
    });

    // Actualizar índice (updatedb) — corre en hilo separado para no bloquear GTK
    {
        let status2 = status_lbl.clone();
        let btn2    = btn_updatedb.clone();
        btn_updatedb.connect_clicked(move |_| {
            status2.set_text(tr("buscador.status_indexando"));
            btn2.set_sensitive(false);
            // Resultado compartido entre hilo OS y timer GTK
            let resultado: std::sync::Arc<std::sync::Mutex<Option<bool>>> =
                std::sync::Arc::new(std::sync::Mutex::new(None));
            let res2 = resultado.clone();
            std::thread::spawn(move || {
                let db_path = dirs::home_dir()
                    .unwrap_or_else(|| std::path::PathBuf::from("/"))
                    .join(".gradio/data/locatedb");
                let home = dirs::home_dir()
                    .unwrap_or_else(|| std::path::PathBuf::from("/"));
                let ok = std::process::Command::new("updatedb")
                    .arg("--output").arg(&db_path)
                    .arg("--localpaths").arg(&home)
                    .status()
                    .map(|s| s.success())
                    .unwrap_or(false);
                *res2.lock().unwrap() = Some(ok);
            });
            let status3 = status2.clone();
            let btn3    = btn2.clone();
            glib::timeout_add_local(std::time::Duration::from_millis(500), move || {
                if let Some(ok) = *resultado.lock().unwrap() {
                    status3.set_text(if ok { tr("buscador.status_index_ok") } else { tr("buscador.status_index_err") });
                    btn3.set_sensitive(true);
                    glib::ControlFlow::Break
                } else {
                    glib::ControlFlow::Continue
                }
            });
        });
    }

    // Ícono en taskbar
    {
        let loader = gdk4::gdk_pixbuf::PixbufLoader::new();
        let _ = loader.write(&skin::icono("busqueda.png", ICONO_BUSCADOR));
        let _ = loader.close();
        if let Some(pb) = loader.pixbuf() {
            let tex = gdk4::Texture::for_pixbuf(&pb);
            let win = window.clone();
            window.connect_realize(move |w| {
                if let Some(surf) = w.surface() {
                    use gdk4::prelude::ToplevelExt;
                    if let Some(tl) = surf.dynamic_cast_ref::<gdk4::Toplevel>() {
                        tl.set_icon_list(&[tex.clone()]);
                    }
                    let display = gtk4::prelude::WidgetExt::display(w);
                    use gdk4::prelude::DisplayExt;
                    if let Some(monitor) = display.monitor_at_surface(&surf) {
                        use gdk4::prelude::MonitorExt;
                        let geo = monitor.geometry();
                        let ancho = (geo.width() - 20).min(950).max(600);
                        let alto  = (geo.height() - 90).min(680).max(400);
                        win.set_default_size(ancho, alto);
                    }
                }
            });
        }
    }

    window.present();
    // Foco en el entry al abrir
    entry.grab_focus();
}

/// Construye una fila de resultado con número, título, tiempo y ruta.
/// Doble clic → agrega a playlist4.
/// Arrastre → DragSource con la ruta como text/uri-list.
/// Estado compartido del pipeline CUE del buscador
static CUE_PIPELINE: std::sync::OnceLock<Arc<Mutex<Option<gstreamer::Pipeline>>>> =
    std::sync::OnceLock::new();

fn get_cue_pipeline() -> &'static Arc<Mutex<Option<gstreamer::Pipeline>>> {
    CUE_PIPELINE.get_or_init(|| Arc::new(Mutex::new(None)))
}

fn abrir_ventana_cue_buscador(path: &Path, title: &str) {
    // Inicializar GStreamer (seguro llamar múltiples veces)
    gstreamer::init().unwrap_or(());
    
    let title = title.to_string();
    let uri = format!("file://{}", path.display());
    
    // Detener CUE anterior
    if let Ok(mut guard) = get_cue_pipeline().lock() {
        if let Some(old) = guard.take() {
            let _ = old.set_state(gstreamer::State::Null);
        }
    }

    // Leer tarjeta CUE desde gradio.config
    let cue_dev: Option<String> = {
        let home = dirs::home_dir().unwrap_or_default();
        let cfg = home.join(".gradio/data/tmp/gradio.config");
        std::fs::read_to_string(cfg).ok()
            .and_then(|s| s.lines().nth(1).map(|l| l.trim().to_string()))
            .filter(|s| !s.is_empty())
    };

    // Construir pipeline CUE
    let pipeline = gstreamer::Pipeline::new();
    let src = match gstreamer::ElementFactory::make("uridecodebin")
        .property("uri", &uri).build() { Ok(e) => e, Err(_) => return };
    let conv  = gstreamer::ElementFactory::make("audioconvert").build().unwrap();
    let resamp = gstreamer::ElementFactory::make("audioresample").build().unwrap();
    let sink = if let Some(ref dev) = cue_dev {
        gstreamer::ElementFactory::make("pulsesink")
            .property("device", dev.as_str()).build()
            .unwrap_or_else(|_| gstreamer::ElementFactory::make("autoaudiosink").build().unwrap())
    } else {
        gstreamer::ElementFactory::make("autoaudiosink").build().unwrap()
    };
    let _ = pipeline.add_many([&src, &conv, &resamp, &sink]);
    let _ = gstreamer::Element::link_many([&conv, &resamp, &sink]);
    let conv_w = conv.downgrade();
    src.connect_pad_added(move |_, pad| {
        if let Some(c) = conv_w.upgrade() {
            if let Some(sink_pad) = c.static_pad("sink") {
                let _ = pad.link(&sink_pad);
            }
        }
    });
    let _ = pipeline.set_state(gstreamer::State::Playing);

    // Guardar pipeline
    *get_cue_pipeline().lock().unwrap() = Some(pipeline.clone());

    // ── Ventana de control ──────────────────────────────────────────────────
    let win = gtk4::Window::new();
    win.set_title(Some(&format!("🎧 CUE: {}", title)));
    win.set_default_size(360, 100);
    win.set_resizable(false);

    let vbox = GtkBox::new(Orientation::Vertical, 8);
    vbox.set_margin_top(12); vbox.set_margin_bottom(12);
    vbox.set_margin_start(12); vbox.set_margin_end(12);

    let seek = gtk4::Scale::new(
        Orientation::Horizontal,
        Some(&gtk4::Adjustment::new(0.0, 0.0, 100.0, 1.0, 5.0, 0.0))
    );
    seek.set_hexpand(true);
    seek.set_draw_value(false);

    let time_lbl = gtk4::Label::new(Some("00:00 / 00:00"));

    let time_row = GtkBox::new(Orientation::Horizontal, 8);
    time_row.append(&seek);
    time_row.append(&time_lbl);

    let btn_row = GtkBox::new(Orientation::Horizontal, 8);
    btn_row.set_halign(gtk4::Align::Center);
    let btn_pause = Button::with_label(tr("buscador.btn_pause"));
    let btn_stop  = Button::with_label(tr("buscador.btn_stop"));
    btn_row.append(&btn_pause);
    btn_row.append(&btn_stop);

    vbox.append(&time_row);
    vbox.append(&btn_row);
    win.set_child(Some(&vbox));

    // Pausa/Resume
    let pipe_p = pipeline.clone();
    btn_pause.connect_clicked(move |btn| {
        match pipe_p.current_state() {
            gstreamer::State::Playing => {
                let _ = pipe_p.set_state(gstreamer::State::Paused);
                btn.set_label(tr("buscador.btn_resume"));
            }
            _ => {
                let _ = pipe_p.set_state(gstreamer::State::Playing);
                btn.set_label(tr("buscador.btn_pause"));
            }
        }
    });

    // Stop
    let pipe_s = pipeline.clone();
    let win_s = win.clone();
    btn_stop.connect_clicked(move |_| {
        let _ = pipe_s.set_state(gstreamer::State::Null);
        *get_cue_pipeline().lock().unwrap() = None;
        win_s.close();
    });

    // Cerrar ventana = detener
    let pipe_c = pipeline.clone();
    win.connect_close_request(move |_| {
        let _ = pipe_c.set_state(gstreamer::State::Null);
        *get_cue_pipeline().lock().unwrap() = None;
        glib::Propagation::Proceed
    });

    // Timer para seek bar y tiempo
    let pipe_t = pipeline.clone();
    let seek_t = seek.clone();
    let time_t = time_lbl.clone();
    let win_t  = win.clone();
    glib::timeout_add_local(std::time::Duration::from_millis(500), move || {
        if !win_t.is_visible() { return glib::ControlFlow::Break; }
        if let Some(pos) = pipe_t.query_position::<gstreamer::ClockTime>() {
            if let Some(dur) = pipe_t.query_duration::<gstreamer::ClockTime>() {
                let ps = pos.seconds();
                let ds = dur.seconds();
                time_t.set_text(&format!("{:02}:{:02} / {:02}:{:02}",
                    ps/60, ps%60, ds/60, ds%60));
                if ds > 0 {
                    seek_t.set_value((ps as f64 / ds as f64) * 100.0);
                }
            }
        }
        // Detectar EOS
        if let Some(bus) = pipe_t.bus() {
            if let Some(msg) = bus.timed_pop_filtered(gstreamer::ClockTime::ZERO,
                &[gstreamer::MessageType::Eos]) {
                if let gstreamer::MessageView::Eos(_) = msg.view() {
                    win_t.close();
                    return glib::ControlFlow::Break;
                }
            }
        }
        glib::ControlFlow::Continue
    });

    // Seek al hacer clic en la barra
    let pipe_sk = pipeline.clone();
    seek.connect_change_value(move |_, _, val| {
        if let Some(dur) = pipe_sk.query_duration::<gstreamer::ClockTime>() {
            let pos = gstreamer::ClockTime::from_seconds(
                (val / 100.0 * dur.seconds() as f64) as u64
            );
            let _ = pipe_sk.seek_simple(
                gstreamer::SeekFlags::FLUSH | gstreamer::SeekFlags::KEY_UNIT, pos
            );
        }
        glib::Propagation::Proceed
    });

    win.present();
}

fn build_result_row(num: usize, path: &Path) -> ListBoxRow {
    let row_box = GtkBox::new(Orientation::Horizontal, 0);
    row_box.add_css_class("result-row");

    let title = path.file_stem()
        .and_then(|s| s.to_str())
        .unwrap_or("Audio")
        .to_string();

    let dur = get_duration(path);
    let parent_str = path.parent()
        .map(|p| p.to_string_lossy().to_string())
        .unwrap_or_default();

    // Botón azul numérico: click → crossfade inmediato al deck libre
    let btn_play = Button::with_label(&format!("{}", num));
    btn_play.add_css_class("btn-play-now");
    btn_play.set_tooltip_text(Some(tr("buscador.btn_play_now_tip")));
    {
        let path_play = path.to_path_buf();
        let dur_play  = dur.clone();
        btn_play.connect_clicked(move |_| {
            write_play_now(&path_play, &dur_play);
        });
    }

    let lbl_title = Label::new(Some(&title));
    let lbl_time  = Label::new(Some(&dur));
    let lbl_path  = Label::new(Some(&parent_str));

    lbl_title.set_size_request(280, -1);
    lbl_title.set_halign(gtk::Align::Start);
    lbl_title.set_ellipsize(gtk::pango::EllipsizeMode::End);
    lbl_title.add_css_class("col-title");

    lbl_time.set_size_request(90, -1);
    lbl_time.set_halign(gtk::Align::Start);
    lbl_time.add_css_class("col-time");

    lbl_path.set_hexpand(true);
    lbl_path.set_halign(gtk::Align::Start);
    lbl_path.set_ellipsize(gtk::pango::EllipsizeMode::Start);
    lbl_path.add_css_class("col-path");

    // Botones inline: insertar al inicio / al final de playlist4
    let btn_first = icon_button(&skin::icono("up.png", ICONO_UP),     tr("buscador.btn_first"), 28);
    let btn_last  = icon_button(&skin::icono("down.png", ICONO_DOWN), tr("buscador.btn_last"),   28);
    btn_first.add_css_class("btn-pos");
    btn_last.add_css_class("btn-pos");
    {
        let path_first = path.to_path_buf();
        btn_first.connect_clicked(move |_| prepend_to_playlist(&path_first));
    }
    {
        let path_last = path.to_path_buf();
        btn_last.connect_clicked(move |_| append_to_playlist(&path_last));
    }

    row_box.append(&btn_play);
    row_box.append(&lbl_title);
    row_box.append(&lbl_time);
    row_box.append(&lbl_path);
    row_box.append(&btn_first);
    row_box.append(&btn_last);

    let row = ListBoxRow::new();
    row.set_child(Some(&row_box));

    // ── Doble clic izquierdo: agregar al final de playlist4 ──────────────────
    {
        let path_owned = path.to_path_buf();
        let click = GestureClick::new();
        click.set_button(1);
        click.connect_released(move |_, n_press, _, _| {
            if n_press == 2 {
                append_to_playlist(&path_owned);
            }
        });
        row.add_controller(click);
    }

    // ── Clic derecho: popover con CUE y Agregar ──────────────────────────────
    {
        let path_rc = Rc::new(path.to_path_buf());
        let title_rc = title.clone();
        let right = GestureClick::new();
        right.set_button(3);
        right.connect_released(move |gesture, _, x, y| {
            gesture.set_state(gtk::EventSequenceState::Claimed);

            let pop = gtk::Popover::new();
            pop.set_has_arrow(false);
            pop.set_autohide(true);
            pop.set_pointing_to(Some(&gdk4::Rectangle::new(x as i32, y as i32, 1, 1)));

            let vbox = GtkBox::new(Orientation::Vertical, 0);
            vbox.set_margin_top(2); vbox.set_margin_bottom(2);
            vbox.set_margin_start(2); vbox.set_margin_end(2);

            let btn_cue = Button::with_label(tr("buscador.menu_cue"));
            let btn_add = Button::with_label(tr("buscador.menu_add"));
            btn_cue.add_css_class("context-menu-item");
            btn_add.add_css_class("context-menu-item");
            vbox.append(&btn_cue);
            vbox.append(&btn_add);
            pop.set_child(Some(&vbox));

            // Anclar al row
            if let Some(w) = gesture.widget() {
                pop.set_parent(&w);
            }

            // CUE → ventana auxiliar GStreamer con seek, pausa, stop
            let path_cue  = path_rc.clone();
            let title_cue = title_rc.clone();
            let pop_cue   = pop.clone();
            btn_cue.connect_clicked(move |_| {
                pop_cue.popdown();
                abrir_ventana_cue_buscador(&path_cue, &title_cue);
            });

            // Agregar a playlist
            let path_add = path_rc.clone();
            let pop_add  = pop.clone();
            btn_add.connect_clicked(move |_| {
                pop_add.popdown();
                append_to_playlist(&path_add);
            });

            pop.popup();
        });
        row.add_controller(right);
    }

    // ── DragSource: arrastar URI para soltar en main ──────────────────────────
    // FileList (GDK_TYPE_FILE_LIST) en lugar de for_bytes: el GType se anuncia
    // directamente y los DropTarget(FileList) del scroll/frame aceptan la zona
    // expandida sin necesidad de deserializar primero el MIME type.
    let drag_src = gtk4::DragSource::new();
    drag_src.set_actions(gdk4::DragAction::COPY);
    let gio_file = gtk4::gio::File::for_path(path);
    let file_list = gdk4::FileList::from_array(&[gio_file]);
    drag_src.connect_prepare(move |_, _, _| {
        Some(ContentProvider::for_value(&file_list.to_value()))
    });
    row.add_controller(drag_src);

    row
}


fn abrir_ventana_cue(path: &std::path::Path, title: &str) {
    // Leer tarjeta CUE de gradio.config (línea 2)
    let cue_dev = dirs::home_dir()
        .and_then(|h| std::fs::read_to_string(h.join(".gradio/data/tmp/gradio.config")).ok())
        .and_then(|s| s.lines().nth(1).map(|l| l.trim().to_string()))
        .filter(|s| !s.is_empty());

    let uri = format!("file://{}", path.display());

    // Construir pipeline GStreamer
    gstreamer::init().unwrap_or(());
    let pipeline = gstreamer::Pipeline::new();
    let src = match gstreamer::ElementFactory::make("uridecodebin")
        .property("uri", &uri).build() { Ok(e) => e, Err(_) => return };
    let convert = match gstreamer::ElementFactory::make("audioconvert").build() { Ok(e) => e, Err(_) => return };
    let resample = match gstreamer::ElementFactory::make("audioresample").build() { Ok(e) => e, Err(_) => return };
    let sink = match cue_dev {
        Some(ref dev) => gstreamer::ElementFactory::make("pulsesink")
            .property("device", dev.as_str()).build()
            .unwrap_or_else(|_| gstreamer::ElementFactory::make("autoaudiosink").build().unwrap()),
        None => match gstreamer::ElementFactory::make("autoaudiosink").build() { Ok(e) => e, Err(_) => return },
    };
    if pipeline.add_many([&src, &convert, &resample, &sink]).is_err() { return; }
    if gstreamer::Element::link_many([&convert, &resample, &sink]).is_err() { return; }
    let convert_w = convert.downgrade();
    src.connect_pad_added(move |_, pad| {
        if let Some(c) = convert_w.upgrade() {
            if let Some(sp) = c.static_pad("sink") {
                if !sp.is_linked() { let _ = pad.link(&sp); }
            }
        }
    });
    let _ = pipeline.set_state(gstreamer::State::Playing);
    let pipeline = Rc::new(pipeline);

    // Ventana auxiliar
    let win = gtk::Window::new();
    win.set_title(Some(&format!("🎧 CUE: {}", title)));
    win.set_default_size(340, 90);
    win.set_resizable(false);

    let vbox = gtk::Box::new(gtk::Orientation::Vertical, 8);
    vbox.set_margin_top(12); vbox.set_margin_bottom(12);
    vbox.set_margin_start(12); vbox.set_margin_end(12);

    let seek = gtk::Scale::new(gtk::Orientation::Horizontal,
        Some(&gtk::Adjustment::new(0.0, 0.0, 100.0, 1.0, 5.0, 0.0)));
    seek.set_hexpand(true);
    seek.set_draw_value(false);
    let time_lbl = gtk::Label::new(Some("00:00 / 00:00"));

    let time_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
    time_row.append(&seek);
    time_row.append(&time_lbl);

    let btn_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
    btn_row.set_halign(gtk::Align::Center);
    let btn_pause = Button::with_label(tr("buscador.btn_pause"));
    let btn_stop  = Button::with_label(tr("buscador.btn_stop"));
    btn_row.append(&btn_pause);
    btn_row.append(&btn_stop);
    vbox.append(&time_row);
    vbox.append(&btn_row);
    win.set_child(Some(&vbox));

    // Pausa/resume
    let pipe_p = pipeline.clone();
    btn_pause.connect_clicked(move |btn| {
        match pipe_p.current_state() {
            gstreamer::State::Playing => {
                let _ = pipe_p.set_state(gstreamer::State::Paused);
                btn.set_label(tr("buscador.btn_resume"));
            }
            _ => {
                let _ = pipe_p.set_state(gstreamer::State::Playing);
                btn.set_label(tr("buscador.btn_pause"));
            }
        }
    });

    // Stop
    let pipe_s  = pipeline.clone();
    let win_s   = win.clone();
    btn_stop.connect_clicked(move |_| {
        let _ = pipe_s.set_state(gstreamer::State::Null);
        win_s.close();
    });

    // Cerrar = detener
    let pipe_cl = pipeline.clone();
    win.connect_close_request(move |_| {
        let _ = pipe_cl.set_state(gstreamer::State::Null);
        glib::Propagation::Proceed
    });

    // Timer: posición + EOS
    let pipe_t  = pipeline.clone();
    let seek_c  = seek.clone();
    let time_c  = time_lbl.clone();
    let win_t   = win.clone();
    let pipe_t2 = pipeline.clone();
    glib::timeout_add_local(std::time::Duration::from_millis(300), move || {
        let pos = pipe_t.query_position::<gstreamer::ClockTime>()
            .map(|t| t.seconds() as f64).unwrap_or(0.0);
        let dur = pipe_t.query_duration::<gstreamer::ClockTime>()
            .map(|t| t.seconds() as f64).unwrap_or(1.0).max(1.0);
        seek_c.set_range(0.0, dur);
        seek_c.set_value(pos);
        let fmt = |s: f64| format!("{:02}:{:02}", s as u64 / 60, s as u64 % 60);
        time_c.set_text(&format!("{} / {}", fmt(pos), fmt(dur)));
        if let Some(bus) = pipe_t.bus() {
            while let Some(msg) = bus.pop() {
                if let gstreamer::MessageView::Eos(_) = msg.view() {
                    let _ = pipe_t2.set_state(gstreamer::State::Null);
                    win_t.close();
                    return glib::ControlFlow::Break;
                }
            }
        }
        if !win_t.is_visible() { return glib::ControlFlow::Break; }
        glib::ControlFlow::Continue
    });

    // Seek manual
    let pipe_sk = pipeline.clone();
    seek.connect_change_value(move |_, _, v| {
        let _ = pipe_sk.seek_simple(
            gstreamer::SeekFlags::FLUSH | gstreamer::SeekFlags::KEY_UNIT,
            gstreamer::ClockTime::from_seconds(v as u64),
        );
        glib::Propagation::Proceed
    });

    win.present();
}
