from __future__ import annotations

import os
import xml.etree.ElementTree as ET
from typing import TYPE_CHECKING, Callable

from bec_lib.logger import bec_logger
from qtpy.QtCore import Qt, Signal  # type: ignore
from qtpy.QtGui import QFontMetrics, QPainter, QPainterPath, QPixmap
from qtpy.QtWidgets import (
    QApplication,
    QComboBox,
    QFileDialog,
    QHBoxLayout,
    QLabel,
    QPushButton,
    QSizePolicy,
    QSpacerItem,
    QWidget,
)

import bec_widgets
from bec_widgets.cli.rpc.rpc_register import RPCRegister
from bec_widgets.utils.bec_plugin_helper import get_all_plugin_widgets
from bec_widgets.utils.container_utils import WidgetContainerUtils
from bec_widgets.utils.error_popups import SafeSlot
from bec_widgets.utils.name_utils import pascal_to_snake
from bec_widgets.utils.plugin_utils import get_plugin_auto_updates
from bec_widgets.utils.round_frame import RoundedFrame
from bec_widgets.utils.screen_utils import apply_window_geometry, centered_geometry_for_app
from bec_widgets.utils.toolbars.toolbar import ModularToolBar
from bec_widgets.utils.ui_loader import UILoader
from bec_widgets.widgets.containers.auto_update.auto_updates import AutoUpdates
from bec_widgets.widgets.containers.dock_area.dock_area import BECDockArea
from bec_widgets.widgets.containers.dock_area.profile_utils import get_last_profile, list_profiles
from bec_widgets.widgets.containers.main_window.main_window import BECMainWindow, BECMainWindowNoRPC
from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import DarkModeButton

if TYPE_CHECKING:  # pragma: no cover
    from qtpy.QtCore import QObject

    from bec_widgets.utils.bec_widget import BECWidget

logger = bec_logger.logger
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
START_EMPTY_PROFILE_OPTION = "Start Empty (No Profile)"


class LaunchTile(RoundedFrame):
    DEFAULT_SIZE = (250, 300)
    open_signal = Signal()

    def __init__(
        self,
        parent: QObject | None = None,
        icon_path: str | None = None,
        top_label: str | None = None,
        main_label: str | None = None,
        description: str | None = None,
        show_selector: bool = False,
        tile_size: tuple[int, int] | None = None,
    ):
        super().__init__(parent=parent, orientation="vertical")

        # Provide a per‑instance TILE_SIZE so the class can compute layout
        if tile_size is None:
            tile_size = self.DEFAULT_SIZE
        self.tile_size = tile_size

        self.icon_label = QLabel(parent=self)
        self.icon_label.setFixedSize(100, 100)
        self.icon_label.setScaledContents(True)
        pixmap = QPixmap(icon_path)
        if not pixmap.isNull():
            size = 100
            circular_pixmap = QPixmap(size, size)
            circular_pixmap.fill(Qt.transparent)

            painter = QPainter(circular_pixmap)
            painter.setRenderHints(QPainter.RenderHint.Antialiasing, True)
            path = QPainterPath()
            path.addEllipse(0, 0, size, size)
            painter.setClipPath(path)
            pixmap = pixmap.scaled(
                size,
                size,
                Qt.AspectRatioMode.KeepAspectRatio,
                Qt.TransformationMode.SmoothTransformation,
            )
            painter.drawPixmap(0, 0, pixmap)
            painter.end()

            self.icon_label.setPixmap(circular_pixmap)
        self.layout.addWidget(self.icon_label, alignment=Qt.AlignmentFlag.AlignCenter)

        # Top label
        self.top_label = QLabel(top_label.upper())
        font_top = self.top_label.font()
        font_top.setPointSize(10)
        self.top_label.setFont(font_top)
        self.layout.addWidget(self.top_label, alignment=Qt.AlignmentFlag.AlignCenter)

        # Main label
        self.main_label = QLabel(main_label)

        # Desired default appearance
        font_main = self.main_label.font()
        font_main.setPointSize(14)
        font_main.setBold(True)
        self.main_label.setFont(font_main)
        self.main_label.setAlignment(Qt.AlignmentFlag.AlignCenter)

        # Shrink font if the default would wrap on this platform / DPI
        content_width = (
            self.tile_size[0]
            - self.layout.contentsMargins().left()
            - self.layout.contentsMargins().right()
        )
        self._fit_label_to_width(self.main_label, content_width)

        # Give every tile the same reserved height for the title so the
        # description labels start at an identical y‑offset.
        self.main_label.setFixedHeight(QFontMetrics(self.main_label.font()).height() + 2)

        self.layout.addWidget(self.main_label)

        self.spacer_top = QSpacerItem(0, 10, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
        self.layout.addItem(self.spacer_top)

        # Description
        self.description_label = QLabel(description)
        self.description_label.setWordWrap(True)
        self.description_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.layout.addWidget(self.description_label)

        # Selector
        if show_selector:
            self.selector = QComboBox(self)
            self.layout.addWidget(self.selector)
        else:
            self.selector = None

        self.spacer_bottom = QSpacerItem(
            0, 0, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Expanding
        )
        self.layout.addItem(self.spacer_bottom)

        # Action button
        self.action_button = QPushButton("Open")
        self.action_button.setStyleSheet("""
        QPushButton {
            background-color: #007AFF;
            border: none;
            padding: 8px 16px;
            color: white;
            border-radius: 6px;
            font-weight: bold;
        }
        QPushButton:hover {
            background-color: #005BB5;
        }
        """)
        self.layout.addWidget(self.action_button, alignment=Qt.AlignmentFlag.AlignCenter)

    def _fit_label_to_width(self, label: QLabel, max_width: int, min_pt: int = 10):
        """
        Fit the label text to the specified maximum width by adjusting the font size.

        Args:
            label(QLabel): The label to adjust.
            max_width(int): The maximum width the label can occupy.
            min_pt(int): The minimum font point size to use.
        """
        font = label.font()
        for pt in range(font.pointSize(), min_pt - 1, -1):
            font.setPointSize(pt)
            metrics = QFontMetrics(font)
            if metrics.horizontalAdvance(label.text()) <= max_width:
                label.setFont(font)
                label.setWordWrap(False)
                return
        # If nothing fits, fall back to eliding
        metrics = QFontMetrics(font)
        label.setFont(font)
        label.setWordWrap(False)
        label.setText(metrics.elidedText(label.text(), Qt.TextElideMode.ElideRight, max_width))


class LaunchWindow(BECMainWindow):
    RPC = True
    PLUGIN = False
    TILE_SIZE = (250, 300)
    DEFAULT_LAUNCH_SIZE = (800, 600)
    USER_ACCESS = ["show_launcher", "hide_launcher"]

    def __init__(
        self,
        parent=None,
        gui_id: str = None,
        window_title="BEC Launcher",
        launch_gui_class: str = None,
        launch_gui_id: str = None,
        *args,
        **kwargs,
    ):
        super().__init__(parent=parent, gui_id=gui_id, window_title=window_title, **kwargs)

        self.app = QApplication.instance()
        self.tiles: dict[str, LaunchTile] = {}
        # Track the smallest main‑label font size chosen so far
        self._min_main_label_pt: int | None = None

        # Toolbar
        self.dark_mode_button = DarkModeButton(parent=self, toolbar=True)
        self.toolbar = ModularToolBar(parent=self)
        self.addToolBar(Qt.TopToolBarArea, self.toolbar)
        self.spacer = QWidget(self)
        self.spacer.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
        self.toolbar.addWidget(self.spacer)
        self.toolbar.addWidget(self.dark_mode_button)

        # Main Widget
        self.central_widget = QWidget(self)
        self.central_widget.layout = QHBoxLayout(self.central_widget)
        self.setCentralWidget(self.central_widget)

        self.register_tile(
            name="dock_area",
            icon_path=os.path.join(MODULE_PATH, "assets", "app_icons", "bec_widgets_icon.png"),
            top_label="Get started",
            main_label="BEC Advanced Dock Area",
            description="Flexible application for managing modular widgets and user profiles.",
            action_button=self._open_dock_area,
            show_selector=True,
            selector_items=list_profiles("bec"),
        )
        self._refresh_dock_area_profiles(preserve_selection=False)

        self.available_auto_updates: dict[str, type[AutoUpdates]] = (
            self._update_available_auto_updates()
        )
        self.register_tile(
            name="auto_update",
            icon_path=os.path.join(MODULE_PATH, "assets", "app_icons", "auto_update.png"),
            top_label="Get automated",
            main_label="BEC Auto Update Dock Area",
            description="Dock area with auto update functionality for BEC widgets plotting.",
            action_button=self._open_auto_update,
            show_selector=True,
            selector_items=list(self.available_auto_updates.keys()) + ["Default"],
        )

        self.register_tile(
            name="custom_ui_file",
            icon_path=os.path.join(MODULE_PATH, "assets", "app_icons", "ui_loader_tile.png"),
            top_label="Get customized",
            main_label="Launch Custom UI File",
            description="GUI application with custom UI file.",
            action_button=self._open_custom_ui_file,
            show_selector=False,
        )

        # plugin widgets
        self.available_widgets: dict[str, type[BECWidget]] = get_all_plugin_widgets().as_dict()
        if self.available_widgets:
            plugin_repo_name = next(iter(self.available_widgets.values())).__module__.split(".")[0]
            plugin_repo_name = plugin_repo_name.removesuffix("_bec").upper()
            self.register_tile(
                name="widget",
                icon_path=os.path.join(
                    MODULE_PATH, "assets", "app_icons", "widget_launch_tile.png"
                ),
                top_label="Get quickly started",
                main_label=f"Launch a {plugin_repo_name} Widget",
                description=f"GUI application with one widget from the {plugin_repo_name} repository.",
                action_button=self._open_widget,
                show_selector=True,
                selector_items=list(self.available_widgets.keys()),
            )

        self._update_theme()

        self.register = RPCRegister()
        self.register.callbacks.append(self._turn_off_the_lights)
        self.register.broadcast()

        if launch_gui_class and launch_gui_id:
            # If a specific gui class is provided, launch it and hide the launcher
            self.launch(launch_gui_class, name=launch_gui_id)
            self.hide()

    def register_tile(
        self,
        name: str,
        icon_path: str | None = None,
        top_label: str | None = None,
        main_label: str | None = None,
        description: str | None = None,
        action_button: Callable | None = None,
        show_selector: bool = False,
        selector_items: list[str] | None = None,
    ):
        """
        Register a tile in the launcher window.

        Args:
            name(str): The name of the tile.
            icon_path(str): The path to the icon.
            top_label(str): The top label of the tile.
            main_label(str): The main label of the tile.
            description(str): The description of the tile.
            action_button(callable): The action to be performed when the button is clicked.
            show_selector(bool): Whether to show a selector or not.
            selector_items(list[str]): The items to be shown in the selector.
        """

        tile = LaunchTile(
            icon_path=icon_path,
            top_label=top_label,
            main_label=main_label,
            description=description,
            show_selector=show_selector,
            tile_size=self.TILE_SIZE,
        )
        tile.setFixedWidth(self.TILE_SIZE[0])
        tile.setMinimumHeight(self.TILE_SIZE[1])
        tile.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.MinimumExpanding)
        if action_button:
            tile.action_button.clicked.connect(action_button)
        if show_selector and selector_items:
            tile.selector.addItems(selector_items)
        self.central_widget.layout.addWidget(tile)

        # keep all tiles' main labels at a unified point size
        current_pt = tile.main_label.font().pointSize()
        if self._min_main_label_pt is None or current_pt < self._min_main_label_pt:
            # New global minimum – shrink every existing tile to this size
            self._min_main_label_pt = current_pt
            for t in self.tiles.values():
                f = t.main_label.font()
                f.setPointSize(self._min_main_label_pt)
                t.main_label.setFont(f)
                t.main_label.setFixedHeight(QFontMetrics(f).height() + 2)
        elif current_pt > self._min_main_label_pt:
            # Tile is larger than global minimum – shrink it to match
            f = tile.main_label.font()
            f.setPointSize(self._min_main_label_pt)
            tile.main_label.setFont(f)
            tile.main_label.setFixedHeight(QFontMetrics(f).height() + 2)

        self.tiles[name] = tile

    def _refresh_dock_area_profiles(self, preserve_selection: bool = True) -> None:
        """
        Refresh the dock-area profile selector, optionally preserving the selection.
        Defaults to Start Empty when no valid selection can be preserved.

        Args:
            preserve_selection(bool): Whether to preserve the current selection or not.
        """
        tile = self.tiles.get("dock_area")
        if tile is None or tile.selector is None:
            return

        selector = tile.selector
        selected_text = (
            selector.currentText().strip() if preserve_selection and selector.count() > 0 else ""
        )

        profiles = list_profiles("bec")
        selector_items = [START_EMPTY_PROFILE_OPTION, *profiles]
        selector.blockSignals(True)
        selector.clear()
        for profile in selector_items:
            selector.addItem(profile)

        if selected_text:
            # Try to preserve the current selection
            idx = selector.findText(selected_text, Qt.MatchFlag.MatchExactly)
            if idx >= 0:
                selector.setCurrentIndex(idx)
            else:
                # Selection no longer exists, fall back to default startup selection.
                self._set_selector_to_default_profile(selector, profiles)
        else:
            # No selection to preserve, use default startup selection.
            self._set_selector_to_default_profile(selector, profiles)
        selector.blockSignals(False)

    def _set_selector_to_default_profile(self, selector: QComboBox, profiles: list[str]) -> None:
        """
        Set the selector default.

        Preference order:
            1) Start Empty option (if available)
            2) Last used profile
            3) First available profile

        Args:
            selector(QComboBox): The combobox to set.
            profiles(list[str]): List of available profiles.
        """
        start_empty_idx = selector.findText(START_EMPTY_PROFILE_OPTION, Qt.MatchFlag.MatchExactly)
        if start_empty_idx >= 0:
            selector.setCurrentIndex(start_empty_idx)
            return

        # Try to get last used profile
        last_profile = get_last_profile(namespace="bec")
        if last_profile and last_profile in profiles:
            idx = selector.findText(last_profile, Qt.MatchFlag.MatchExactly)
            if idx >= 0:
                selector.setCurrentIndex(idx)
                return

        # If nothing else, select first item
        if selector.count() > 0:
            selector.setCurrentIndex(0)

    def launch(
        self,
        launch_script: str,
        name: str | None = None,
        geometry: tuple[int, int, int, int] | None = None,
        **kwargs,
    ) -> QWidget | None:
        """Launch the specified script. If the launch script creates a QWidget, it will be
        embedded in a BECMainWindow. If the launch script creates a BECMainWindow, it will be shown
        as a separate window.

        Args:
            launch_script(str): The name of the script to be launched.
            name(str): The name of the dock area.
            geometry(tuple): The geometry parameters to be passed to the dock area.
        Returns:
            QWidget: The created dock area.
        """
        from bec_widgets.applications import bw_launch

        with RPCRegister.delayed_broadcast() as rpc_register:
            if geometry is None and launch_script != "custom_ui_file":
                geometry = self._default_launch_geometry()
            existing_dock_areas = rpc_register.get_names_of_rpc_by_class_type(BECDockArea)
            if name is not None:
                WidgetContainerUtils.raise_for_invalid_name(name)
                # If name already exists, generate a unique one with counter suffix
                if name in existing_dock_areas:
                    name = WidgetContainerUtils.generate_unique_name(name, existing_dock_areas)
            else:
                name = "dock_area"
                name = WidgetContainerUtils.generate_unique_name(name, existing_dock_areas)

            if launch_script is None:
                launch_script = "dock_area"
            if not isinstance(launch_script, str):
                raise ValueError(f"Launch script must be a string, but got {type(launch_script)}.")

            if launch_script == "custom_ui_file":
                ui_file = kwargs.pop("ui_file", None)
                if not ui_file:
                    return None
                return self._launch_custom_ui_file(ui_file)

            if launch_script == "auto_update":
                auto_update = kwargs.pop("auto_update", None)
                return self._launch_auto_update(auto_update, geometry=geometry)

            if launch_script == "widget":
                widget = kwargs.pop("widget", None)
                if widget is None:
                    raise ValueError("Widget name must be provided.")
                return self._launch_widget(widget, geometry=geometry)

            launch = getattr(bw_launch, launch_script, None)
            if launch is None:
                raise ValueError(f"Launch script {launch_script} not found.")

            result_widget = launch(name, **kwargs)
            # TODO Should we simply use the specified name as title here?
            result_widget.window().setWindowTitle(f"BEC - {name}")
            logger.info(f"Created new dock area: {name}")

            if isinstance(result_widget, BECMainWindow):
                apply_window_geometry(result_widget, geometry)
                result_widget.show()
            else:
                window = BECMainWindowNoRPC()
                window.setCentralWidget(result_widget)
                window.setWindowTitle(f"BEC - {result_widget.objectName()}")
                apply_window_geometry(window, geometry)
                window.show()
            return result_widget

    def _launch_custom_ui_file(self, ui_file: str | None) -> BECMainWindow:
        """
        Load a custom .ui file. If the top-level widget is a MainWindow subclass,
        instantiate it directly; otherwise, embed it in a UILaunchWindow.
        """
        if ui_file is None:
            raise ValueError("UI file must be provided for custom UI file launch.")
        filename = os.path.basename(ui_file).split(".")[0]

        WidgetContainerUtils.raise_for_invalid_name(filename)

        # Parse the UI to detect top-level widget class
        tree = ET.parse(ui_file)
        root = tree.getroot()
        # Check if the top-level widget is a QMainWindow
        widget = root.find("widget")
        if widget is None:
            raise ValueError("No widget found in the UI file.")

        # Load the UI into a widget
        loader = UILoader(None)
        loaded = loader.loader(ui_file)

        # Display the UI in a BECMainWindow
        if isinstance(loaded, BECMainWindow):
            window = loaded
            window.object_name = filename
        else:
            window = BECMainWindow(object_name=filename)
            window.setCentralWidget(loaded)

        window.setWindowTitle(f"BEC - {filename}")
        apply_window_geometry(window, None)
        window.show()
        logger.info(f"Launched custom UI: {filename}, type: {type(window).__name__}")
        return window

    def _launch_auto_update(
        self, auto_update: str, geometry: tuple[int, int, int, int] | None = None
    ) -> AutoUpdates:
        if auto_update in self.available_auto_updates:
            auto_update_cls = self.available_auto_updates[auto_update]
            window = auto_update_cls()
        else:

            auto_update = "auto_updates"
            window = AutoUpdates()

        window.resize(window.minimumSizeHint())
        window.setWindowTitle(f"BEC - {window.objectName()}")
        apply_window_geometry(window, geometry)
        window.show()
        return window

    def _launch_widget(
        self, widget: type[BECWidget], geometry: tuple[int, int, int, int] | None = None
    ) -> QWidget:
        name = pascal_to_snake(widget.__name__)

        WidgetContainerUtils.raise_for_invalid_name(name)

        window = BECMainWindowNoRPC()

        widget_instance = widget(root_widget=True, object_name=name)
        assert isinstance(widget_instance, QWidget)

        window.setCentralWidget(widget_instance)
        window.resize(window.minimumSizeHint())
        window.setWindowTitle(f"BEC - {widget_instance.objectName()}")
        apply_window_geometry(window, geometry)
        window.show()
        return window

    def apply_theme(self, theme: str):
        """
        Change the theme of the application.
        """
        for tile in self.tiles.values():
            tile.apply_theme(theme)

        super().apply_theme(theme)

    def _open_auto_update(self):
        """
        Open the auto update window.
        """
        if self.tiles["auto_update"].selector is None:
            auto_update = None
        else:
            auto_update = self.tiles["auto_update"].selector.currentText()
            if auto_update == "Default":
                auto_update = None
        return self.launch("auto_update", auto_update=auto_update)

    def _open_dock_area(self):
        """
        Open Advanced Dock Area using the selected profile.
        """
        tile = self.tiles.get("dock_area")
        if tile is None or tile.selector is None:
            startup_profile = None
        else:
            selection = tile.selector.currentText().strip()
            if selection == START_EMPTY_PROFILE_OPTION:
                startup_profile = None
            else:
                startup_profile = selection if selection else None
        return self.launch("dock_area", startup_profile=startup_profile)

    def _open_widget(self):
        """
        Open a widget from the available widgets.
        """
        if self.tiles["widget"].selector is None:
            return
        widget = self.tiles["widget"].selector.currentText()
        if widget not in self.available_widgets:
            raise ValueError(f"Widget {widget} not found in available widgets.")
        return self.launch("widget", widget=self.available_widgets[widget])

    def _default_launch_geometry(self) -> tuple[int, int, int, int] | None:
        width, height = self.DEFAULT_LAUNCH_SIZE
        return centered_geometry_for_app(width=width, height=height)

    @SafeSlot(popup_error=True)
    def _open_custom_ui_file(self):
        """
        Open a file dialog to select a custom UI file and launch it.
        """
        ui_file, _ = QFileDialog.getOpenFileName(
            self, "Select UI File", "", "UI Files (*.ui);;All Files (*)"
        )
        self.launch("custom_ui_file", ui_file=ui_file)

    @staticmethod
    def _update_available_auto_updates() -> dict[str, type[AutoUpdates]]:
        """
        Load all available auto updates from the plugin repository.
        """
        try:
            auto_updates = get_plugin_auto_updates()
            logger.info(f"Available auto updates: {auto_updates.keys()}")
        except Exception as exc:
            logger.error(f"Failed to load auto updates: {exc}")
            return {}
        return auto_updates

    def show_launcher(self):
        """
        Show the launcher window.
        """
        self.show()

    def hide_launcher(self):
        """
        Hide the launcher window.
        """
        self.hide()

    def showEvent(self, event):
        self._refresh_dock_area_profiles()
        super().showEvent(event)
        self.setFixedSize(self.size())

    def _launcher_is_last_widget(self, connections: dict) -> bool:
        """
        Check if the launcher is the last widget in the application.
        """

        # get all parents of connections
        for connection in connections.values():
            try:
                parent = connection.parent()
                if parent is None and connection.objectName() != self.objectName():
                    logger.info(
                        f"Found non-launcher connection without parent: {connection.objectName()}"
                    )
                    return False
            except Exception as e:
                logger.error(f"Error getting parent of connection: {e}")
                return False
        return True

    def _turn_off_the_lights(self, connections: dict):
        """
        If there is only one connection remaining, it is the launcher, so we show it.
        Once the launcher is closed as the last window, we quit the application.
        """
        if self._launcher_is_last_widget(connections):
            self.show()
            self.activateWindow()
            self.raise_()
            if self.app:
                self.app.setQuitOnLastWindowClosed(True)  # type: ignore
            return

        self.hide()
        if self.app:
            self.app.setQuitOnLastWindowClosed(False)  # type: ignore

    def closeEvent(self, event):
        """
        Close the launcher window.
        """
        connections = self.register.list_all_connections()
        if self._launcher_is_last_widget(connections):
            event.accept()
            return

        event.ignore()
        self.hide()


if __name__ == "__main__":  # pragma: no cover
    import sys

    from bec_widgets.utils.colors import apply_theme

    app = QApplication(sys.argv)
    apply_theme("dark")
    launcher = LaunchWindow()
    launcher.show()
    sys.exit(app.exec())
