# window.py
#
# Copyright 2025 mirkobrombin <brombin94@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, in version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#

import contextlib
import os
import webbrowser
from datetime import datetime, timedelta
from gettext import gettext as _
from typing import Optional

from gi.repository import Adw, Gdk, Gio, GLib, GObject, Gtk

from bottles.backend.globals import Paths
from bottles.backend.health import HealthChecker
from bottles.backend.logger import Logger
from bottles.backend.managers.data import DataManager, UserDataKeys
from bottles.backend.managers.journal import JournalManager
from bottles.backend.managers.manager import Manager
from bottles.backend.models.config import BottleConfig
from bottles.backend.models.result import Result
from bottles.backend.state import Notification, SignalManager, Signals
from bottles.backend.utils.connection import ConnectionUtils
from bottles.backend.utils.threading import RunAsync
from bottles.frontend.operation import TaskSyncer
from bottles.frontend.params import APP_ID, PROFILE
from bottles.frontend.utils.gtk import GtkUtils
from bottles.frontend.views.details import DetailsView
from bottles.frontend.views.importer import ImporterView
from bottles.frontend.views.library import LibraryView
from bottles.frontend.views.list import BottleView
from bottles.frontend.views.loading import LoadingView
from bottles.frontend.views.new_bottle_dialog import BottlesNewBottleDialog
from bottles.frontend.views.preferences import PreferencesWindow
from bottles.frontend.windows.crash import CrashReportDialog
from bottles.frontend.windows.depscheck import DependenciesCheckDialog
from bottles.frontend.windows.onboard import OnboardDialog
from bottles.frontend.windows.winebridgeupdate import WineBridgeUpdateDialog
from bottles.frontend.windows.funding import FundingDialog

logging = Logger()


@Gtk.Template(resource_path="/com/usebottles/bottles/window.ui")
class BottlesWindow(Adw.ApplicationWindow):
    __gtype_name__ = "BottlesWindow"

    # region Widgets
    stack_main = Gtk.Template.Child()
    btn_add = Gtk.Template.Child()
    btn_search = Gtk.Template.Child()
    btn_donate = Gtk.Template.Child()
    btn_noconnection = Gtk.Template.Child()
    banner_offline = Gtk.Template.Child()
    box_actions = Gtk.Template.Child()
    headerbar = Gtk.Template.Child()
    view_switcher_title = Gtk.Template.Child()
    view_switcher_bar = Gtk.Template.Child()
    main_leaf = Gtk.Template.Child()
    toasts = Gtk.Template.Child()
    # endregion

    # Common variables
    previous_page = ""
    settings = Gio.Settings.new(APP_ID)
    argument_executed = False
    _winebridge_dialog_shown = False

    def __init__(self, arg_bottle, **kwargs):
        width = self.settings.get_int("window-width")
        height = self.settings.get_int("window-height")

        super().__init__(**kwargs, default_width=width, default_height=height)

        self.data_mgr = DataManager()
        self._show_funding = False
        
        show_funding_setting = self.settings.get_boolean("show-funding")
        dismissed = self.data_mgr.get(UserDataKeys.FundingDismissed, False)
        
        if show_funding_setting and not dismissed:
            last_prompt = self.data_mgr.get(UserDataKeys.LastFundingPrompt, "")
            
            if not last_prompt:
                self._show_funding = True
            else:
                try:
                    last_date = datetime.strptime(last_prompt, "%Y-%m-%d")
                    if datetime.now() - last_date >= timedelta(days=7):
                        self._show_funding = True
                except ValueError:
                    self._show_funding = True

        self.utils_conn = ConnectionUtils(
            force_offline=self.settings.get_boolean("force-offline")
        )
        self.manager = None
        self.arg_bottle = arg_bottle
        self._showing_onboard = False
        self._winebridge_prompt_attempts = 0
        self.app = kwargs.get("application")
        self.set_icon_name(APP_ID)

        if PROFILE == "development":
            self.add_css_class("devel")

        self.btn_donate.add_css_class("donate")
        self.__schedule_donate_icon_swap()

        # Set night theme according to user settings
        if self.settings.get_boolean("dark-theme"):
            manager = Adw.StyleManager.get_default()
            manager.set_color_scheme(Adw.ColorScheme.FORCE_DARK)

        # Loading view
        self.page_loading = LoadingView()

        # Populate stack
        self.stack_main.add_named(
            child=self.page_loading, name="page_loading"
        ).set_visible(False)
        self.headerbar.add_css_class("flat")

        # Signal connections
        self.btn_donate.connect(
            "clicked",
            self.open_url,
            "https://usebottles.com/funding/",
        )
        self.btn_add.connect("clicked", self.show_add_view)
        self.btn_noconnection.connect("clicked", self.check_for_connection)
        self.banner_offline.connect("button-clicked", self.check_for_connection)
        self.stack_main.connect("notify::visible-child", self.__on_page_changed)

        # backend signal handlers
        self.task_syncer = TaskSyncer(self)
        SignalManager.connect(Signals.TaskAdded, self.task_syncer.task_added_handler)
        SignalManager.connect(
            Signals.TaskRemoved, self.task_syncer.task_removed_handler
        )
        SignalManager.connect(
            Signals.TaskUpdated, self.task_syncer.task_updated_handler
        )
        SignalManager.connect(
            Signals.NetworkStatusChanged, self.network_changed_handler
        )
        SignalManager.connect(Signals.GNotification, self.g_notification_handler)
        SignalManager.connect(Signals.GShowUri, self.g_show_uri_handler)

        # keep the session awake while one or more programs are running
        self.__inhibit_cookie = 0
        self.__running_launches = set()
        SignalManager.connect(Signals.ProgramStarted, self.__on_program_started)
        SignalManager.connect(Signals.ProgramFinished, self.__on_program_finished)

        self.__on_start()
        logging.info(
            "Bottles Started!",
        )
        GLib.idle_add(self.__maybe_show_funding_dialog)

    def __schedule_donate_icon_swap(self):
        GLib.timeout_add_seconds(5, self.__on_donate_icon_timeout)

    def __on_donate_icon_timeout(self):
        icon_name = self.__resolve_donate_icon_name()
        if icon_name:
            self.btn_donate.set_label("")
            self.btn_donate.set_icon_name(icon_name)
        return GLib.SOURCE_REMOVE

    def __resolve_donate_icon_name(self) -> Optional[str]:
        display = self.get_display()
        icon_theme = None
        if display is not None:
            icon_theme = Gtk.IconTheme.get_for_display(display)
        if icon_theme is None:
            icon_theme = Gtk.IconTheme.get_default()
        if icon_theme is None:
            return None

        try:
            icon_theme.add_resource_path("/com/usebottles/bottles/icons")
        except AttributeError:
            pass

        for icon_name in (
            "heart-symbolic",
            "love-symbolic",
            "emblem-favorite-symbolic",
        ):
            if icon_theme.has_icon(icon_name):
                return icon_name
        return None

    @Gtk.Template.Callback()
    def on_close_request(self, *args):
        self.settings.set_int("window-width", self.get_width())
        self.settings.set_int("window-height", self.get_height())

    # region Backend signal handlers
    @GtkUtils.run_in_main_loop
    def network_changed_handler(self, res: Result):
        self.banner_offline.set_revealed(not res.status)

    @GtkUtils.run_in_main_loop
    def g_notification_handler(self, res: Result):
        """handle backend notification request"""
        notify: Notification = res.data
        self.send_notification(title=notify.title, text=notify.text, image=notify.image)

    @GtkUtils.run_in_main_loop
    def g_show_uri_handler(self, res: Result):
        """handle backend show_uri request"""
        uri: str = res.data
        Gtk.show_uri(self, uri, Gdk.CURRENT_TIME)

    @GtkUtils.run_in_main_loop
    def __on_program_started(self, res: Result):
        """Inhibit session idle while a program is running, so the screen does
        not blank during controller-only gameplay."""
        launch_id = getattr(res.data, "launch_id", None)
        if launch_id is None:
            return
        self.__running_launches.add(launch_id)

        if self.__inhibit_cookie:
            return
        app = self.get_application()
        if app:
            self.__inhibit_cookie = app.inhibit(
                self,
                Gtk.ApplicationInhibitFlags.IDLE | Gtk.ApplicationInhibitFlags.SUSPEND,
                _("A program is running"),
            )

    @GtkUtils.run_in_main_loop
    def __on_program_finished(self, res: Result):
        launch_id = getattr(res.data, "launch_id", None)
        if launch_id is not None:
            self.__running_launches.discard(launch_id)

        if self.__running_launches or not self.__inhibit_cookie:
            return
        app = self.get_application()
        if app:
            app.uninhibit(self.__inhibit_cookie)
        self.__inhibit_cookie = 0

    # endregion

    def update_library(self):
        GLib.idle_add(self.page_library.update)

    def title(self, title, subtitle: str = ""):
        self.view_switcher_title.set_title(title)
        self.view_switcher_title.set_subtitle(subtitle)

    def check_for_connection(self, *_args):
        """
        This method checks if the client has an internet connection.
        If true, the manager checks will be performed, unlocking all the
        features locked for no internet connection. Runs off the main loop so
        the UI stays responsive and the component catalog is refetched, so the
        runners/components/dependencies tabs work again without a restart.
        """

        def task():
            if self.utils_conn.check_connection():
                self.manager.checks(install_latest=False, first_run=True)

        RunAsync(task)

    def __maybe_prompt_winebridge_update(self):
        if self._winebridge_dialog_shown or self._showing_onboard:
            return

        if not self.manager:
            return

        if (
            not self.manager.supported_winebridge
            and self._winebridge_prompt_attempts < 5
        ):
            self._winebridge_prompt_attempts += 1
            GLib.timeout_add_seconds(1, self.__maybe_prompt_winebridge_update)
            return

        status = self.manager.winebridge_update_status()
        needs_update = status.get("needs_latest", False)
        missing = status.get("missing", False)
        latest = status.get("latest_supported")
        installed = status.get("installed_identifier")
        offline = not self.utils_conn.check_connection()

        if not (needs_update or missing):
            return

        self._winebridge_dialog_shown = True

        dialog = WineBridgeUpdateDialog(
            self,
            manager=self.manager,
            latest_version=latest,
            installed_version=installed,
            offline=offline,
        )
        dialog.present()

    def __on_start(self):
        """
        This method is called before the window is shown. This check if there
        is at least one local runner installed. If not, the user will be
        prompted with the onboard dialog.
        """

        @GtkUtils.run_in_main_loop
        def set_manager(result: Manager, error=None):
            self.manager = result

            tmp_runners = [
                x for x in self.manager.runners_available if not x.startswith("sys-")
            ]
            if len(tmp_runners) == 0:
                self._showing_onboard = True
                self.show_onboard_view()

            # Pages
            self.page_details = DetailsView(self)
            self.page_list = BottleView(self, arg_bottle=self.arg_bottle)
            self.page_importer = ImporterView(self)
            self.page_library = LibraryView(self)

            self.main_leaf.append(self.page_details)
            self.main_leaf.append(self.page_importer)

            self.main_leaf.get_page(self.page_details).set_navigatable(False)
            self.main_leaf.get_page(self.page_importer).set_navigatable(False)

            self.stack_main.add_titled(
                child=self.page_list, name="page_list", title=_("Bottles")
            ).set_icon_name(f"{APP_ID}-symbolic")
            self.stack_main.add_titled(
                child=self.page_library, name="page_library", title=_("Library")
            ).set_icon_name("library-symbolic")

            self.page_list.search_bar.set_key_capture_widget(self)
            self.btn_search.bind_property(
                "active",
                self.page_list.search_bar,
                "search-mode-enabled",
                GObject.BindingFlags.BIDIRECTIONAL,
            )

            if (
                self.stack_main.get_child_by_name(
                    self.settings.get_string("startup-view")
                )
                is None
            ):
                self.stack_main.set_visible_child_name("page_list")

            self.settings.bind(
                "startup-view",
                self.stack_main,
                "visible-child-name",
                Gio.SettingsBindFlags.DEFAULT,
            )

            self.lock_ui(False)
            self.headerbar.get_style_context().remove_class("flat")

            user_defined_bottles_path = self.manager.data_mgr.get(
                UserDataKeys.CustomBottlesPath
            )
            if user_defined_bottles_path and Paths.bottles != user_defined_bottles_path:
                dialog = Adw.MessageDialog.new(
                    self,
                    _("Custom Bottles Path not Found"),
                    _(
                        "Falling back to default path. No bottles from the given path will be listed."
                    ),
                )
                dialog.add_response("cancel", _("_Dismiss"))
                dialog.present()

            GLib.idle_add(self.__maybe_prompt_winebridge_update)

        def get_manager():
            if self.utils_conn.check_connection():
                SignalManager.connect(
                    Signals.RepositoryFetched, self.page_loading.add_fetched
                )

            # do not redo connection if aborted connection
            mng = Manager(
                g_settings=self.settings,
                check_connection=self.utils_conn.aborted_connections == 0,
            )
            return mng

        self.show_loading_view()
        RunAsync(get_manager, callback=set_manager)

        self.check_crash_log()

    def send_notification(self, title, text, image="", ignore_user=False):
        """
        This method is used to send a notification to the user using
        Gio.Notification. The notification is sent only if the
        user has enabled it in the settings. It is possible to ignore the
        user settings by passing the argument ignore_user=False.
        """
        if ignore_user or self.settings.get_boolean("notifications"):
            notification = Gio.Notification.new(title)
            notification.set_body(text)
            if image:
                icon = Gio.ThemedIcon.new(image)
                notification.set_icon(icon)

            self.props.application.send_notification(None, notification)

    def go_back(self, *_args):
        self.main_leaf.navigate(direction=Adw.NavigationDirection.BACK)

    def show_details_view(self, widget=False, config: Optional[BottleConfig] = None):
        self.main_leaf.set_visible_child(self.page_details)
        self.page_details.set_config(config or BottleConfig())

    def show_loading_view(self, widget=False):
        self.lock_ui()
        self.stack_main.set_visible_child_name("page_loading")

    def show_onboard_view(self, widget=False):
        onboard_window = OnboardDialog(self)
        onboard_window.present(self)

    def show_add_view(self, widget=False):
        new_bottle_dialog = BottlesNewBottleDialog()
        new_bottle_dialog.present(self)

    def show_list_view(self, widget=False):
        self.stack_main.set_visible_child_name("page_list")

    def show_importer_view(self, widget=False):
        self.main_leaf.set_visible_child(self.page_importer)

    def show_prefs_view(self, widget=False, view=0):
        preferences_window = PreferencesWindow(self)
        preferences_window.present()

    def show_download_preferences_view(self, widget=False):
        self.show_prefs_view(widget, view=1)

    def show_runners_preferences_view(self, widget=False):
        self.show_prefs_view(widget, view=2)

    def check_crash_log(self):
        xdg_data_home = GLib.get_user_data_dir()
        log_path = f"{xdg_data_home}/bottles/crash.log"

        with contextlib.suppress(FileNotFoundError):
            with open(log_path, "r") as log_file:
                crash_log = log_file.readlines()
                os.remove(log_path)

            if crash_log:
                CrashReportDialog(self, crash_log).present()

    def __maybe_show_funding_dialog(self):
        if not self._show_funding:
            return

        count = self.data_mgr.get(UserDataKeys.FundingPromptCount) or 0
        self.data_mgr.set(UserDataKeys.FundingPromptCount, count + 1)
        
        today = datetime.now().strftime("%Y-%m-%d")
        self.data_mgr.set(UserDataKeys.LastFundingPrompt, today)

        dialog = FundingDialog(self, show_dont_show=count >= 7)
        dialog.connect("response", self.__funding_response)
        dialog.present()

    def __funding_response(self, dialog, response):
        if response == "dismiss":
            self.data_mgr.set(UserDataKeys.FundingDismissed, True)
            self.settings.set_boolean("show-funding", False)

        dialog.destroy()

    def toggle_selection_mode(self, status: bool = True):
        context = self.headerbar.get_style_context()
        if status:
            context.add_class("selection-mode")
        else:
            context.remove_class("selection-mode")

    def lock_ui(self, status: bool = True):
        widgets = [
            self.btn_add,
            self.view_switcher_title,
        ]
        if self.btn_noconnection.get_visible():
            widgets.append(self.btn_noconnection)
        for w in widgets:
            w.set_visible(not status)

    def show_toast(
        self,
        message,
        timeout=3,
        action_label=None,
        action_callback=None,
        dismissed_callback=None,
    ) -> None:
        toast = Adw.Toast.new(message)
        toast.props.timeout = timeout

        if action_label and action_callback:
            toast.set_button_label(action_label)

            def wrapper_callback(*args):
                action_callback(toast)
                toast.handler_block_by_func(dismissed_callback)

            toast.connect("button-clicked", wrapper_callback)

        if dismissed_callback:
            toast.connect("dismissed", dismissed_callback)

        self.toasts.add_toast(toast)

    def __on_page_changed(self, stack, *args):
        is_bottles_list = stack.get_visible_child_name() == "page_list"
        self.btn_search.set_visible(is_bottles_list)

    @staticmethod
    def proper_close():
        """Properly close Bottles"""
        quit()

    @staticmethod
    def open_url(widget, url):
        webbrowser.open_new_tab(url)
