import stillCenter.constants as constants
import sadb.database
import threading
import os
import logging
import gi
from sam.quick import get_queue_dicts

gi.require_version("Gtk", "4.0")
gi.require_version("Adw", "1")
from gi.repository import Gtk, Adw, GLib, Gio, Gdk

# Import templates for xml builder
import stillCenter.AppStore as AppStore
import stillCenter.AppPage as AppPage
import stillCenter.FlowApps as FlowApps
import stillCenter.NetworkErrors as NetworkErrors
import stillCenter.AppGridView as AppGridView  # Needed for builder
import stillCenter.AppListView as AppListView  # Needed for builder
import stillCenter.CategoryPage as CategoryPage
from stillCenter.i18n import _

from stillCenter.CategoryButtonBig import CategoryButtonBig
from stillCenter.SamInterface import sam_interface, get_sam_error
from stillCenter.LoadingPage import LoadingPage
from stillCenter.ErrorPage import ErrorPage

logger = logging.getLogger(__name__)


class StillCenter(Adw.Application):
    def __init__(self):
        super().__init__(application_id="io.stillhq.stillCenter")

        self.builder = Gtk.Builder()
        self.builder.set_translation_domain(constants.GETTEXT_DOMAIN)
        self.builder.add_from_file(os.path.join(constants.UI_DIR, "stillCenter.ui"))

        # Get objects from the builder
        self.window = self.builder.get_object("window")
        self.sidebar = self.builder.get_object("main_sidebar")
        self.sidebar_split = self.builder.get_object("sidebar_split_view")
        self.main_stack = self.builder.get_object("main_stack")
        self.main_view = self.builder.get_object("main_view")
        self.home_sections = self.builder.get_object("home_sections")
        self.categories_flowbox = self.builder.get_object("categories_flowbox")
        self.search_stack = self.builder.get_object("search_stack")
        self.search_app_list = self.builder.get_object("search_app_list")
        self.search_entry = self.builder.get_object("search_entry")
        self.search_placeholder = self.builder.get_object("search_placeholder")
        self.update_box = self.builder.get_object("update_box")
        self.update_all_button = self.builder.get_object("update_all_button")
        self.available_updates = self.builder.get_object("available_updates")
        self._search_timeout_id = None
        self._search_generation = 0
        self._installed_refresh_lock = threading.Lock()
        self.sam_access_allowed = True
        self._sam_access_warning_shown = False
        self._last_sam_error = None

        export_logs_action = Gio.SimpleAction.new("export-sam-logs", None)
        export_logs_action.connect("activate", self.export_sam_logs)
        self.add_action(export_logs_action)

        # Set Window Name
        self.window.set_title(_("stillCenter"))

        # Setting up the sidebar
        self.sidebar_index = []
        main_stack_pages = self.main_stack.get_pages()
        for i in range(main_stack_pages.get_n_items()):
            stack_page = main_stack_pages.get_item(i)
            sidebar_row = Gtk.ListBoxRow()
            label = Gtk.Label(label=_(stack_page.get_title()), xalign=0)
            sidebar_row.set_child(label)
            self.sidebar_index.append(stack_page.get_name())
            self.sidebar.append(sidebar_row)
        self.sidebar.connect("row-selected", self.sidebar_selected)

        # Populating Categories
        self.categories_flowbox.append(
            CategoryButtonBig(self, "Audio", _("Audio"), "audio-x-generic-symbolic")
        )
        self.categories_flowbox.append(
            CategoryButtonBig(self, "Development", _("Development"), "applications-engineering-symbolic")
        )
        self.categories_flowbox.append(
            CategoryButtonBig(self, "Education", _("Education"), "accessories-dictionary-symbolic")
        )
        self.categories_flowbox.append(
            CategoryButtonBig(self, "Game", _("Games"), "applications-games-symbolic")
        )
        self.categories_flowbox.append(
            CategoryButtonBig(self, "Graphics", _("Graphics"), "applications-graphics-symbolic")
        )
        self.categories_flowbox.append(
            CategoryButtonBig(self, "Network", _("Network"), "preferences-system-network-symbolic")
        )
        self.categories_flowbox.append(
            CategoryButtonBig(self, "Office", _("Office"), "x-office-document-symbolic")
        )
        self.categories_flowbox.append(
            CategoryButtonBig(self, "Utility", _("Utilities"), "applications-utilities-symbolic")
        )
        self.categories_flowbox.append(
            CategoryButtonBig(self, "Video", _("Video"), "applications-multimedia-symbolic")
        )
        self.categories_flowbox.append(
            CategoryButtonBig(self, "System", _("System"), "preferences-other-symbolic")
        )

        # Check for SAM error and push error screen
        sam_error, sam_error_msg = get_sam_error()
        if sam_error:
            logger.error("SAM startup check failed: %s", sam_error_msg)
            errorpage = ErrorPage(
                self, "still App Manager Error", sam_error_msg + \
                "\n\nCheck if the SAM systemd service is running,"
                " or check the systemd status of SAM for possible errors."
            )
            errorpage.push()
            return

        self.sam_access_allowed = self.get_sam_access_allowed()

        # Setup search
        self.search_entry.connect("changed", self.search_changed)

        # Loading base pages
        self.app_page = AppPage.AppPage(self)
        self.category_page = CategoryPage.CategoryPage(self)

        # Set update button
        sam_interface.connect_to_signal("queue_changed", self.run_update_installed_page)
        sam_interface.connect_to_signal("error_occurred", self.sam_error_occurred)
        self.update_all_button.connect("clicked", self.update_all)
        self.builder.get_object("installed").set_store(self, Gio.ListStore())
        self.available_updates.set_store(self, Gio.ListStore())
        self.available_updates.set_visible(False)
        self.update_box.set_visible(False)

        # Loading screen
        self.loading_screen = LoadingPage(self)

        self.loading_screen.push()
        thread = threading.Thread(target=self.load_apps, daemon=True)
        thread.start()

    def set_loading_status(self, title: str, description: str = ""):
        GLib.idle_add(self.loading_screen.set_status, title, description)

    def get_sam_access_allowed(self) -> bool:
        try:
            return bool(sam_interface.can_access())
        except Exception:
            logger.exception("Failed to query SAM access; assuming access is allowed")
            return True

    def show_browse_only_warning(self):
        if self._sam_access_warning_shown or self.sam_access_allowed:
            return

        dialog = AppPage.TextDialog(
            _("Limited access"),
            _("You do not have permission to install, update, or remove apps with stillCenter.\n\n"
              "You can still browse the app catalog and view app details.")
        )
        dialog.present(self.window)
        self._sam_access_warning_shown = True

    def show_install_permission_denied_dialog(self, app_name: str | None = None):
        message = _("You do not have permission to install apps with stillCenter.\n\n")
        if app_name:
            message += _("Ask your administrator to install {app_name} for you.").format(app_name=app_name)
        else:
            message += _("Ask your administrator to install the app for you.")

        dialog = AppPage.TextDialog(_("Install permission required"), message)
        dialog.present(self.window)

    def show_manage_permission_denied_dialog(self, action_name: str):
        dialog = AppPage.TextDialog(
            _("Administrator permission required"),
            _("You do not have permission to {action_name} apps with stillCenter.\n\n"
              "Ask your administrator to manage this app for you.").format(action_name=action_name)
        )
        dialog.present(self.window)

    def sam_error_occurred(self, action):
        GLib.idle_add(self.show_sam_error_notification, dict(action))

    def show_sam_error_notification(self, action: dict):
        self._last_sam_error = action
        app_name = str(action.get("app_name") or action.get("package_id") or _("Unknown app"))
        task = str(action.get("task") or _("manage")).lower()
        failure_kind = str(action.get("failure_kind") or "unknown")
        can_export_logs = str(action.get("can_export_logs", "False")).lower() == "true"

        if failure_kind == "network":
            body = _("The network connection was lost while managing {app_name}. SAM will resume when the connection is back.").format(app_name=app_name)
        elif failure_kind == "storage":
            body = _("SAM could not access the required storage while managing {app_name}. It will retry automatically.").format(app_name=app_name)
        else:
            body = _("SAM failed to {task} {app_name}.").format(task=task, app_name=app_name)

        notification = Gio.Notification.new(_("SAM action failed"))
        notification.set_body(body)
        notification.set_priority(Gio.NotificationPriority.HIGH)
        if can_export_logs:
            notification.add_button(_("Export Logs"), "app.export-sam-logs")
        self.send_notification("sam-action-failed", notification)
        return GLib.SOURCE_REMOVE

    def export_sam_logs(self, *_args):
        thread = threading.Thread(target=self._export_sam_logs_thread, daemon=True)
        thread.start()

    def _export_sam_logs_thread(self):
        try:
            export_path = str(sam_interface.export_logs())
            GLib.idle_add(self.show_log_export_success, export_path)
        except Exception as error:  # pylint: disable=broad-exception-caught
            logger.exception("Failed to export SAM logs")
            GLib.idle_add(self.show_log_export_failure, str(error))

    def show_log_export_success(self, export_path: str):
        notification = Gio.Notification.new(_("SAM logs exported"))
        notification.set_body(_("Logs were saved to {path}.").format(path=export_path))
        self.send_notification("sam-logs-exported", notification)
        return GLib.SOURCE_REMOVE

    def show_log_export_failure(self, error: str):
        dialog = AppPage.TextDialog(
            _("Could not export SAM logs"),
            error
        )
        dialog.present(self.window)
        return GLib.SOURCE_REMOVE

    def update_all(self, button):
        if not self.sam_access_allowed:
            self.show_manage_permission_denied_dialog("update")
            return
        logger.info("User requested update-all from stillCenter")
        thread = threading.Thread(target=lambda: sam_interface.update_all(False), daemon=True)
        thread.start()

    def run_update_installed_page(self):
        thread = threading.Thread(target=self.update_installed_page, daemon=True)
        thread.start()

    def load_cached_installed_page(self):
        try:
            AppStore.refresh_installed_store()
        except Exception:
            logger.exception("Failed to load cached installed app database")
            return

        GLib.idle_add(self.update_installed_gui)

    # Update installed page
    def update_installed_page(self):
        if not self._installed_refresh_lock.acquire(blocking=False):
            logger.info("Installed app refresh is already running")
            return

        try:
            try:
                logger.info("Refreshing installed app database")
                sadb.database.update_installed()
            except FileNotFoundError as error:
                # Some sources (e.g. SWAI) may reference removed desktop files.
                # Skip those entries instead of crashing the background loader.
                logger.warning("Skipping unreadable desktop entry: %s", error)
            except Exception:
                logger.exception("Failed to refresh installed app database")

            AppStore.refresh_installed_store()

            GLib.idle_add(self.update_installed_gui)
        finally:
            self._installed_refresh_lock.release()

    def update_installed_gui(self):
        # Set models of ListViews
        self.builder.get_object("installed").set_store(self, AppStore.INSTALLED_STORE["no_update"])
        self.builder.get_object("available_updates").set_store(self, AppStore.INSTALLED_STORE["update"])

        if AppStore.INSTALLED_STORE["update"].get_n_items() == 0:
            self.available_updates.set_visible(False)
            self.update_box.set_visible(False)
        else:
            self.available_updates.set_visible(True)
            self.update_box.set_visible(True)

        queue = get_queue_dicts()
        if not queue:
            queue = []  # Prevent function from breaking if queue is empty
        if len(queue) != 0:
            self.update_all_button.set_sensitive(False)
        else:
            self.update_all_button.set_sensitive(True)

        return GLib.SOURCE_REMOVE

    def load_apps(self):
        try:
            logger.info("Loading app repository")
            self.set_loading_status(
                _("Downloading app data..."),
                _("Fetching the latest app catalog for stillCenter...")
            )
            sadb.database.update_db()

            self.set_loading_status(
                _("Building app index..."),
                _("Preparing the searchable app list...")
            )
            AppStore.refresh_app_store()
            self.load_cached_installed_page()
            logger.info("App repository loaded successfully")
        except Exception as error:  # pylint: disable=broad-exception-caught
            logger.exception("Failed to load app repository")
            GLib.idle_add(self.show_repo_error, error)
            return

        self.set_loading_status(
            _("Loading homepage..."),
            _("Preparing featured sections and finishing startup...")
        )
        GLib.idle_add(lambda: self.populate_apps())
        GLib.idle_add(self.loading_screen.pop)
        self.run_update_installed_page()

    def retry_initial_load(self, error_page=None):
        if error_page is not None:
            error_page.pop()

        logger.info("Retrying initial stillCenter load")
        self.loading_screen.set_status(
            _("Retrying startup..."),
            _("Trying to connect again and finish loading stillCenter...")
        )
        self.loading_screen.push()
        thread = threading.Thread(target=self.load_apps, daemon=True)
        thread.start()

    def show_repo_error(self, error):
        logger.error("Showing repository error to user: %s", error)
        self.loading_screen.pop()
        title = _("App repository could not be loaded")
        description = _("stillCenter could not load the app repository.\n\n{error}").format(error=error)

        if NetworkErrors.is_connection_error(error):
            title = NetworkErrors.get_connection_error_title(error)
            description = NetworkErrors.build_connection_error_description("the app repository", error)

        errorpage = ErrorPage(
            self,
            title,
            description
        )
        errorpage.set_icon("network-error-symbolic")
        errorpage.set_retry(self.retry_initial_load)
        errorpage.push()
        return GLib.SOURCE_REMOVE

    def populate_apps(self):
        self.search_placeholder.set_title(_("Search from {app_count} Apps").format(app_count=len(AppStore.STORE["all"])))
        FlowApps.populate_apps(self.home_sections, self)

    def sidebar_selected(self, _listbox, row):
        self.main_stack.set_visible_child_name(self.sidebar_index[row.get_index()])
        if self.sidebar_split.get_collapsed() and not self.sidebar_split.get_show_content():
            self.sidebar_split.set_show_content(True)

    def search_changed(self, entry):
        # Cancel any pending search
        if self._search_timeout_id:
            GLib.source_remove(self._search_timeout_id)
        
        # Wait 250ms before searching to wait for user to stop typing
        # Removed immediate search call to prevent duplicate searches
        self._search_timeout_id = GLib.timeout_add(250, self.do_search, entry.get_text())
        logger.debug("Scheduled search timeout id %s", self._search_timeout_id)


    def do_search(self, query: str):
        self._search_timeout_id = None
        query = query.strip()
        self._search_generation += 1
        search_generation = self._search_generation

        if len(query) >= 2:  # Require at least 2 characters for search
            threading.Thread(
                target=self.run_search,
                args=(query, search_generation),
                daemon=True,
            ).start()
        else:
            self.search_stack.set_visible_child_name("search_placeholder")
        return GLib.SOURCE_REMOVE

    def run_search(self, query: str, search_generation: int):
        try:
            results = AppStore.search_rows(query)
        except Exception:
            logger.exception("Search failed for query: %s", query)
            results = []

        GLib.idle_add(self.apply_search_results, query, search_generation, results)

    def apply_search_results(self, query: str, search_generation: int, results: list):
        if search_generation != self._search_generation:
            return GLib.SOURCE_REMOVE

        if self.search_entry.get_text().strip() != query:
            return GLib.SOURCE_REMOVE

        if len(results) == 0:
            self.search_stack.set_visible_child_name("no_results")
            return GLib.SOURCE_REMOVE

        self.search_app_list.set_store(self, AppStore.rows_to_store(results))
        self.search_stack.set_visible_child_name("results")
        return GLib.SOURCE_REMOVE

    def do_activate(self):
        self.window.set_application(self)
        self.window.present()
        self.show_browse_only_warning()
