import os
import socket
from datetime import datetime
from pathlib import Path

import humanize
from PyQt6 import QtCore
from PyQt6.QtCore import Qt, QSettings, pyqtSignal as Signal, QThread, pyqtSignal, QTimer
from PyQt6.QtGui import QIcon
from PyQt6.QtWidgets import QApplication, QMainWindow, QListWidget, QListWidgetItem, QStackedWidget, QVBoxLayout, \
    QWidget, QLabel, QCheckBox, QLineEdit, \
    QPushButton, QHBoxLayout, QGroupBox, QTableWidget, QAbstractItemView, QTableWidgetItem, QHeaderView, \
    QMessageBox, QProgressBar

from src.api.server_proxy import RenderServerProxy
from src.engines.engine_manager import EngineManager
from src.utilities.config import Config
from src.utilities.misc_helper import launch_url
from src.version import APP_AUTHOR, APP_NAME

settings = QSettings(APP_AUTHOR, APP_NAME)

class GetEngineInfoWorker(QThread):
    """
    The GetEngineInfoWorker class fetches engine information from a server in a background thread.

    Attributes:
        done: A signal emitted when the engine information is retrieved.

    Methods:
        run(self): Fetches engine information from the server.
    """
    done = pyqtSignal(object)  # emits the result when finished

    def __init__(self, parent=None):
        super().__init__(parent)
        self.parent = parent

    def run(self):
        data = RenderServerProxy(socket.gethostname()).get_all_engine_info()
        self.done.emit(data)

class SettingsWindow(QMainWindow):
    """
    The SettingsWindow class provides a user interface for managing engine settings.
    """

    def __init__(self):
        super().__init__()

        self.engine_download_progress_bar = None
        self.engines_last_update_label = None
        self.check_for_engine_updates_checkbox = None
        self.delete_engine_button = None
        self.launch_engine_button = None
        self.show_password_button = None
        self.network_password_line = None
        self.enable_network_password_checkbox = None
        self.check_for_new_engines_button = None

        if not EngineManager.engines_path:  # fix issue where sometimes path was not set
            EngineManager.engines_path = Path(Config.upload_folder).expanduser() / "engines"

        self.installed_engines_table = None

        self.setWindowTitle("Settings")

        # Create the main layout
        main_layout = QVBoxLayout()

        # Create the sidebar (QListWidget) for navigation
        self.sidebar = QListWidget()
        self.sidebar.setFixedWidth(150)

        # Set the icon size
        self.sidebar.setIconSize(QtCore.QSize(32, 32))  # Increase the icon size to 32x32 pixels

        # Adjust the font size for the sidebar items
        font = self.sidebar.font()
        font.setPointSize(12)  # Increase the font size
        self.sidebar.setFont(font)

        # Add items with icons to the sidebar
        resources_dir = os.path.join(Path(__file__).resolve().parent.parent.parent, 'resources')
        self.add_sidebar_item("General", os.path.join(resources_dir, "Gear.png"))
        self.add_sidebar_item("Server", os.path.join(resources_dir, "Server.png"))
        self.add_sidebar_item("Engines", os.path.join(resources_dir, "Blender.png"))
        self.sidebar.setCurrentRow(0)

        # Create the stacked widget to hold different settings pages
        self.stacked_widget = QStackedWidget()

        # Create pages for each section
        general_page = self.create_general_page()
        network_page = self.create_network_page()
        engines_page = self.create_engines_page()

        # Add pages to the stacked widget
        self.stacked_widget.addWidget(general_page)
        self.stacked_widget.addWidget(network_page)
        self.stacked_widget.addWidget(engines_page)

        # Connect the sidebar to the stacked widget
        self.sidebar.currentRowChanged.connect(self.stacked_widget.setCurrentIndex)

        # Create a horizontal layout to hold the sidebar and stacked widget
        content_layout = QHBoxLayout()
        content_layout.addWidget(self.sidebar)
        content_layout.addWidget(self.stacked_widget)

        # Add the content layout to the main layout
        main_layout.addLayout(content_layout)

        # Add the "OK" button at the bottom
        ok_button = QPushButton("OK")
        ok_button.clicked.connect(self.close)
        ok_button.setFixedWidth(80)
        ok_button.setDefault(True)
        main_layout.addWidget(ok_button, alignment=Qt.AlignmentFlag.AlignRight)

        # Create a central widget and set the layout
        central_widget = QWidget()
        central_widget.setLayout(main_layout)
        self.setCentralWidget(central_widget)

        self.setMinimumSize(700, 400)

        # timers for background download UI updates
        self.timer = QTimer(self)
        self.timer.timeout.connect(self.update_engine_download_status)

    def add_sidebar_item(self, name, icon_path):
        """Add an item with an icon to the sidebar."""
        item = QListWidgetItem(QIcon(icon_path), name)
        self.sidebar.addItem(item)

    def create_general_page(self):
        """Create the General settings page."""
        page = QWidget()
        layout = QVBoxLayout()

        # Startup Settings Group
        startup_group = QGroupBox("Startup Settings")
        startup_layout = QVBoxLayout()
        # startup_layout.addWidget(QCheckBox("Start application on system startup"))
        check_for_updates_checkbox = QCheckBox("Check for updates automatically")
        check_for_updates_checkbox.setChecked(settings.value("auto_check_for_updates", True, type=bool))
        check_for_updates_checkbox.stateChanged.connect(lambda state: settings.setValue("auto_check_for_updates", bool(state)))
        startup_layout.addWidget(check_for_updates_checkbox)
        startup_group.setLayout(startup_layout)

        # Local Files Group
        data_path = Path(Config.upload_folder).expanduser()
        path_size = sum(f.stat().st_size for f in Path(data_path).rglob('*') if f.is_file())
        database_group = QGroupBox("Local Files")
        database_layout = QVBoxLayout()
        database_layout.addWidget(QLabel(f"Local Directory: {data_path}"))
        database_layout.addWidget(QLabel(f"Size: {humanize.naturalsize(path_size, binary=True)}"))
        open_database_path_button = QPushButton("Open Directory")
        open_database_path_button.clicked.connect(lambda: launch_url(data_path))
        open_database_path_button.setFixedWidth(200)
        database_layout.addWidget(open_database_path_button)
        database_group.setLayout(database_layout)


        # Render Settings Group
        render_settings_group = QGroupBox("Render Engine Settings")
        render_settings_layout = QVBoxLayout()
        render_settings_layout.addWidget(QLabel("Restrict to render nodes with same:"))
        require_same_engine_checkbox = QCheckBox("Renderer Version")
        require_same_engine_checkbox.setChecked(settings.value("render_require_same_engine_version", False, type=bool))
        require_same_engine_checkbox.stateChanged.connect(lambda state: settings.setValue("render_require_same_engine_version", bool(state)))
        render_settings_layout.addWidget(require_same_engine_checkbox)
        require_same_cpu_checkbox = QCheckBox("CPU Architecture")
        require_same_cpu_checkbox.setChecked(settings.value("render_require_same_cpu_type", False, type=bool))
        require_same_cpu_checkbox.stateChanged.connect(lambda state: settings.setValue("render_require_same_cpu_type", bool(state)))
        render_settings_layout.addWidget(require_same_cpu_checkbox)
        require_same_os_checkbox = QCheckBox("Operating System")
        require_same_os_checkbox.setChecked(settings.value("render_require_same_os", False, type=bool))
        require_same_os_checkbox.stateChanged.connect(lambda state: settings.setValue("render_require_same_os", bool(state)))
        render_settings_layout.addWidget(require_same_os_checkbox)
        render_settings_group.setLayout(render_settings_layout)

        layout.addWidget(startup_group)
        layout.addWidget(database_group)
        layout.addWidget(render_settings_group)

        layout.addStretch()  # Add a stretch to push content to the top
        page.setLayout(layout)
        return page

    def create_network_page(self):
        """Create the Network settings page."""
        page = QWidget()
        layout = QVBoxLayout()

        # Sharing Settings Group
        sharing_group = QGroupBox("Sharing Settings")
        sharing_layout = QVBoxLayout()

        enable_sharing_checkbox = QCheckBox("Enable other computers on the network to render to this machine")
        enable_sharing_checkbox.setChecked(settings.value("enable_network_sharing", False, type=bool))
        enable_sharing_checkbox.stateChanged.connect(self.toggle_render_sharing)
        sharing_layout.addWidget(enable_sharing_checkbox)

        password_enabled = (settings.value("enable_network_sharing", False, type=bool) and
                            settings.value("enable_network_password", False, type=bool))

        password_layout = QHBoxLayout()
        password_layout.setContentsMargins(0, 0, 0, 0)
        self.enable_network_password_checkbox = QCheckBox("Enable network password:")
        self.enable_network_password_checkbox.setChecked(settings.value("enable_network_password", False, type=bool))
        self.enable_network_password_checkbox.stateChanged.connect(self.enable_network_password_changed)
        self.enable_network_password_checkbox.setEnabled(settings.value("enable_network_sharing", False, type=bool))
        sharing_layout.addWidget(self.enable_network_password_checkbox)
        self.network_password_line = QLineEdit()
        self.network_password_line.setPlaceholderText("Enter a password")
        self.network_password_line.setEchoMode(QLineEdit.EchoMode.Password)
        self.network_password_line.setEnabled(password_enabled)
        password_layout.addWidget(self.network_password_line)
        self.show_password_button = QPushButton("Show")
        self.show_password_button.setEnabled(password_enabled)
        self.show_password_button.clicked.connect(self.show_password_button_pressed)
        password_layout.addWidget(self.show_password_button)
        sharing_layout.addLayout(password_layout)

        sharing_group.setLayout(sharing_layout)

        layout.addWidget(sharing_group)

        layout.addStretch()  # Add a stretch to push content to the top
        page.setLayout(layout)
        return page

    def toggle_render_sharing(self, enable_sharing):
        settings.setValue("enable_network_sharing", enable_sharing)
        self.enable_network_password_checkbox.setEnabled(enable_sharing)
        enable_password = enable_sharing and settings.value("enable_network_password", False, type=bool)
        self.network_password_line.setEnabled(enable_password)
        self.show_password_button.setEnabled(enable_password)

    def enable_network_password_changed(self, new_value):
        settings.setValue("enable_network_password", new_value)
        self.network_password_line.setEnabled(new_value)
        self.show_password_button.setEnabled(new_value)

    def show_password_button_pressed(self):
        # toggle showing / hiding the password
        show_pass = self.show_password_button.text() == "Show"
        self.show_password_button.setText("Hide" if show_pass else "Show")
        self.network_password_line.setEchoMode(QLineEdit.EchoMode.Normal if show_pass else QLineEdit.EchoMode.Password)

    def create_engines_page(self):
        """Create the Engines settings page."""
        page = QWidget()
        layout = QVBoxLayout()

        # Installed Engines Group
        installed_group = QGroupBox("Installed Engines")
        installed_layout = QVBoxLayout()

        # Setup table
        self.installed_engines_table = EngineTableWidget()
        self.installed_engines_table.row_selected.connect(self.engine_table_selected)
        installed_layout.addWidget(self.installed_engines_table)

        # Ignore system installs
        engine_ignore_system_installs_checkbox = QCheckBox("Ignore system installs")
        engine_ignore_system_installs_checkbox.setChecked(settings.value("engines_ignore_system_installs", False, type=bool))
        engine_ignore_system_installs_checkbox.stateChanged.connect(self.change_ignore_system_installs)
        installed_layout.addWidget(engine_ignore_system_installs_checkbox)

        # Engine Launch / Delete buttons
        installed_buttons_layout = QHBoxLayout()
        self.launch_engine_button = QPushButton("Launch")
        self.launch_engine_button.setEnabled(False)
        self.launch_engine_button.clicked.connect(self.launch_selected_engine)
        self.delete_engine_button = QPushButton("Delete")
        self.delete_engine_button.setEnabled(False)
        self.delete_engine_button.clicked.connect(self.delete_selected_engine)

        installed_buttons_layout.addWidget(self.launch_engine_button)
        installed_buttons_layout.addWidget(self.delete_engine_button)
        installed_layout.addLayout(installed_buttons_layout)
        installed_group.setLayout(installed_layout)

        # Engine Updates Group
        engine_updates_group = QGroupBox("Auto-Install")
        engine_updates_layout = QVBoxLayout()

        engine_download_layout = QHBoxLayout()
        engine_download_layout.addWidget(QLabel("Enable Downloads for:"))

        at_least_one_downloadable = False
        for engine in EngineManager.downloadable_engines():
            engine_download_check = QCheckBox(engine.name())
            is_checked = settings.value(f"engine_download-{engine.name()}", False, type=bool)
            at_least_one_downloadable |= is_checked
            engine_download_check.setChecked(is_checked)
            # Capture the checkbox correctly using a default argument in lambda
            engine_download_check.clicked.connect(
                lambda state, checkbox=engine_download_check: self.engine_download_settings_changed(state, checkbox.text())
            )
            engine_download_layout.addWidget(engine_download_check)

        engine_updates_layout.addLayout(engine_download_layout)

        self.check_for_engine_updates_checkbox = QCheckBox("Check for new versions on launch")
        self.check_for_engine_updates_checkbox.setChecked(settings.value('check_for_engine_updates_on_launch', True, type=bool))
        self.check_for_engine_updates_checkbox.setEnabled(at_least_one_downloadable)
        self.check_for_engine_updates_checkbox.stateChanged.connect(
            lambda state: settings.setValue("check_for_engine_updates_on_launch", bool(state)))
        engine_updates_layout.addWidget(self.check_for_engine_updates_checkbox)
        self.engines_last_update_label = QLabel()
        self.update_last_checked_label()
        self.engines_last_update_label.setEnabled(at_least_one_downloadable)
        engine_updates_layout.addWidget(self.engines_last_update_label)
        self.engine_download_progress_bar = QProgressBar()
        engine_updates_layout.addWidget(self.engine_download_progress_bar)
        self.engine_download_progress_bar.setHidden(True)
        self.check_for_new_engines_button = QPushButton("Check for New Versions...")
        self.check_for_new_engines_button.setEnabled(at_least_one_downloadable)
        self.check_for_new_engines_button.clicked.connect(self.check_for_new_engines)
        engine_updates_layout.addWidget(self.check_for_new_engines_button)
        engine_updates_group.setLayout(engine_updates_layout)

        layout.addWidget(installed_group)
        layout.addWidget(engine_updates_group)

        layout.addStretch()  # Add a stretch to push content to the top
        page.setLayout(layout)
        return page

    def change_ignore_system_installs(self, value):
        settings.setValue("engines_ignore_system_installs", bool(value))
        self.installed_engines_table.update_engines_table()

    def update_last_checked_label(self):
        """Retrieve the last check timestamp and return a human-friendly string."""
        last_checked_str = settings.value("engines_last_update_time", None)
        if not last_checked_str:
            time_string = "Never"
        else:
            last_checked_dt = datetime.fromisoformat(last_checked_str)
            now = datetime.now()
            time_string = humanize.naturaltime(now - last_checked_dt)
        self.engines_last_update_label.setText(f"Last Updated: {time_string}")

    def engine_download_settings_changed(self, state, engine_name):
        settings.setValue(f"engine_download-{engine_name}", state)

        at_least_one_downloadable = False
        for engine in EngineManager.downloadable_engines():
            at_least_one_downloadable |= settings.value(f"engine_download-{engine.name()}", False, type=bool)
        self.check_for_new_engines_button.setEnabled(at_least_one_downloadable)
        self.check_for_engine_updates_checkbox.setEnabled(at_least_one_downloadable)
        self.engines_last_update_label.setEnabled(at_least_one_downloadable)

    def delete_selected_engine(self):
        engine_info = self.installed_engines_table.selected_engine_data()
        reply = QMessageBox.question(self, f"Delete {engine_info['engine']} {engine_info['version']}?",
                                     f"Do you want to delete {engine_info['engine']} {engine_info['version']}?",
                                     QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)

        if reply is not QMessageBox.StandardButton.Yes:
            return

        delete_result = EngineManager.delete_engine_download(engine_info.get('engine'),
                                                             engine_info.get('version'),
                                                             engine_info.get('system_os'),
                                                             engine_info.get('cpu'))
        self.installed_engines_table.update_engines_table(use_cached=False)
        if delete_result:
            QMessageBox.information(self, f"{engine_info['engine']} {engine_info['version']} Deleted",
                                         f"{engine_info['engine']} {engine_info['version']} deleted successfully",
                                         QMessageBox.StandardButton.Ok)
        else:
            QMessageBox.warning(self, f"Unknown Error",
                                f"Unknown error while deleting {engine_info['engine']} {engine_info['version']}.",
                                QMessageBox.StandardButton.Ok)

    def launch_selected_engine(self):
        engine_info = self.installed_engines_table.selected_engine_data()
        if engine_info:
            launch_url(engine_info['path'])

    def engine_table_selected(self):
        engine_data = self.installed_engines_table.selected_engine_data()
        if engine_data:
            self.launch_engine_button.setEnabled(bool(engine_data.get('path') or True))
            self.delete_engine_button.setEnabled(engine_data.get('type') == 'managed')
        else:
            self.launch_engine_button.setEnabled(False)
            self.delete_engine_button.setEnabled(False)

    def check_for_new_engines(self):

        ignore_system = settings.value("engines_ignore_system_installs", False, type=bool)
        messagebox_shown = False
        for engine in EngineManager.downloadable_engines():
            if settings.value(f'engine_download-{engine.name()}', False, type=bool):
                result = EngineManager.is_engine_update_available(engine, ignore_system_installs=ignore_system)
                if result:
                    result['name'] = engine.name()
                    msg_box = QMessageBox()
                    msg_box.setWindowTitle(f"{result['name']} ({result['version']}) Available")
                    msg_box.setText(f"A new version of {result['name']} is available ({result['version']}).\n\n"
                                    f"Would you like to download it now?")
                    msg_box.setIcon(QMessageBox.Icon.Question)
                    msg_box.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
                    msg_result = msg_box.exec()
                    messagebox_shown = True
                    if msg_result == QMessageBox.StandardButton.Yes:
                        EngineManager.download_engine(engine_name=engine.name(), version=result['version'], background=True,
                                                      ignore_system=ignore_system)
                        self.engine_download_progress_bar.setHidden(False)
                        self.engine_download_progress_bar.setValue(0)
                        self.engine_download_progress_bar.setMaximum(100)
                        self.check_for_new_engines_button.setEnabled(False)
                        self.timer.start(1000)

        if not messagebox_shown:
            msg_box = QMessageBox()
            msg_box.setWindowTitle("No Updates Available")
            msg_box.setText("No Updates Available.")
            msg_box.setIcon(QMessageBox.Icon.Information)
            msg_box.setStandardButtons(QMessageBox.StandardButton.Ok)
            msg_box.exec()

        settings.setValue("engines_last_update_time", datetime.now().isoformat())
        self.update_engine_download_status()

    def update_engine_download_status(self):

        running_tasks = EngineManager.active_downloads()
        if not running_tasks:
            self.timer.stop()
            self.engine_download_progress_bar.setHidden(True)
            self.installed_engines_table.update_engines_table(use_cached=False)
            self.update_last_checked_label()
            self.check_for_new_engines_button.setEnabled(True)
            return

        percent_complete = int(running_tasks[0].percent_complete * 100)
        self.engine_download_progress_bar.setValue(percent_complete)
        if percent_complete == 100:
            status_update = f"Installing {running_tasks[0].engine.capitalize()} {running_tasks[0].version}..."
        else:
            status_update = f"Downloading {running_tasks[0].engine.capitalize()} {running_tasks[0].version}..."
        self.engines_last_update_label.setText(status_update)


class EngineTableWidget(QWidget):
    """
    The EngineTableWidget class displays a table of installed engines.

    Attributes:
        table: A table widget displaying engine information.

    Methods:
        on_selection_changed(self): Emits a signal when the user selects a different row in the table.
    """

    row_selected = Signal()

    def __init__(self):
        super().__init__()

        self.__get_engine_info_worker = None
        self.table = QTableWidget(0, 4)
        self.table.setHorizontalHeaderLabels(["Engine", "Version", "Type", "Path"])
        self.table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
        self.table.verticalHeader().setVisible(False)
        # self.table_widget.itemSelectionChanged.connect(self.engine_picked)
        self.table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
        self.table.selectionModel().selectionChanged.connect(self.on_selection_changed)

        layout = QVBoxLayout(self)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(0)
        layout.addWidget(self.table)

        self.raw_server_data = None

    def showEvent(self, event):
        """Runs when the widget is about to be shown."""
        self.update_engines_table()
        super().showEvent(event)  # Ensure normal event processing

    def engine_data_ready(self, raw_server_data):
        self.raw_server_data = raw_server_data
        self.update_engines_table()

    def update_engines_table(self, use_cached=True):
        if not self.raw_server_data or not use_cached:
            self.__get_engine_info_worker = GetEngineInfoWorker(self)
            self.__get_engine_info_worker.done.connect(self.engine_data_ready)
            self.__get_engine_info_worker.start()
        if not self.raw_server_data:
            return

        table_data = []  # convert the data into a flat list
        for _, engine_data in self.raw_server_data.items():
            table_data.extend(engine_data['versions'])

        if settings.value("engines_ignore_system_installs", False, type=bool):
            table_data = [x for x in table_data if x['type'] != 'system']

        self.table.clear()
        self.table.setRowCount(len(table_data))
        self.table.setColumnCount(4)

        self.table.setHorizontalHeaderLabels(['Engine', 'Version', 'Type', 'Path'])
        self.table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed)
        self.table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed)
        self.table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Fixed)
        self.table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.Stretch)

        for row, engine in enumerate(table_data):
            self.table.setItem(row, 0, QTableWidgetItem(engine['engine']))
            self.table.setItem(row, 1, QTableWidgetItem(engine['version']))
            self.table.setItem(row, 2, QTableWidgetItem(engine['type']))
            self.table.setItem(row, 3, QTableWidgetItem(engine['path']))

        self.table.selectRow(0)

    def selected_engine_data(self):
        """Returns the data from the selected row as a dictionary."""
        row = self.table.currentRow()  # Get the selected row index

        if row < 0 or not len(self.table.selectedItems()):  # No row selected
            return None

        data = {
            "engine": self.table.item(row, 0).text(),
            "version": self.table.item(row, 1).text(),
            "type": self.table.item(row, 2).text(),
            "path": self.table.item(row, 3).text(),
        }

        return data

    def on_selection_changed(self):
        self.row_selected.emit()



if __name__ == "__main__":
    app = QApplication([])
    window = SettingsWindow()
    window.show()
    app.exec()