import logging
import os
import queue
import threading
from concurrent.futures import ThreadPoolExecutor
from typing import Dict, List, Optional, Tuple

import customtkinter as ctk
import cv2
import numpy as np
from PIL import Image


# --- CONFIGURACIÓN Y ESTILOS ---
class Theme:
    BG_COLOR = "#101010"           # Fondo general un poco más oscuro
    ITEM_BG = "#3A3A3A"            # Fondo de cada tarjeta
    TEXT_MAIN = "#FFFFFF"
    TEXT_SEC = "#FDEF2F"
    ACCENT = "#F7F7F7"             # Dorado original
    ERROR = "#CF6679"              # Rojo suave para errores
    BTN_HOVER_DANGER = "#B00020"
    BTN_HOVER_NORMAL = "#4A4A4A"
    BORDER_COLOR = "#555555"

    FONT_MAIN = ("Roboto Medium", 13)
    FONT_SUB = ("Roboto", 11)
    FONT_MONO = ("Consolas", 10)


# Configuración de logging para debug
logging.basicConfig(level=logging.ERROR,
                    format='%(asctime)s - %(levelname)s - %(message)s')


class QueueItem:
    """Clase de datos para manejar el estado de cada archivo individualmente."""

    def __init__(self, path: str):
        self.path = path
        self.name = os.path.basename(path)
        self.info_text = "Cargando info..."
        self.thumbnail: Optional[ctk.CTkImage] = None
        self.is_loaded = False
        self.has_error = False
        self.id = id(self)  # Identificador único para referenciar widgets


class FileQueueManager(ctk.CTkScrollableFrame):
    def __init__(self, master, clear_icon=None, on_queue_empty_callback=None, **kwargs):
        super().__init__(master, fg_color=Theme.BG_COLOR, **kwargs)

        self.on_queue_empty_callback = on_queue_empty_callback
        self.clear_icon = clear_icon

        # Estado interno
        self.queue_items: List[QueueItem] = []
        # Mapa para acceder a widgets por ID de item
        self._widget_refs: Dict[int, dict] = {}

        # Factores de redimensionado
        self.upscale_factor = 1
        self.input_resize_factor = 0
        self.output_resize_factor = 0

        # Sistema de hilos para no congelar la GUI
        self.executor = ThreadPoolExecutor(max_workers=2)
        self.msg_queue = queue.Queue()

        # Grid layout básico
        self.grid_columnconfigure(0, weight=1)

        # Iniciar loop de verificación de mensajes (para actualizaciones desde hilos)
        self._check_queue()

    def _check_queue(self):
        """Revisa la cola de mensajes para actualizar la UI desde el hilo principal."""
        try:
            while True:
                task_type, item_id, data = self.msg_queue.get_nowait()
                if task_type == "UPDATE_ITEM":
                    self._update_item_ui(item_id, data)
        except queue.Empty:
            pass
        finally:
            self.after(100, self._check_queue)

    def add_files(self, file_paths: List[str]):
        """Agrega archivos y lanza la carga en segundo plano."""
        added_any = False
        existing_paths = {item.path for item in self.queue_items}

        for path in file_paths:
            norm_path = os.path.normpath(path)
            if norm_path not in existing_paths and os.path.exists(path):
                new_item = QueueItem(norm_path)
                self.queue_items.append(new_item)
                added_any = True

                # Renderizar placeholder inmediatamente
                self._render_item(new_item, index=len(self.queue_items)-1)

                # Lanzar tarea pesada en background
                self.executor.submit(self._worker_load_info, new_item)

        if added_any:
            self._update_layout_indices()

    def _worker_load_info(self, item: QueueItem):
        """Método que corre en otro hilo (Thread) para procesar OpenCV."""
        try:
            info_text, pil_img = self._extract_file_data(item.path)

            # Crear CTkImage debe hacerse preferiblemente al volver al main,
            # pero PIL Image es seguro pasarlo.
            item.info_text = info_text
            item.is_loaded = True

            # Enviamos resultados al hilo principal
            self.msg_queue.put(
                ("UPDATE_ITEM", item.id, {"info": info_text, "img": pil_img}))

        except Exception as e:
            logging.error(f"Error procesando {item.path}: {e}")
            item.has_error = True
            item.info_text = "Error al leer archivo"
            self.msg_queue.put(("UPDATE_ITEM", item.id, {"error": True}))

    def _update_item_ui(self, item_id, data):
        """Actualiza un widget específico una vez que el hilo terminó."""
        widgets = self._widget_refs.get(item_id)
        if not widgets:
            return

        if "error" in data:
            widgets['lbl_info'].configure(
                text="⚠️ Archivo corrupto o ilegible", text_color=Theme.ERROR)
            return

        # Actualizar texto
        widgets['lbl_info'].configure(text=data["info"])

        # Actualizar imagen si existe
        if data["img"]:
            ctk_img = ctk.CTkImage(data["img"], size=data["img"].size)
            # Guardamos referencia en el objeto item para que no se pierda
            item = next((x for x in self.queue_items if x.id == item_id), None)
            if item:
                item.thumbnail = ctk_img
                widgets['lbl_icon'].configure(
                    image=ctk_img, text="")  # Quitar texto placeholder

    def _render_item(self, item: QueueItem, index: int):
        """Dibuja una tarjeta para el archivo."""
        row_idx = index + 1  # +1 para dejar espacio al botón Clean arriba si fuera necesario

        # Frame contenedor (Tarjeta)
        card = ctk.CTkFrame(self, fg_color=Theme.ITEM_BG, corner_radius=8,
                            border_width=1, border_color=Theme.BORDER_COLOR)
        card.grid(row=row_idx, column=0, sticky="ew", padx=10, pady=5)
        card.grid_columnconfigure(1, weight=1)

        # 1. Icono / Thumbnail
        lbl_icon = ctk.CTkLabel(card, text="⏳", width=64,
                                height=64, fg_color="#222", corner_radius=6)
        lbl_icon.grid(row=0, column=0, rowspan=2, padx=8, pady=8)

        # Si ya teníamos la imagen (ej: reordenando lista), la ponemos directo
        if item.thumbnail:
            lbl_icon.configure(image=item.thumbnail, text="")

        # 2. Información
        lbl_name = ctk.CTkLabel(
            card, text=item.name, font=Theme.FONT_MAIN, text_color=Theme.ACCENT, anchor="w")
        lbl_name.grid(row=0, column=1, sticky="w", padx=5, pady=(8, 0))

        lbl_info = ctk.CTkLabel(card, text=item.info_text, font=Theme.FONT_MONO,
                                text_color=Theme.TEXT_SEC, justify="left", anchor="w")
        lbl_info.grid(row=1, column=1, sticky="w", padx=5, pady=(0, 8))

        # 3. Controles (Frame derecho)
        ctrl_frame = ctk.CTkFrame(card, fg_color="transparent")
        ctrl_frame.grid(row=0, column=2, rowspan=2, padx=8, sticky="e")

        # Botón Subir
        btn_up = ctk.CTkButton(ctrl_frame, text="▲", width=28, height=28,
                               fg_color="transparent", border_width=1, border_color=Theme.TEXT_SEC,
                               hover_color=Theme.BTN_HOVER_NORMAL, text_color=Theme.TEXT_MAIN,
                               font=("Arial", 12),
                               command=lambda: self.move_up(item))
        btn_up.pack(side="left", padx=2)

        # Botón Eliminar
        btn_del = ctk.CTkButton(ctrl_frame, text="✕", width=28, height=28,
                                fg_color="transparent", border_width=1, border_color=Theme.ERROR,
                                text_color=Theme.ERROR, hover_color=Theme.BTN_HOVER_DANGER,
                                font=("Arial", 12, "bold"),
                                command=lambda: self.remove_item(item))
        btn_del.pack(side="left", padx=2)

        # Guardar referencias para actualizaciones asíncronas
        self._widget_refs[item.id] = {
            "card": card,
            "lbl_info": lbl_info,
            "lbl_icon": lbl_icon,
            "btn_up": btn_up  # Guardamos por si queremos deshabilitar el primero
        }

        # Renderizar encabezado si es el primer elemento
        if index == 0:
            self._render_header()

    def _render_header(self):
        """Dibuja el botón de limpiar todo si hay elementos."""
        # Limpiar header anterior si existe
        for w in self.grid_slaves(row=0):
            w.destroy()

        if self.queue_items:
            header_frame = ctk.CTkFrame(self, fg_color="transparent")
            header_frame.grid(row=0, column=0, sticky="ew",
                              padx=10, pady=(5, 0))

            ctk.CTkLabel(header_frame, text=f"Queue: {len(self.queue_items)} Files",
                         font=Theme.FONT_SUB, text_color=Theme.TEXT_SEC).pack(side="left")

            ctk.CTkButton(header_frame, text="CLEAN ALL", image=self.clear_icon,
                          font=Theme.FONT_SUB, height=24, width=100,
                          fg_color="transparent", border_width=1, border_color=Theme.ACCENT, text_color=Theme.ACCENT,
                          hover_color=Theme.BTN_HOVER_NORMAL,
                          command=self.clean_file_list).pack(side="right")

    def _update_layout_indices(self):
        """Re-dibuja toda la lista. Útil para reordenar o borrar."""
        # Nota: En una app muy grande esto sería ineficiente, pero para <100 archivos está bien
        # destruir y recrear widgets garantiza orden visual correcto.

        # Limpiar UI actual
        for widget in self.winfo_children():
            widget.destroy()

        self._widget_refs.clear()

        # Redibujar
        if self.queue_items:
            self._render_header()
            for i, item in enumerate(self.queue_items):
                self._render_item(item, i)
        else:
            # Lista vacía
            if self.on_queue_empty_callback:
                self.on_queue_empty_callback()

    # --- LÓGICA DE DATOS ---

    def move_up(self, item: QueueItem):
        try:
            idx = self.queue_items.index(item)
            if idx > 0:
                self.queue_items[idx], self.queue_items[idx -
                                                        1] = self.queue_items[idx - 1], self.queue_items[idx]
                self._update_layout_indices()
        except ValueError:
            pass

    def remove_item(self, item: QueueItem):
        if item in self.queue_items:
            self.queue_items.remove(item)
            # Eliminar referencias para liberar memoria
            if item.id in self._widget_refs:
                del self._widget_refs[item.id]
            self._update_layout_indices()

    def clean_file_list(self):
        self.queue_items.clear()
        self._widget_refs.clear()
        self._update_layout_indices()

    def get_selected_file_list(self) -> List[str]:
        return [item.path for item in self.queue_items if not item.has_error]

    def regenerate_all_info(self):
        """Recalcula el texto de redimensionado sin recargar imágenes."""
        for item in self.queue_items:
            if not item.has_error and item.is_loaded:
                # Recalculamos solo el texto basado en lo que ya sabemos (o recargamos dims rápidas)
                # Para optimizar, asumimos que podemos volver a lanzar el worker o
                # simplemente recalcular si guardáramos width/height en el objeto.
                # Por simplicidad y robustez, relanzamos el worker ligero.
                self.executor.submit(self._worker_load_info, item)

    # --- SETTERS ---
    def set_upscale_factor(self, f):
        self.upscale_factor = f

    def set_input_resize_factor(self, f):
        self.input_resize_factor = f

    def set_output_resize_factor(self, f):
        self.output_resize_factor = f

    # --- UTILIDADES ESTÁTICAS (Lógica pura) ---

    def _extract_file_data(self, file_path) -> Tuple[str, Optional[Image.Image]]:
        """Extrae info y genera thumbnail PIL (Seguro para Threads)."""
        info_str = "Info no disponible"
        pil_image = None

        try:
            exts_video = ['.mp4', '.avi', '.mkv',
                          '.mov', '.wmv', '.flv', '.webm']
            is_video = any(file_path.lower().endswith(ext)
                           for ext in exts_video)

            width, height = 0, 0

            if is_video:
                cap = cv2.VideoCapture(file_path)
                if cap.isOpened():
                    width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
                    height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
                    frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
                    fps = cap.get(cv2.CAP_PROP_FPS)

                    # Leer frame central para thumbnail
                    try:
                        if frames > 100:
                            cap.set(cv2.CAP_PROP_POS_FRAMES, 50)
                        ret, frame = cap.read()
                        if ret:
                            frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
                            pil_image = self._process_thumbnail(frame)
                    except:
                        pass  # Fallo silencioso solo en thumbnail

                    dur = frames/fps if fps > 0 else 0
                    m, s = divmod(int(dur), 60)
                    info_str = f"Video: {m}m:{s:02d}s • {frames} frames • {width}x{height}\n"
                cap.release()
            else:
                # Imagen
                # cv2.imdecode es mejor para rutas con caracteres especiales en Windows
                img_array = np.fromfile(file_path, np.uint8)
                img = cv2.imdecode(img_array, cv2.IMREAD_COLOR)

                if img is not None:
                    height, width = img.shape[:2]
                    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
                    pil_image = self._process_thumbnail(img)
                    info_str = f"Image: {width}x{height}\n"

            # Añadir datos de redimensionado calculado
            if width > 0 and height > 0:
                info_str += self._calculate_resize_text(width, height)

        except Exception as e:
            logging.error(f"Error interno extract: {e}")
            raise e

        return info_str, pil_image

    def _process_thumbnail(self, img_array) -> Image.Image:
        """Redimensiona y recorta imagen para thumbnail."""
        h, w = img_array.shape[:2]
        target_size = 64

        # Mantener aspect ratio dentro de 64x64
        scale = target_size / max(h, w)
        new_w, new_h = int(w * scale), int(h * scale)

        resized = cv2.resize(img_array, (new_w, new_h),
                             interpolation=cv2.INTER_AREA)
        return Image.fromarray(resized)

    def _calculate_resize_text(self, w, h) -> str:
        """Lógica pura de cálculo de dimensiones."""
        if self.upscale_factor <= 0:
            return ""

        iw = int(w * (self.input_resize_factor/100)
                 ) if self.input_resize_factor else w
        ih = int(h * (self.input_resize_factor/100)
                 ) if self.input_resize_factor else h

        uw, uh = int(iw * self.upscale_factor), int(ih * self.upscale_factor)

        ow = int(uw * (self.output_resize_factor/100)
                 ) if self.output_resize_factor else uw
        oh = int(uh * (self.output_resize_factor/100)
                 ) if self.output_resize_factor else uh

        txt = ""
        if self.input_resize_factor:
            txt += f"Input AI ({int(self.input_resize_factor)}%) ➜ {iw}x{ih}\n"

        txt += f"Output AI (x{self.upscale_factor}) ➜ {uw}x{uh}"

        if self.output_resize_factor:
            txt += f"\nFinal Output ({int(self.output_resize_factor)}%) ➜ {ow}x{oh}"

        return txt
