"""
##################################################################################
#
# Website As App
# Run any website as standalone desktop application
#
# @author    Marcin Orlowski <mail (#) marcinOrlowski (.) com>
# @copyright 2023-2024 Marcin Orlowski
# @license   https://www.opensource.org/licenses/mit-license.php MIT
# @link      https://github.com/MarcinOrlowski/website-as-app
#
##################################################################################
"""
import os
import sys
from typing import Optional

import fasteners
from PySide6.QtCore import QUrl, QFileSystemWatcher, QStandardPaths, Qt
from PySide6.QtGui import QAction
from PySide6.QtWebEngineCore import (
    QWebEngineProfile,
    QWebEngineSettings,
)
from PySide6.QtWebEngineWidgets import QWebEngineView
from PySide6.QtWidgets import (
    QApplication,
    QMainWindow,
    QVBoxLayout,
    QWidget,
    QSystemTrayIcon,
    QMenu,
    QFileDialog,
)
from PySide6.QtWebEngineCore import QWebEnginePage

from websiteapp.about import About
from websiteapp.const import Const
from websiteapp.utils import Utils


class WebApp(QMainWindow):
    about_dialog: Optional[About] = None
    lock: Optional[fasteners.InterProcessLock] = None
    file_watcher: Optional[QFileSystemWatcher] = None

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

        # Check for --version before full argument parsing
        if '--version' in sys.argv:
            print(f'{Const.APP_NAME} {Const.APP_VERSION}')
            sys.exit(0)

        self.app = QApplication.instance()
        self.args = Utils.handle_args()

        if not self.args.allow_multiple:
            if not self.acquire_lock():
                sys.exit(0)  # Exit silently as we've activated the existing instance

        self.setup_activation_listener()

        # Set window geometry
        x, y, width, height = Utils.parse_geometry(self.args.geometry)
        self.setGeometry(x, y, width, height)
        self.dbug(f'Geometry: {width}x{height}+{x}+{y}')

        app_icon = Utils.get_icon(self.args.icon)
        self.setWindowIcon(app_icon)
        if not self.args.no_tray:
            self.setup_tray_icon(app_icon)

        window_title = self.args.name if self.args.name else self.args.url
        window_title += f' ({self.args.profile})' if self.args.debug else ''
        self.setWindowTitle(f'{window_title} · {Const.APP_NAME}')

        # Create a persistent profile (cookie jar etc.)
        self.dbug(f'Profile: {self.args.profile}')
        self.profile = QWebEngineProfile(self.args.profile, self)
        self.page = QWebEnginePage(self.profile, self)

        # Create and configure the webpage
        self.page = QWebEnginePage(self.profile, self)
        # Connect permission request handler
        self.page.featurePermissionRequested.connect(self.handle_permission_request)

        # Handle downloads
        self.profile.downloadRequested.connect(self.on_download_requested)

        self.browser = QWebEngineView(self)
        web_settings = self.browser.settings()

        # Enable all required clipboard permissions
        web_settings.setAttribute(QWebEngineSettings.WebAttribute.JavascriptCanAccessClipboard, True)
        web_settings.setAttribute(QWebEngineSettings.WebAttribute.JavascriptCanPaste, True)
        web_settings.setAttribute(QWebEngineSettings.WebAttribute.AllowWindowActivationFromJavaScript, True)
        # Additional profile settings for clipboard
        self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.JavascriptCanAccessClipboard, True)
        self.profile.settings().setAttribute(QWebEngineSettings.WebAttribute.JavascriptCanPaste, True)

        self.browser.setPage(self.page)
        self.browser.setZoomFactor(self.args.zoom)

        # Ensure the browser widget can receive focus and key events
        self.browser.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
        self.browser.setFocus()

        self.dbug(f'URL: {self.args.url}')
        self.browser.setUrl(QUrl(self.args.url))

        # Window layout
        layout = QVBoxLayout()
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(0)
        layout.addWidget(self.browser)

        central_widget = QWidget()
        central_widget.setLayout(layout)
        self.setCentralWidget(central_widget)

    def acquire_lock(self):
        """
        Acquires a lock for the current profile to prevent multiple instances.
        Returns True if lock was acquired, False otherwise.
        """
        lock_file = os.path.join(os.path.expanduser("~"), f".websiteapp_{self.args.profile}.lock")
        self.lock = fasteners.InterProcessLock(lock_file)
        if not self.lock.acquire(blocking=False):
            self.activate_existing_instance()
            return False
        return True

    def activate_existing_instance(self):
        """
        Sends a signal to the existing instance to activate its window.
        """
        self.dbug(f"Bringing existing instance: {Const.APP_NAME}, profile: {self.args.profile}")
        signal_file = os.path.join(os.path.expanduser("~"),
                                   f".websiteapp_{self.args.profile}_signal")
        with open(signal_file, 'w') as f:
            f.write("activate")

    def setup_activation_listener(self):
        """
        Sets up a file system watcher to listen for activation signals.
        """
        self.file_watcher = QFileSystemWatcher(self)
        signal_file = os.path.join(os.path.expanduser("~"),
                                   f".websiteapp_{self.args.profile}_signal")
        self.file_watcher.addPath(os.path.dirname(signal_file))
        self.file_watcher.directoryChanged.connect(self.check_activation_signal)

    def check_activation_signal(self, path):
        """
        Checks for activation signals and brings the window to front if signal is received.
        """
        signal_file = os.path.join(path, f".websiteapp_{self.args.profile}_signal")
        if os.path.exists(signal_file):
            os.remove(signal_file)
            self.activate_window()

    def activate_window(self):
        """
        Brings the window to front and restores it if minimized.
        """
        self.showNormal()
        self.activateWindow()
        self.raise_()

    def closeEvent(self, event) -> None:
        """
        Override closeEvent to hide the window instead of closing it and hides the window instead.

        :param event:
        """
        if not self.args.no_tray:
            self.hide()
            event.ignore()
        else:
            if self.lock:
                self.lock.release()
                self.lock = None
            super().closeEvent(event)

    def setup_tray_icon(self, icon) -> None:
        """
        This method is used to set up the system tray icon for the application.

        :param icon: QPixmap object representing the icon that will be used in the system tray.
        """
        tray_menu = QMenu()

        tray_icon = QSystemTrayIcon(icon, self.app)
        tray_icon.setContextMenu(tray_menu)
        tray_icon.activated.connect(self.on_tray_icon_activated)

        about_label = f'About {Const.APP_NAME}'
        about_action = QAction(about_label, self.app)
        about_action.triggered.connect(self.open_about_dialog)
        tray_menu.addAction(about_action)

        show_label = f'Show {self.args.name}' if self.args.name else 'Show'
        show_action = QAction(show_label, self)
        show_action.triggered.connect(self.show)
        tray_menu.addAction(show_action)

        quit_label = f'Quit {self.args.name}' if self.args.name else 'Quit'
        quit_action = QAction(quit_label, self.app)
        quit_action.triggered.connect(self.quit_app)
        tray_menu.addAction(quit_action)

        tray_icon.show()

    def quit_app(self) -> None:
        """
        Closes the application.
        """
        if self.lock:
            self.lock.release()
            self.lock = None
        self.app.quit()

    def open_about_dialog(self) -> None:
        """
        Opens the about dialog.
        """
        if not self.about_dialog:
            self.about_dialog = About()

        if not self.about_dialog.isVisible():
            self.about_dialog.show()
        self.about_dialog.activateWindow()

    def on_tray_icon_activated(self, reason) -> None:
        """
        Callback invoked when application icon in system tray is clicked.
        """
        if reason == QSystemTrayIcon.ActivationReason.Trigger:
            # Check for left-click (Trigger)
            self.toggle_window()

    def toggle_window(self) -> None:
        """
        Toggles the visibility of the window.
        """
        self.hide() if self.isVisible() else self.show()

    # ############################################################################################ #

    def dbug(self, msg: str) -> None:
        if self.args.debug:
            print(msg, file=sys.stderr)

    # ############################################################################################ #

    @staticmethod
    def run() -> None:
        """
        Application entry point. When renamed, ensure setup.py's reference is updated as well.
        """
        app = QApplication(sys.argv)
        app.setOrganizationName("MarcinOrlowski")
        app.setApplicationName("Website As App")

        window = WebApp()

        if window.args.minimized and window.args.no_tray:
            # Cannot use --no-tray and --minimized at the same time. Ignoring --minimized
            window.args.minimized = False

        if not window.args.minimized:
            window.show()

        exit_code = app.exec()

        if window.lock:
            window.lock.release()
            window.lock = None

        sys.exit(exit_code)

    def on_download_requested(self, download):
        """
        Handles file download requests from the web page.
        """
        print('on_download_requested')
        # Prompt the user to select a download location
        suggested_filename = download.downloadFileName()
        options = QFileDialog.Options()
        path, _ = QFileDialog.getSaveFileName(self, "Save File", suggested_filename, options=options)
        if path:
            download.setDownloadFileName(os.path.basename(path))
            download.setDownloadDirectory(os.path.dirname(path))
            download.accept()
        else:
            download.cancel()

    def handle_permission_request(self, origin, feature):
        """
        Handle permission requests from the webpage.
        """
        from PySide6.QtWebEngineCore import QWebEnginePage

        self.dbug(f"Permission requested: {feature} from {origin}")

        # Define all clipboard-related features
        clipboard_features = [
            QWebEnginePage.Feature.ClipboardReadWrite,
            QWebEnginePage.Feature.Clipboard,  # For backwards compatibility
        ]

        if feature in clipboard_features:
            self.dbug(f"Granting clipboard permission for feature: {feature}")
            self.page.setFeaturePermission(
                origin,
                feature,
                QWebEnginePage.PermissionPolicy.PermissionGrantedByUser
            )
        else:
            self.dbug(f"Denying permission for feature: {feature}")
            self.page.setFeaturePermission(
                origin,
                feature,
                QWebEnginePage.PermissionPolicy.PermissionDeniedByUser
            )
