"""
Grid de seleccion de paquetes con tarjetas artisticas abstractas.

Cada tarjeta usa un Canvas con formas organicas tipo pintura fluida
(blobs suaves, colores vibrantes, efecto acuarela abstracto).
Clic derecho en una tarjeta custom abre el dialogo de edicion.
"""
import hashlib
import json
import math
import random
import threading
import tkinter as tk
import customtkinter as ctk
from typing import Callable, Optional
import api
import session

# ── Altura fija de cada tarjeta ───────────────────────────────────────────────
_CARD_H = 178
_POLL_INTERVAL = 10_000   # ms

# ── Paletas por color de paquete ──────────────────────────────────────────────
_PALETTES = {
    "blue": {
        "bg":     "#0a1828",
        "blobs":  ["#1a3a6a", "#234888", "#2858a0", "#1e5090", "#163060"],
        "accent": "#90caf9",
    },
    "green": {
        "bg":     "#091a10",
        "blobs":  ["#163e22", "#1e5030", "#246440", "#1a7848", "#124030"],
        "accent": "#a5d6a7",
    },
    "red": {
        "bg":     "#1a0c0c",
        "blobs":  ["#601818", "#782020", "#902828", "#a03030", "#6c2020"],
        "accent": "#ef9a9a",
    },
    "all": {
        "bg":     "#0d0820",
        "blobs":  ["#281450", "#341870", "#401e8c", "#4824a8", "#2c1460"],
        "accent": "#ce93d8",
    },
}
_DEFAULT_PAL = {
    "bg":     "#0b1020",
    "blobs":  ["#1e2a40", "#2a3a5a", "#243050", "#304070", "#1a2438"],
    "accent": "#90a4ae",
}
_DISABLED_PAL = {
    "bg":     "#0c0c14",
    "blobs":  ["#181820", "#1c1c28", "#141420", "#1a1a26", "#121218"],
    "accent": "#3a3a4a",
}


def _darken(col: str, f: float) -> str:
    """Oscurece un color hex (f=0 negro, f=1 sin cambio)."""
    try:
        h = col.lstrip("#")
        r, g, b = int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)
        return f"#{int(r*f):02x}{int(g*f):02x}{int(b*f):02x}"
    except Exception:
        return col


def _hardware_id(pkg: dict) -> str:
    """
    Genera un ID visual tipo hardware desde los datos del paquete (servidor).
    Formato: GG.BB.RR + 6 chars hex — determinista para el mismo paquete.
    Ejemplo: 05.60.08AC3F2B
    """
    mins  = pkg.get("minutes", 0)
    g     = pkg.get("minutes_green", 5)
    b     = pkg.get("minutes_blue",  mins)
    r     = pkg.get("minutes_red",   8)
    raw   = f"{pkg.get('id', 'x')}:{mins}:{pkg.get('price', 0)}"
    code  = hashlib.md5(raw.encode()).hexdigest()[:6].upper()
    return f"{g:02d}.{b:02d}.{r:02d}{code}"


def _palette_for(pkg: dict) -> dict:
    if pkg.get("custom") and pkg.get("color1"):
        c1, c2 = pkg["color1"], pkg.get("color2", pkg["color1"])
        return {
            "bg":     _darken(c1, 0.17),
            "blobs":  [c1, c2, _darken(c1, 0.5), _darken(c2, 0.5), _darken(c1, 0.32)],
            "accent": c1,
        }
    return _PALETTES.get(pkg.get("color", "blue"), _DEFAULT_PAL)


def _blob_pts(cx, cy, rx, ry, n=12, roughness=0.36, rng=None):
    """Puntos para un blob organico (poligono suave)."""
    rng = rng or random
    pts = []
    for i in range(n):
        a = 2 * math.pi * i / n
        pts += [
            cx + rx * (1 + rng.uniform(-roughness, roughness)) * math.cos(a),
            cy + ry * (1 + rng.uniform(-roughness, roughness)) * math.sin(a),
        ]
    return pts


# ── Tarjeta individual ────────────────────────────────────────────────────────

class PackageCard(ctk.CTkFrame):
    """Tarjeta con fondo abstracto artistico tipo pintura fluida."""

    def __init__(self, parent, pkg: dict, on_click: Callable,
                 on_edit: Optional[Callable] = None,
                 disabled: bool = False,
                 show_times: bool = False):
        self._disabled = disabled
        self._show_times = show_times
        pal = _palette_for(pkg) if not disabled else _DISABLED_PAL
        super().__init__(
            parent,
            corner_radius=14, border_width=2,
            border_color=pal["accent"],
            fg_color=pal["bg"],
            cursor="arrow" if disabled else "hand2",
        )
        self.configure(height=_CARD_H)
        self._pkg   = pkg
        self._pal   = pal
        self._hover = False

        # Canvas interior para el fondo artistico
        cursor = "arrow" if disabled else "hand2"
        self._cv = tk.Canvas(
            self, bg=pal["bg"],
            highlightthickness=0, bd=0, cursor=cursor,
        )
        self._cv.place(x=3, y=3, relwidth=1, relheight=1, width=-6, height=-6)

        for w in (self, self._cv):
            w.bind("<Button-1>", lambda e, p=pkg: on_click(p))
            w.bind("<Enter>",    self._enter)
            w.bind("<Leave>",    self._leave)

        if on_edit:
            for w in (self, self._cv):
                w.bind("<Button-3>", lambda e, p=pkg: on_edit(p))

        self._cv.bind("<Configure>", lambda e: self._render())

    def _enter(self, _=None):
        self._hover = True
        self.configure(border_width=3)
        self._render()

    def _leave(self, _=None):
        self._hover = False
        self.configure(border_width=2)
        self._render()

    def _render(self):
        cv = self._cv
        w, h = cv.winfo_width(), cv.winfo_height()
        if w <= 1 or h <= 1:
            return

        cv.delete("all")
        pal = self._pal
        rng = random.Random(hash(self._pkg.get("id", "x")) & 0x7FFFFFFF)

        # Fondo solido
        cv.create_rectangle(0, 0, w, h, fill=pal["bg"], outline="")

        # Blobs organicos
        for col in pal["blobs"]:
            cx = rng.uniform(-0.2, 1.2) * w
            cy = rng.uniform(-0.2, 1.2) * h
            rx = rng.uniform(0.18, 0.54) * w
            ry = rng.uniform(0.18, 0.54) * h
            pts = _blob_pts(cx, cy, rx, ry, n=14, roughness=0.38, rng=rng)
            cv.create_polygon(pts, smooth=True, fill=col, outline="")

        # Overlay hover sutil
        if self._hover:
            cv.create_rectangle(0, 0, w, h, fill="white",
                                outline="", stipple="gray12")

        # Texto
        self._render_text(cv, w, h)

    def _render_text(self, cv, w, h):
        pkg    = self._pkg
        accent = self._pal["accent"]
        tcol   = pkg.get("text_color", "#ffffff")

        if self._disabled:
            tcol = "#555566"

        # Modo sale con show_times: mostrar tiempos RGB en vez de nombre/precio
        if self._show_times and not self._disabled:
            self._render_times(cv, w, h, tcol, accent)
        else:
            self._render_name_price(cv, w, h, tcol, accent)

    def _render_name_price(self, cv, w, h, tcol, accent):
        """Render clasico: nombre + precio (usado en config y combos sin asignar)."""
        pkg   = self._pkg
        mins  = pkg.get("minutes_blue", 0) or pkg.get("minutes", 0)
        price = pkg.get("price", 0)
        name  = pkg.get("name", "")
        sz_map = {"S": 20, "M": 26, "L": 32}
        sz     = sz_map.get(pkg.get("font_size", "M"), 26)

        if mins >= 60:
            ts = f"{mins//60}h {mins%60}min" if mins % 60 else f"{mins//60}h"
        else:
            ts = f"{mins} min"

        name_half  = max(sz, 26)
        price_half = 24
        gap        = 10
        cy_name  = h // 2 - gap // 2 - price_half // 2
        cy_price = cy_name + name_half // 2 + gap + price_half // 2

        pad = 8
        cv.create_rectangle(pad, cy_name - name_half // 2 - 8,
                            w - pad, cy_price + price_half // 2 + 4,
                            fill="#000000", outline="", stipple="gray50")

        cv.create_text(w//2 + 2, cy_name + 2, text=name,
                       fill="#000000", font=("Segoe UI", sz, "bold"), anchor="center")
        cv.create_text(w//2, cy_name, text=name,
                       fill=tcol, font=("Segoe UI", sz, "bold"), anchor="center")

        if self._disabled:
            cv.create_text(w//2, cy_price, text="Sin asignar",
                           fill="#ff9800", font=("Segoe UI", 14, "bold"), anchor="center")
        else:
            price_text = f"● {ts}  ·  ${price:.2f}"
            cv.create_text(w//2 + 1, cy_price + 1, text=price_text,
                           fill="#000000", font=("Segoe UI", 16), anchor="center")
            cv.create_text(w//2, cy_price, text=price_text,
                           fill="#64b5f6", font=("Segoe UI", 16), anchor="center")

    def _render_times(self, cv, w, h, tcol, accent):
        """Render modo venta: nombre terminal + duracion + valor."""
        pkg = self._pkg
        name  = pkg.get("name", "")
        price = pkg.get("price", 0)
        b = pkg.get("minutes_blue",  0)
        total = b

        if total >= 60:
            total_txt = f"{total//60}h {total%60}min" if total % 60 else f"{total//60}h"
        else:
            total_txt = f"{total} min"

        # 3 zonas verticales: nombre (arriba), duracion (centro), valor (abajo)
        cy_name  = h // 2 - 38
        cy_dur   = h // 2 + 2
        cy_price = h // 2 + 38

        # Backdrop
        pad = 8
        cv.create_rectangle(pad, cy_name - 18, w - pad, cy_price + 16,
                            fill="#000000", outline="", stipple="gray50")

        # Nombre terminal
        cv.create_text(w//2 + 1, cy_name + 1, text=name,
                       fill="#000000", font=("Segoe UI", 16, "bold"), anchor="center")
        cv.create_text(w//2, cy_name, text=name,
                       fill="#e0e0e0", font=("Segoe UI", 16, "bold"), anchor="center")

        # Duracion grande
        cv.create_text(w//2 + 2, cy_dur + 2, text=total_txt,
                       fill="#000000", font=("Segoe UI", 26, "bold"), anchor="center")
        cv.create_text(w//2, cy_dur, text=total_txt,
                       fill=tcol, font=("Segoe UI", 26, "bold"), anchor="center")

        # Valor
        price_txt = f"${price:.2f}"
        cv.create_text(w//2 + 1, cy_price + 1, text=price_txt,
                       fill="#000000", font=("Segoe UI", 18, "bold"), anchor="center")
        cv.create_text(w//2, cy_price, text=price_txt,
                       fill="#64b5f6", font=("Segoe UI", 18, "bold"), anchor="center")

        # Badge combo custom
        if pkg.get("custom"):
            badge = "sin asignar" if self._disabled else "custom"
            cv.create_text(w - 8, 9, text=badge,
                           fill=accent, font=("Segoe UI", 8), anchor="ne")


# ── Tarjeta "Agregar" ─────────────────────────────────────────────────────────

class AddCard(ctk.CTkFrame):
    """Tarjeta para agregar un nuevo combo personalizado."""

    def __init__(self, parent, on_click: Callable):
        super().__init__(
            parent,
            corner_radius=14, border_width=2,
            border_color="#2a2a40",
            fg_color="#0a0d18",
            cursor="hand2",
        )
        self.configure(height=_CARD_H)
        self._hover = False

        self._cv = tk.Canvas(
            self, bg="#0a0d18",
            highlightthickness=0, bd=0, cursor="hand2",
        )
        self._cv.place(x=3, y=3, relwidth=1, relheight=1, width=-6, height=-6)

        for w in (self, self._cv):
            w.bind("<Button-1>", lambda e: on_click())
            w.bind("<Enter>",    self._enter)
            w.bind("<Leave>",    self._leave)

        self._cv.bind("<Configure>", lambda e: self._render())

    def _enter(self, _=None):
        self._hover = True
        self.configure(border_color="#4fc3f7", border_width=2)
        self._render()

    def _leave(self, _=None):
        self._hover = False
        self.configure(border_color="#2a2a40", border_width=2)
        self._render()

    def _render(self):
        cv = self._cv
        w, h = cv.winfo_width(), cv.winfo_height()
        if w <= 1 or h <= 1:
            return

        cv.delete("all")
        bg  = "#10192e" if self._hover else "#0a0d18"
        col = "#4fc3f7" if self._hover else "#3a3a60"

        cv.create_rectangle(0, 0, w, h, fill=bg, outline="")

        # Icono + y texto
        cv.create_text(w//2, h//2 - 14, text="＋",
                       fill=col, font=("Segoe UI", 24, "bold"), anchor="center")
        cv.create_text(w//2, h//2 + 14, text="Nuevo combo",
                       fill=col, font=("Segoe UI", 10), anchor="center")


# ── Widget principal ──────────────────────────────────────────────────────────

class PackageSelector(ctk.CTkFrame):
    """
    Widget con el grid de paquetes + boton "+".
    Al hacer clic en un paquete abre SaleDialog directamente.
    Muestra un overlay cuando el agente no esta disponible.
    """

    def __init__(self, parent, cashier: str = "caja1",
                 on_sale_complete: Optional[Callable] = None,
                 on_navigate: Optional[Callable] = None,
                 mode: str = "sale", **kwargs):
        super().__init__(parent, fg_color="transparent", **kwargs)
        self._cashier         = cashier
        self.on_sale_complete = on_sale_complete
        self._on_navigate     = on_navigate
        self._mode            = mode  # "sale" o "config"
        self._agent_online    = True
        self._overlay: Optional[ctk.CTkFrame] = None
        self._grid_frame: Optional[ctk.CTkFrame] = None
        self._pkg_fingerprint = ""  # fingerprint para detectar cambios en paquetes

        self._build_loading()
        self.refresh()
        if self._mode == "sale":
            self._schedule_poll()

    # ------------------------------------------------------------------
    # Estado de carga
    # ------------------------------------------------------------------

    def _build_loading(self):
        self._loading_frame = ctk.CTkFrame(self, fg_color="transparent")
        self._loading_frame.pack(expand=True, fill="both")
        ctk.CTkLabel(
            self._loading_frame,
            text="Cargando paquetes...",
            font=ctk.CTkFont(size=14),
            text_color="#888888",
        ).pack(expand=True)

    def _clear(self):
        for w in self.winfo_children():
            w.destroy()
        self._overlay    = None
        self._grid_frame = None

    # ------------------------------------------------------------------
    # Carga y render
    # ------------------------------------------------------------------

    @staticmethod
    def _fingerprint(packages: list) -> str:
        """Genera un hash corto de la lista de paquetes para detectar cambios."""
        items = []
        for p in packages:
            items.append(f"{p.get('id')}:{p.get('name')}:{p.get('price')}:{p.get('assigned')}")
        return hashlib.md5("|".join(sorted(items)).encode()).hexdigest()

    def refresh(self):
        self._clear()
        packages = api.get_packages()
        self._pkg_fingerprint = self._fingerprint(packages)
        self._build_grid(packages)
        if self._mode == "sale":
            self._poll_agent()

    def _build_grid(self, packages: list):
        if self._mode == "sale":
            self._build_sale_view(packages)
        else:
            self._build_config_view(packages)

    def _build_sale_view(self, packages: list):
        """Modo venta: grid de combos (cada tarjeta muestra nombre+duracion+valor)."""

        self._grid_frame = ctk.CTkFrame(self, fg_color="transparent")
        self._grid_frame.pack(fill="both", expand=True, padx=4, pady=(4, 0))
        self._grid_frame.grid_columnconfigure(0, weight=1)
        self._grid_frame.grid_columnconfigure(1, weight=1)

        for i, pkg in enumerate(packages):
            row, col = i // 2, i % 2
            self._grid_frame.grid_rowconfigure(row, weight=1)
            is_unassigned = pkg.get("custom") and pkg.get("assigned") is False
            card = PackageCard(
                self._grid_frame, pkg,
                on_click=self._on_card_click,
                disabled=is_unassigned,
                show_times=True,
            )
            card.grid(row=row, column=col, padx=7, pady=7, sticky="nsew")

        # Barra de acciones
        toolbar = ctk.CTkFrame(self, fg_color="transparent")
        toolbar.pack(fill="x", padx=4, pady=(2, 4))

        if session.tiene_permiso("lb_vender"):
            ctk.CTkButton(
                toolbar, text="🎁  Cortesia",
                width=150, height=32, corner_radius=8,
                fg_color="#1b5e20", hover_color="#2e7d32",
                font=ctk.CTkFont(size=12, weight="bold"),
                command=self._open_cortesia,
            ).pack(side="left", padx=4)

        ctk.CTkButton(
            toolbar, text="✕  Anulaciones",
            width=150, height=32, corner_radius=8,
            fg_color="#8b1a1a", hover_color="#a52525",
            font=ctk.CTkFont(size=12, weight="bold"),
            command=self._open_anulaciones,
        ).pack(side="left", padx=4)

        ctk.CTkButton(
            toolbar, text="⚙  Configuracion",
            width=150, height=32, corner_radius=8,
            fg_color="#1a3a5f", hover_color="#244b70",
            font=ctk.CTkFont(size=12, weight="bold"),
            command=self._open_configuraciones,
        ).pack(side="left", padx=4)

    def _build_config_view(self, packages: list):
        """Modo config: grid de combos editables + boton agregar."""
        self._grid_frame = ctk.CTkFrame(self, fg_color="transparent")
        self._grid_frame.pack(fill="both", expand=True, padx=4, pady=(4, 0))
        self._grid_frame.grid_columnconfigure(0, weight=1)
        self._grid_frame.grid_columnconfigure(1, weight=1)

        for i, pkg in enumerate(packages):
            row, col = i // 2, i % 2
            self._grid_frame.grid_rowconfigure(row, weight=1)
            card = PackageCard(
                self._grid_frame, pkg,
                on_click=self._on_card_click,
                on_edit=self._open_edit_combo,
                disabled=False,
            )
            card.grid(row=row, column=col, padx=7, pady=7, sticky="nsew")

        add_i   = len(packages)
        add_row = add_i // 2
        add_col = add_i % 2
        self._grid_frame.grid_rowconfigure(add_row, weight=1)
        AddCard(self._grid_frame, on_click=self._open_add_combo).grid(
            row=add_row, column=add_col, padx=7, pady=7, sticky="nsew"
        )

    # ------------------------------------------------------------------
    # Overlay de desconexion
    # ------------------------------------------------------------------

    def _show_offline_overlay(self):
        if self._overlay is not None:
            return

        self._overlay = ctk.CTkFrame(
            self, fg_color="#0a0d16", corner_radius=14,
            border_width=2, border_color="#f44336",
        )
        self._overlay.place(relx=0, rely=0, relwidth=1, relheight=1)
        self._overlay.lift()

        ctk.CTkLabel(self._overlay, text="⚠",
                     font=ctk.CTkFont(size=48), text_color="#f44336").pack(expand=True, pady=(40, 4))
        ctk.CTkLabel(self._overlay, text="Sin conexion con el agente",
                     font=ctk.CTkFont(size=18, weight="bold"),
                     text_color="#f44336").pack(pady=4)
        ctk.CTkLabel(self._overlay,
                     text="No se puede programar pulseras.\nVerifica que el agente (agent.py) este corriendo.",
                     font=ctk.CTkFont(size=13), text_color="#888888",
                     justify="center").pack(pady=8)
        self._btn_reconectar = ctk.CTkButton(
            self._overlay, text="Reconectar",
            width=160, height=40, corner_radius=8,
            fg_color="#1565c0", hover_color="#0d47a1",
            font=ctk.CTkFont(size=14, weight="bold"),
            command=self._reconnect,
        )
        self._btn_reconectar.pack(pady=16, expand=True)

    def _hide_offline_overlay(self):
        if self._overlay is not None:
            self._overlay.place_forget()
            self._overlay.destroy()
            self._overlay = None

    def _reconnect(self):
        if hasattr(self, "_btn_reconectar"):
            self._btn_reconectar.configure(text="Verificando...", state="disabled")
        self._poll_agent()

    # ------------------------------------------------------------------
    # Verificacion de conectividad
    # ------------------------------------------------------------------

    def _schedule_poll(self):
        self.after(_POLL_INTERVAL, self._poll_and_reschedule)

    def _poll_and_reschedule(self):
        self._poll_agent()
        self._poll_packages()
        self._schedule_poll()

    def _poll_agent(self):
        def _check():
            try:
                import httpx
                cfg   = api.load_config()
                agent = cfg.get("agent_url", "http://127.0.0.1:5555")
                r     = httpx.get(f"{agent}/status", timeout=3)
                online = r.status_code == 200
            except Exception:
                online = False
            self.after(0, lambda: self._apply_connectivity(online))

        threading.Thread(target=_check, daemon=True).start()

    def _poll_packages(self):
        """Verifica si los paquetes cambiaron en el servidor y refresca el grid."""
        def _check():
            try:
                packages = api.get_packages()
                fp = self._fingerprint(packages)
            except Exception:
                return
            if fp != self._pkg_fingerprint:
                self.after(0, lambda: self._apply_package_update(packages, fp))

        threading.Thread(target=_check, daemon=True).start()

    def _apply_package_update(self, packages: list, fingerprint: str):
        """Reconstruye el grid con los paquetes actualizados."""
        self._pkg_fingerprint = fingerprint
        # Preservar overlay si esta activo
        had_overlay = self._overlay is not None
        # Destruir grid viejo (NO el overlay)
        if self._grid_frame:
            self._grid_frame.destroy()
            self._grid_frame = None
        # Destruir toolbar viejo
        for w in self.winfo_children():
            if isinstance(w, ctk.CTkFrame) and w is not self._overlay:
                w.destroy()
        self._build_grid(packages)
        # Restaurar overlay si estaba activo
        if had_overlay and self._overlay is not None:
            self._overlay.lift()
        print(f"[App] Paquetes actualizados ({len(packages)} combos)", flush=True)

    def _apply_connectivity(self, online: bool):
        if online:
            self._agent_online = True
            self._hide_offline_overlay()
        else:
            self._agent_online = False
            self._show_offline_overlay()

    # ------------------------------------------------------------------
    # Acciones
    # ------------------------------------------------------------------

    def _on_card_click(self, pkg: dict):
        if self._mode == "config":
            # En modo config, click abre edicion (custom y server)
            self._open_edit_combo(pkg)
            return
        # Modo sale
        if pkg.get("custom") and pkg.get("assigned") is False:
            self._show_not_assigned()
            return
        self._open_sale(pkg)

    def _open_sale(self, pkg: dict):
        if not self._agent_online:
            return
        if not session.tiene_permiso("lb_vender"):
            self._show_no_permission()
            return
        import wristband_tracker
        stock_disp = api.get_stock_total() - len(wristband_tracker.get_active())
        if stock_disp <= 0:
            if session.tiene_permiso("lb_sobreventa"):
                # Requiere autorizacion de supervisor para continuar
                from ui.supervisor_pin_dialog import SupervisorPinDialog
                SupervisorPinDialog(
                    self.winfo_toplevel(),
                    reason="Sobreventa: no hay manillas disponibles en stock.",
                    accion="cortesia",
                    on_result=lambda ok: self._do_open_sale(pkg) if ok else None,
                )
            else:
                self._show_no_stock()
            return
        self._do_open_sale(pkg)

    def _do_open_sale(self, pkg: dict):
        from ui.sale_dialog import SaleDialog
        SaleDialog(
            self.winfo_toplevel(),
            package=pkg,
            cashier=self._cashier,
            on_sale_complete=self._handle_sale_complete,
        )

    def _handle_sale_complete(self, result: dict):
        if self.on_sale_complete:
            self.on_sale_complete(result)

    def _open_add_combo(self):
        from ui.add_combo_dialog import AddComboDialog
        AddComboDialog(
            self.winfo_toplevel(),
            on_saved=lambda _: self.refresh(),
        )

    def _open_edit_combo(self, combo: dict):
        from ui.add_combo_dialog import AddComboDialog
        AddComboDialog(
            self.winfo_toplevel(),
            on_saved=lambda _: self.refresh(),
            on_deleted=self.refresh,
            edit_combo=combo,
        )

    # ------------------------------------------------------------------
    # Cortesia
    # ------------------------------------------------------------------

    def _open_cortesia(self):
        if not self._agent_online:
            return
        from ui.supervisor_pin_dialog import SupervisorPinDialog
        SupervisorPinDialog(
            self.winfo_toplevel(),
            reason="Cortesia: otorgar pulsera gratuita",
            accion="cortesia",
            on_result=lambda r: self._do_cortesia(r) if r else None,
        )

    def _do_cortesia(self, voucher_data=None):
        try:
            from ui.cortesia_dialog import CortesiaDialog
            packages = api.get_packages()
            supervisor = ""
            motivo = ""
            if isinstance(voucher_data, dict):
                supervisor = voucher_data.get("supervisor", "")
                motivo = voucher_data.get("motivo", "")
            CortesiaDialog(
                self.winfo_toplevel(),
                packages=packages,
                cashier=self._cashier,
                supervisor=supervisor,
                motivo=motivo,
                on_done=lambda r: self.on_sale_complete(r) if self.on_sale_complete else None,
            )
        except Exception as e:
            print(f"[ERROR] _do_cortesia: {e}", flush=True)
            import traceback
            traceback.print_exc()

    # ------------------------------------------------------------------
    # Picker de pulseras activas (reutilizable)
    # ------------------------------------------------------------------

    def _pick_wristband(self, title: str, color: str, on_select: Callable):
        """Muestra un dialogo con las pulseras activas para seleccionar una."""
        import wristband_tracker
        activas = wristband_tracker.get_active()

        root = self.winfo_toplevel()
        cx = root.winfo_rootx() + root.winfo_width() // 2
        cy = root.winfo_rooty() + root.winfo_height() // 2

        if not activas:
            dlg = ctk.CTkToplevel(root)
            dlg.title(title)
            dlg.geometry(f"360x160+{cx - 180}+{cy - 80}")
            dlg.resizable(False, False)
            dlg.grab_set(); dlg.lift(); dlg.focus_force()
            dlg.bind("<Escape>", lambda e: dlg.destroy())
            ctk.CTkLabel(dlg, text="No hay pulseras activas",
                         font=ctk.CTkFont(size=15, weight="bold"),
                         text_color="#ff9800").pack(pady=(24, 6))
            ctk.CTkLabel(dlg, text="No hay pulseras en uso actualmente.",
                         font=ctk.CTkFont(size=12), text_color="#aaaaaa").pack(pady=4)
            ctk.CTkButton(dlg, text="Aceptar", width=110, height=34, corner_radius=6,
                          fg_color="#333333", hover_color="#444444",
                          command=dlg.destroy).pack(pady=16)
            return

        h = min(90 + len(activas) * 56, 480)
        dlg = ctk.CTkToplevel(root)
        dlg.title(title)
        dlg.geometry(f"460x{h}+{cx - 230}+{cy - h // 2}")
        dlg.resizable(False, False)
        dlg.grab_set(); dlg.lift(); dlg.focus_force()
        dlg.bind("<Escape>", lambda e: dlg.destroy())

        ctk.CTkLabel(dlg, text=title,
                     font=ctk.CTkFont(size=16, weight="bold"),
                     text_color=color).pack(pady=(14, 10))

        scroll = ctk.CTkScrollableFrame(dlg, fg_color="transparent")
        scroll.pack(fill="both", expand=True, padx=12, pady=(0, 12))

        for entry in activas:
            name = entry.get("name", "").strip() or "(sin nombre)"
            pkg  = entry.get("package_name", "")
            code = entry.get("code", "")
            ctk.CTkButton(
                scroll, text=f"  {name}   —   {pkg}   ·   {code}",
                font=ctk.CTkFont(size=13),
                fg_color="#0f1d32", hover_color="#152540",
                text_color="#e8edf5",
                height=46, corner_radius=8,
                anchor="w",
                command=lambda e=entry, d=dlg: [d.destroy(), on_select(e)],
            ).pack(fill="x", pady=3)

    # ------------------------------------------------------------------
    # Anulaciones
    # ------------------------------------------------------------------

    def _open_anulaciones(self):
        from ui.supervisor_pin_dialog import SupervisorPinDialog
        SupervisorPinDialog(
            self.winfo_toplevel(),
            reason="Anulacion: cancelar una venta activa",
            accion="anulacion",
            on_result=lambda r: self._pick_anulacion(r) if r else None,
        )

    def _pick_anulacion(self, voucher_data=None):
        self._anular_voucher = voucher_data
        self._pick_wristband(
            "Seleccionar pulsera para anular",
            "#f44336",
            self._do_anular,
        )

    def _do_anular(self, entry: dict):
        from ui.anular_dialog import AnularDialog
        supervisor = ""
        motivo = ""
        if isinstance(self._anular_voucher, dict):
            supervisor = self._anular_voucher.get("supervisor", "")
            motivo = self._anular_voucher.get("motivo", "")
        AnularDialog(
            self.winfo_toplevel(),
            entry=entry,
            cashier=self._cashier,
            supervisor=supervisor,
            motivo=motivo,
            on_done=lambda r: self.on_sale_complete(r) if self.on_sale_complete else None,
        )

    # ------------------------------------------------------------------
    # Reprogramaciones
    # ------------------------------------------------------------------

    def _open_reprogramaciones(self):
        if not self._agent_online:
            return
        from ui.supervisor_pin_dialog import SupervisorPinDialog
        SupervisorPinDialog(
            self.winfo_toplevel(),
            reason="Reprogramacion: cambiar tiempos de una pulsera",
            accion="reprogram",
            on_result=lambda ok: self._pick_reprogramacion() if ok else None,
        )

    def _pick_reprogramacion(self):
        self._pick_wristband(
            "Seleccionar pulsera para reprogramar",
            "#e6a800",
            self._do_reprogramar,
        )

    def _do_reprogramar(self, entry: dict):
        from ui.reprogramar_dialog import ReprogramarDialog
        ReprogramarDialog(
            self.winfo_toplevel(),
            entry=entry,
            cashier=self._cashier,
        )

    # ------------------------------------------------------------------
    # Configuraciones
    # ------------------------------------------------------------------

    def _open_configuraciones(self):
        from ui.supervisor_pin_dialog import SupervisorPinDialog
        SupervisorPinDialog(
            self.winfo_toplevel(),
            reason="Acceso a configuraciones del sistema",
            accion="cortesia",
            on_result=lambda ok: self._do_configuraciones() if ok else None,
        )

    def _do_configuraciones(self):
        from ui.config_dialog import ConfigDialog
        ConfigDialog(self.winfo_toplevel())

    # ------------------------------------------------------------------
    # Popups de error
    # ------------------------------------------------------------------

    def _show_not_assigned(self):
        dlg = ctk.CTkToplevel(self.winfo_toplevel())
        dlg.title("Combo sin asignar")
        dlg.geometry("380x180")
        dlg.resizable(False, False)
        dlg.grab_set(); dlg.lift(); dlg.focus_force()
        dlg.bind("<Escape>", lambda e: dlg.destroy())
        root = self.winfo_toplevel()
        dlg.geometry(f"+{root.winfo_rootx()+root.winfo_width()//2-190}"
                     f"+{root.winfo_rooty()+root.winfo_height()//2-90}")
        ctk.CTkLabel(dlg, text="Combo sin asignar",
                     font=ctk.CTkFont(size=15, weight="bold"),
                     text_color="#ff9800").pack(pady=(24, 6))
        ctk.CTkLabel(dlg,
                     text="Este combo necesita ser configurado\ndesde el Operator Panel.",
                     font=ctk.CTkFont(size=12), text_color="#aaaaaa",
                     justify="center").pack(pady=4)
        ctk.CTkButton(dlg, text="Aceptar", width=110, height=34, corner_radius=6,
                      fg_color="#333333", hover_color="#444444",
                      command=dlg.destroy).pack(pady=16)

    def _show_no_stock(self):
        dlg = ctk.CTkToplevel(self.winfo_toplevel())
        dlg.title("Sin manillas disponibles")
        dlg.geometry("340x180")
        dlg.resizable(False, False)
        dlg.grab_set(); dlg.lift(); dlg.focus_force()
        dlg.bind("<Escape>", lambda e: dlg.destroy())
        root = self.winfo_toplevel()
        dlg.geometry(f"+{root.winfo_rootx()+root.winfo_width()//2-170}"
                     f"+{root.winfo_rooty()+root.winfo_height()//2-90}")
        ctk.CTkLabel(dlg, text="Sin manillas disponibles",
                     font=ctk.CTkFont(size=15, weight="bold"),
                     text_color="#f44336").pack(pady=(24, 6))
        ctk.CTkLabel(dlg,
                     text="Todas las manillas están en uso.\nEspera a que se devuelva una.",
                     font=ctk.CTkFont(size=12), text_color="#aaaaaa",
                     justify="center").pack(pady=4)
        ctk.CTkButton(dlg, text="Aceptar", width=110, height=34, corner_radius=6,
                      fg_color="#333333", hover_color="#444444",
                      command=dlg.destroy).pack(pady=16)

    def _show_no_permission(self):
        dlg = ctk.CTkToplevel(self.winfo_toplevel())
        dlg.title("Sin permiso")
        dlg.geometry("340x160")
        dlg.resizable(False, False)
        dlg.grab_set(); dlg.lift(); dlg.focus_force()
        dlg.bind("<Escape>", lambda e: dlg.destroy())
        root = self.winfo_toplevel()
        dlg.geometry(f"+{root.winfo_rootx()+root.winfo_width()//2-170}"
                     f"+{root.winfo_rooty()+root.winfo_height()//2-80}")
        ctk.CTkLabel(dlg, text="Sin permiso para vender",
                     font=ctk.CTkFont(size=15, weight="bold"),
                     text_color="#f44336").pack(pady=(22, 6))
        ctk.CTkLabel(dlg,
                     text="Tu rol no incluye el permiso 'lb_vender'.\nContacta al administrador.",
                     font=ctk.CTkFont(size=12), text_color="#aaaaaa",
                     justify="center").pack(pady=4)
        ctk.CTkButton(dlg, text="Aceptar", width=110, height=34, corner_radius=6,
                      fg_color="#333333", hover_color="#444444",
                      command=dlg.destroy).pack(pady=12)
