"""Ventana principal: lista de juegos con scroll + panel de log."""

from __future__ import annotations

from pathlib import Path

from PySide6.QtCore import QEasingCurve, QPropertyAnimation, QThreadPool, Qt, QTimer
from PySide6.QtGui import QAction, QActionGroup, QPixmap
from PySide6.QtWidgets import (
    QApplication,
    QDialog,
    QDialogButtonBox,
    QHBoxLayout,
    QInputDialog,
    QLabel,
    QLineEdit,
    QMainWindow,
    QMessageBox,
    QPlainTextEdit,
    QProgressBar,
    QScrollArea,
    QVBoxLayout,
    QWidget,
)

from .. import __version__, gitops
from ..config import Config, Game
from ..paths import app_icon_path
from ..settings import load_settings, save_settings
from ..workers import CheckUpdatesWorker, DownloadWorker, RunWorker
from . import theme
from .game_row import GameRow

APP_NAME = "Jail Launcher"
WINDOW_TITLE = f"© 2026 {APP_NAME} — JailDesigner"

CONSOLE_HEIGHT = 150   # alçada de la consola desplegada (px)
CONSOLE_ANIM_MS = 220  # durada de l'animació de desplegar/replegar
CONSOLE_IDLE_MS = 4000  # marge sense activitat abans de replegar en mode auto

CONSOLE_SHOW = "show"
CONSOLE_AUTO = "auto"
CONSOLE_HIDE = "hide"


class MainWindow(QMainWindow):
    def __init__(self, config: Config, root: Path, parent=None) -> None:
        super().__init__(parent)
        self.config = config
        self.root = root
        self.settings = load_settings()
        self.pool = QThreadPool.globalInstance()
        self.rows: dict[str, GameRow] = {}
        self._delete_mode = False
        # Mantener referencias a los workers en vuelo: si no, Python los recolecta
        # (junto a su objeto de señales) antes de que la señal en cola `finished`
        # llegue al hilo principal, y la UI nunca se refresca.
        self._workers: set = set()

        self.setWindowTitle(WINDOW_TITLE)
        self.resize(720, 640)

        # Aplica el tema guardado (system/light/dark) i vigila els canvis del SO
        # només quan estem en mode 'system'.
        app = QApplication.instance()
        if app is not None:
            theme.apply_theme(app, self.settings.theme)
            theme.watch_system_theme(app, lambda: self.settings.theme == theme.THEME_SYSTEM)

        self._build_menu()

        central = QWidget()
        root_layout = QVBoxLayout(central)
        root_layout.setContentsMargins(0, 0, 0, 0)
        root_layout.setSpacing(0)

        # --- Lista de juegos con scroll ---
        list_container = QWidget()
        list_layout = QVBoxLayout(list_container)
        list_layout.setContentsMargins(6, 6, 6, 6)
        list_layout.setSpacing(6)
        for game in config.games:
            row = GameRow(game, root)
            row.activated.connect(self._on_activate)
            row.delete_requested.connect(self._on_delete)
            self.rows[game.id] = row
            list_layout.addWidget(row)
        list_layout.addStretch(1)

        scroll = QScrollArea()
        scroll.setWidgetResizable(True)
        scroll.setWidget(list_container)
        root_layout.addWidget(scroll, stretch=1)

        # --- Panel de log (consola colapsable amb alçada animada) ---
        self.log_view = QPlainTextEdit()
        self.log_view.setReadOnly(True)
        self.log_view.setMaximumBlockCount(5000)
        self.log_view.setStyleSheet("font-family: monospace; font-size: 11px;")
        self.log_view.setMinimumHeight(0)
        root_layout.addWidget(self.log_view)

        self.setCentralWidget(central)
        self._setup_console()

        # Barra de progrés (cantonada de la status bar) per a la comprovació d'updates;
        # amagada fins que arrenca una comprovació.
        self.progress = QProgressBar()
        self.progress.setMaximumWidth(220)
        self.progress.setFormat("Comprovant %v/%m")
        self.progress.hide()
        self.statusBar().addPermanentWidget(self.progress)

        # Estado persistido: marcas de update + filtro de ocultar no descargados.
        for game_id in self.settings.updates_pending:
            if game_id in self.rows:
                self.rows[game_id].set_update_available(True)
        self._apply_filter()

        # Comprobación automática de updates al iniciar (si está activada). Se difiere
        # con un timer 0 para que la ventana se muestre antes de lanzar el worker.
        if self.settings.check_updates_on_start:
            QTimer.singleShot(0, self._check_updates)

    def _net_config(self) -> gitops.NetConfig:
        """Construye la config de red/timeouts a partir de las preferencias guardadas."""
        s = self.settings
        return gitops.NetConfig(
            fetch_timeout=s.git_fetch_timeout,
            clone_timeout=s.git_clone_timeout,
            http_timeout=s.http_timeout,
            stall_limit=s.git_stall_limit,
            stall_time=s.git_stall_time,
        )

    # --------------------------------------------------------------- consola

    def _setup_console(self) -> None:
        """Prepara la animació d'alçada i l'estat inicial segons console_mode."""
        self._console_open = False
        self._console_anim_start = 0  # alçada de consola en arrencar l'animació
        self._console_win_start = 0   # alçada de finestra en arrencar l'animació
        self._console_grow_window = True
        self._console_anim = QPropertyAnimation(self.log_view, b"maximumHeight", self)
        self._console_anim.setDuration(CONSOLE_ANIM_MS)
        self._console_anim.setEasingCurve(QEasingCurve.InOutCubic)
        self._console_anim.valueChanged.connect(self._on_console_anim_value)
        self._console_anim.finished.connect(self._on_console_anim_done)

        # Timer que replega la consola en mode auto després d'un marge sense activitat.
        self._console_idle_timer = QTimer(self)
        self._console_idle_timer.setSingleShot(True)
        self._console_idle_timer.setInterval(CONSOLE_IDLE_MS)
        self._console_idle_timer.timeout.connect(self._on_console_idle)

        # Estat inicial sense animació: oberta només en mode "show".
        self.log_view.setMaximumHeight(0)
        self.log_view.hide()
        if self.settings.console_mode == CONSOLE_SHOW:
            self._set_console_open(True, animated=False)

    def _set_console_open(self, open_: bool, animated: bool = True) -> None:
        """Desplega/replega la consola fent créixer o encongir la finestra (perquè la
        consola guanyi espai en comptes de menjar-ne a la llista)."""
        if open_ == self._console_open:
            return
        self._console_open = open_
        start = self.log_view.maximumHeight()
        target = CONSOLE_HEIGHT if open_ else 0
        # Si la finestra està maximitzada/pantalla completa no la podem fer créixer:
        # caiem al comportament d'encongir la llista.
        grow = not (self.isMaximized() or self.isFullScreen())
        if open_:
            self.log_view.show()  # visible abans d'animar l'obertura
        self._console_anim.stop()
        if not animated:
            if grow:
                self.resize(self.width(), self.height() + (target - start))
            self.log_view.setMinimumHeight(target)
            self.log_view.setMaximumHeight(target)
            if not open_:
                self.log_view.hide()
            return
        self._console_grow_window = grow
        self._console_anim_start = start
        self._console_win_start = self.height()
        self._console_anim.setStartValue(start)
        self._console_anim.setEndValue(target)
        self._console_anim.start()

    def _on_console_anim_value(self, value: int) -> None:
        """A cada pas: fixem l'alçada de la consola a `value` (min=max, perquè agafi
        exactament aquest espai i no el cedeixi a la llista) i fem créixer/encongir la
        finestra el mateix, així la llista (finestra − consola) es manté constant."""
        self.log_view.setMinimumHeight(value)
        if self._console_grow_window:
            delta = value - self._console_anim_start
            self.resize(self.width(), self._console_win_start + delta)

    def _on_console_anim_done(self) -> None:
        if not self._console_open:
            self.log_view.hide()  # replegada del tot: treure-la del layout

    def _on_console_idle(self) -> None:
        if self.settings.console_mode == CONSOLE_AUTO and not self._workers:
            self._set_console_open(False)

    def _console_activity_started(self) -> None:
        """Hi ha activitat (worker o log): en mode auto, desplega i atura el timer."""
        if self.settings.console_mode == CONSOLE_AUTO:
            self._console_idle_timer.stop()
            self._set_console_open(True)

    def _console_activity_maybe_ended(self) -> None:
        """Si no queden workers actius, en mode auto arrenca el compte enrere per replegar."""
        if self.settings.console_mode == CONSOLE_AUTO and not self._workers:
            self._console_idle_timer.start()

    # --------------------------------------------------------------- menú

    def _build_menu(self) -> None:
        menu = self.menuBar().addMenu("Opcions")

        self.action_hide = QAction("Amaga els jocs no descarregats", self, checkable=True)
        self.action_hide.setChecked(self.settings.hide_not_downloaded)
        self.action_hide.toggled.connect(self._on_toggle_hide)
        menu.addAction(self.action_hide)

        self.action_check = QAction("Comprova actualitzacions", self)
        self.action_check.triggered.connect(self._check_updates)
        menu.addAction(self.action_check)

        self.action_check_on_start = QAction(
            "Comprova actualitzacions a l'inici", self, checkable=True
        )
        self.action_check_on_start.setChecked(self.settings.check_updates_on_start)
        self.action_check_on_start.toggled.connect(self._on_toggle_check_on_start)
        menu.addAction(self.action_check_on_start)

        menu.addSeparator()
        self._build_theme_menu(menu)
        self._build_console_menu(menu)

        menu.addSeparator()
        self.action_delete = QAction("Esborra un joc", self, checkable=True)
        self.action_delete.toggled.connect(self._set_delete_mode)
        menu.addAction(self.action_delete)

        menu.addSeparator()
        self.action_token = QAction("Configura el token de Gitea…", self)
        self.action_token.triggered.connect(self._configure_token)
        menu.addAction(self.action_token)

        self._build_help_menu()

    def _build_help_menu(self) -> None:
        """Menú Ajuda amb el «Quant a…». A macOS, AboutRole el mou al menú de l'app."""
        help_menu = self.menuBar().addMenu("Ajuda")
        self.action_about = QAction(f"Quant a {APP_NAME}…", self)
        self.action_about.setMenuRole(QAction.MenuRole.AboutRole)
        self.action_about.triggered.connect(self._show_about)
        help_menu.addAction(self.action_about)

    def _show_about(self) -> None:
        """Diàleg «Quant a» personalitzat: icona, nom gran i tot centrat."""
        dlg = QDialog(self)
        dlg.setWindowTitle(f"Quant a {APP_NAME}")
        dlg.setModal(True)

        lay = QVBoxLayout(dlg)
        lay.setContentsMargins(40, 30, 40, 24)
        lay.setSpacing(0)

        # Logo (si el trobem); s'escala suau des del PNG gran.
        icon_path = app_icon_path()
        if icon_path is not None:
            pix = QPixmap(str(icon_path))
            if not pix.isNull():
                logo = QLabel(alignment=Qt.AlignCenter)
                logo.setPixmap(
                    pix.scaled(
                        96, 96, Qt.KeepAspectRatio, Qt.SmoothTransformation
                    )
                )
                lay.addWidget(logo)
                lay.addSpacing(16)

        # Nom de l'app: gran, en negreta i amb el morat de la marca.
        name = QLabel(APP_NAME, alignment=Qt.AlignCenter)
        nf = name.font()
        nf.setPointSize(nf.pointSize() + 13)
        nf.setBold(True)
        name.setFont(nf)
        name.setStyleSheet("color: #7c4dff;")
        lay.addWidget(name)
        lay.addSpacing(4)

        # Versió en cursiva i atenuada.
        ver = QLabel(f"Versió {__version__}", alignment=Qt.AlignCenter)
        vf = ver.font()
        vf.setItalic(True)
        vf.setPointSize(vf.pointSize() + 1)
        ver.setFont(vf)
        ver.setStyleSheet("color: #8a8a8a;")
        lay.addWidget(ver)
        lay.addSpacing(18)

        # Lema discret.
        tag = QLabel("Clona, compila i juga · jailgames", alignment=Qt.AlignCenter)
        tag.setStyleSheet("color: #8a8a8a;")
        lay.addWidget(tag)
        lay.addSpacing(14)

        copy = QLabel("© 2026 JailDesigner", alignment=Qt.AlignCenter)
        cf = copy.font()
        cf.setPointSize(cf.pointSize() - 1)
        copy.setFont(cf)
        lay.addWidget(copy)
        lay.addSpacing(24)

        # Botó OK centrat.
        buttons = QDialogButtonBox(QDialogButtonBox.Ok)
        buttons.button(QDialogButtonBox.Ok).setText("D'acord")
        buttons.accepted.connect(dlg.accept)
        row = QHBoxLayout()
        row.addStretch(1)
        row.addWidget(buttons)
        row.addStretch(1)
        lay.addLayout(row)

        dlg.exec()

    def _build_theme_menu(self, parent_menu) -> None:
        """Submenú Tema amb tres opcions exclusives: Sistema / Clar / Fosc."""
        submenu = parent_menu.addMenu("Tema")
        group = QActionGroup(self)
        group.setExclusive(True)
        options = [
            ("Sistema", theme.THEME_SYSTEM),
            ("Clar", theme.THEME_LIGHT),
            ("Fosc", theme.THEME_DARK),
        ]
        for label, mode in options:
            action = QAction(label, self, checkable=True)
            action.setChecked(self.settings.theme == mode)
            action.triggered.connect(lambda _checked, m=mode: self._on_theme_selected(m))
            group.addAction(action)
            submenu.addAction(action)

    def _build_console_menu(self, parent_menu) -> None:
        """Submenú Consola amb tres estats exclusius: Mostra / Auto-amaga / Amaga."""
        submenu = parent_menu.addMenu("Consola")
        group = QActionGroup(self)
        group.setExclusive(True)
        options = [
            ("Mostra", CONSOLE_SHOW),
            ("Auto-amaga", CONSOLE_AUTO),
            ("Amaga", CONSOLE_HIDE),
        ]
        for label, mode in options:
            action = QAction(label, self, checkable=True)
            action.setChecked(self.settings.console_mode == mode)
            action.triggered.connect(
                lambda _checked, m=mode: self._on_console_mode_selected(m)
            )
            group.addAction(action)
            submenu.addAction(action)

    def _on_console_mode_selected(self, mode: str) -> None:
        self.settings.console_mode = mode
        save_settings(self.settings)
        self._console_idle_timer.stop()
        if mode == CONSOLE_SHOW:
            self._set_console_open(True)
        elif mode == CONSOLE_HIDE:
            self._set_console_open(False)
        else:  # auto: oberta si hi ha activitat, si no replegada
            self._set_console_open(bool(self._workers))

    def _on_theme_selected(self, mode: str) -> None:
        self.settings.theme = mode
        save_settings(self.settings)
        app = QApplication.instance()
        if app is not None:
            theme.apply_theme(app, mode)
        # Reconstruir las filas: los pills usan `palette(...)` en su stylesheet, que
        # Qt cachea; recrearlos los re-resuelve contra la paleta ya aplicada.
        for row in self.rows.values():
            row.refresh()

    def _configure_token(self) -> None:
        token, ok = QInputDialog.getText(
            self,
            "Token de Gitea",
            "Token personal d'accés (per a repos privats).\n"
            "Es guarda local a settings.json (no es versiona).",
            QLineEdit.Password,
            self.settings.gitea_token,
        )
        if not ok:
            return
        self.settings.gitea_token = token.strip()
        save_settings(self.settings)
        estat = "configurat" if self.settings.gitea_token else "esborrat"
        self._log(f"Token de Gitea {estat}.")

    def _on_toggle_hide(self, checked: bool) -> None:
        self.settings.hide_not_downloaded = checked
        save_settings(self.settings)
        self._apply_filter()

    def _on_toggle_check_on_start(self, checked: bool) -> None:
        self.settings.check_updates_on_start = checked
        save_settings(self.settings)

    def _apply_filter(self) -> None:
        hide = self.action_hide.isChecked()
        for row in self.rows.values():
            row.setVisible(not (hide and not row.is_installed()))

    # ------------------------------------------------------ comprobar updates

    def _check_updates(self) -> None:
        self.action_check.setEnabled(False)
        self._log("=== Comprovant actualitzacions ===")
        worker = CheckUpdatesWorker(
            self.root, self.config.games, self.settings.gitea_token, self._net_config()
        )
        worker.signals.log.connect(self._log)
        worker.signals.result.connect(self._mark_update)
        worker.signals.progress.connect(self._check_progress)
        worker.signals.finished.connect(self._check_done)
        worker.signals.error.connect(self._check_error)
        self._track(worker)
        self.pool.start(worker)

    def _check_progress(self, done: int, total: int) -> None:
        if total <= 0:
            return  # res descarregat a comprovar: no mostrem barra
        self.progress.setMaximum(total)
        self.progress.setValue(done)
        self.progress.show()

    def _mark_update(self, game_id: str, has_update: bool) -> None:
        row = self.rows.get(game_id)
        if row is not None:
            row.set_update_available(has_update)
        pending = set(self.settings.updates_pending)
        pending.add(game_id) if has_update else pending.discard(game_id)
        self.settings.updates_pending = sorted(pending)
        save_settings(self.settings)

    def _check_done(self, _payload) -> None:
        self.action_check.setEnabled(True)
        self.progress.hide()
        self._log("=== Comprovació d'actualitzacions acabada ===")

    def _check_error(self, msg: str) -> None:
        self.action_check.setEnabled(True)
        self.progress.hide()
        self._log(f"!!! Error comprovant actualitzacions: {msg}")

    # --------------------------------------------------------------- helpers

    def _log(self, text: str) -> None:
        self.log_view.appendPlainText(text)
        # En mode auto, qualsevol línia desplega la consola; si no hi ha cap worker
        # actiu (p.ex. un missatge solt), arrenca el compte enrere per replegar-la.
        if self.settings.console_mode == CONSOLE_AUTO:
            self._console_activity_started()
            if not self._workers:
                self._console_idle_timer.start()

    def _track(self, worker) -> None:
        """Retiene el worker hasta que emite finished/error, evitando que el GC
        se lleve su objeto de señales antes de entregar la señal en cola."""
        worker.setAutoDelete(False)
        self._workers.add(worker)
        self._console_activity_started()

        def _done(*_):
            self._workers.discard(worker)
            self._console_activity_maybe_ended()

        worker.signals.finished.connect(_done)
        worker.signals.error.connect(_done)

    # --------------------------------------------------------------- accions

    def _set_delete_mode(self, on: bool) -> None:
        self._delete_mode = on
        for row in self.rows.values():
            row.set_delete_mode(on)

    def _on_activate(self, game: Game) -> None:
        """Clic sobre la fila. En mode esborrar elimina; si no, descarrega/actualitza o juga."""
        row = self.rows[game.id]
        if self._delete_mode:
            if not row.is_installed():
                return  # res a esborrar; segueix en mode esborrar
            if self._on_delete(game):
                self.action_delete.setChecked(False)  # surt del mode esborrar
            return
        if row.primary_action_is_download():
            self._on_download(game)
        else:
            self._on_run(game)

    def _on_download(self, game: Game) -> None:
        row = self.rows[game.id]
        row.set_busy(True, "Descarregant…")
        self._log(f"=== Descàrrega: {game.name} ===")

        worker = DownloadWorker(
            self.root, game, self.settings.gitea_token, self._net_config()
        )
        worker.signals.log.connect(self._log)
        worker.signals.finished.connect(lambda _meta, g=game: self._download_done(g))
        worker.signals.error.connect(lambda msg, g=game: self._op_error(g, msg))
        self._track(worker)
        self.pool.start(worker)

    def _download_done(self, game: Game) -> None:
        self._log(f"=== {game.name}: descàrrega completada ===")
        row = self.rows[game.id]
        row.set_busy(False)
        row.set_update_available(False)  # recién traído del remoto → al día
        if game.id in self.settings.updates_pending:
            self.settings.updates_pending = [
                g for g in self.settings.updates_pending if g != game.id
            ]
            save_settings(self.settings)
        self._apply_filter()  # un juego antes no instalado puede aparecer ahora

    def _on_run(self, game: Game) -> None:
        row = self.rows[game.id]
        row.set_busy(True, "Executant…")
        self._log(f"=== Juga: {game.name} ===")

        worker = RunWorker(self.root, game)
        worker.signals.log.connect(self._log)
        worker.signals.finished.connect(lambda code, g=game: self._run_done(g, code))
        worker.signals.error.connect(lambda msg, g=game: self._op_error(g, msg))
        self._track(worker)
        self.pool.start(worker)

    def _run_done(self, game: Game, code: int) -> None:
        self._log(f"=== {game.name}: ha finalitzat amb codi {code} ===")
        row = self.rows[game.id]
        row.set_busy(False)
        row.refresh()

    def _op_error(self, game: Game, msg: str) -> None:
        self._log(f"!!! {game.name}: ERROR: {msg}")
        row = self.rows[game.id]
        row.set_busy(False)
        row.refresh()

    def _on_delete(self, game: Game) -> bool:
        """Esborra la descàrrega local (amb confirmació). Retorna True si s'ha esborrat."""
        resp = QMessageBox.question(
            self,
            "Esborrar descàrrega",
            f"Segur que vols esborrar la descàrrega local de «{game.name}»?\n\n"
            "S'eliminarà el clon i les dades en local (no es treu de la llista).",
            QMessageBox.Yes | QMessageBox.No,
            QMessageBox.No,
        )
        if resp != QMessageBox.Yes:
            return False
        self._log(f"=== Esborra: {game.name} ===")
        gitops.delete_local(self.root, game, log=self._log)
        if game.id in self.settings.updates_pending:
            self.settings.updates_pending = [
                g for g in self.settings.updates_pending if g != game.id
            ]
            save_settings(self.settings)
        row = self.rows[game.id]
        row.set_update_available(False)
        row.refresh()
        self._apply_filter()  # si està actiu "amaga no descarregats", ara s'amaga
        return True
