# -*- coding: utf-8 -*-
#  _____  ____   _               _
# |_   _|| __ ) | |  ___    ___ | | __
#   | |  |  _ \ | | / _ \  / __|| |/ /
#   | |  | |_) || || (_) || (__ |   <
#   |_|  |____/ |_| \___/  \___||_|\_\
#
# Copyright (C) 2021-2025 Camelia Lavender <camelia@tblock.me>

# 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, either version 3 of the License, or
# (at your option) any later version.

# 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 <https://www.gnu.org/licenses/>.

# Standard libraries
import threading
import gettext
from typing import List, AnyStr

# External libraries
import gi
import tblock
import tblock.utils

# Local libraries
from ._polkit import pk_subprocess_run
from .config import GUISettings
from .const import ExitStatusCode

gi.require_version("Gtk", "3.0")
# pylint: disable=wrong-import-position,wrong-import-order
from gi.repository import Gtk, Gdk

gettext.textdomain("tblock-gui")
_ = gettext.gettext


class ThreadWasKilledError(IOError):
    """
    Error class that is raised when a thread is killed
    """


class JobController:
    """
    Base class for threaded jobs used by TBlock GUI
    """

    # pylint: disable=too-many-instance-attributes,too-many-arguments

    def __init__(
        self,
        application: Gtk.Application,
        status_bar: Gtk.ProgressBar,
        status_label: Gtk.Label,
        finish_button: Gtk.Button,
        return_button: Gtk.Button = None,
    ) -> None:
        """
        Set up the class
        :param application: Member of Gtk.Application type
        :param status_bar: The status bar to modify
        :param status_label: The label to change
        :param finish_button: The button to show when the job is done
        :param return_button: Optional. A second button to show when the job is done
        """
        self._application = application
        self.status_bar = status_bar
        self.status_label = status_label
        self.finish_button = finish_button
        self.return_button = return_button
        self.total = 0
        self.count = 0
        self.thread_data = {}
        self._thread = None
        self.running = False
        self.killed = False
        self.total_errors = 0

    def kill(self) -> None:
        """
        Kill the thread
        """
        if self.running:
            self.killed = True

    def _raise_error(self, message: AnyStr) -> None:
        """
        Raise an error within a thread
        :param message: The error message
        """
        Gdk.threads_enter()
        self._application.error_window.raise_error(message)
        Gdk.threads_leave()
        self.total_errors += 1

    def __widget_update(self, message: AnyStr):
        """
        Update label and progress bar within the thread
        :param message: The message to show
        """
        self._stop_when_killed()
        Gdk.threads_enter()
        self.status_bar.set_fraction(self.count / self.total)
        self.status_label.set_text(message)
        Gdk.threads_leave()
        self.count += 1

    def __pre(self):
        """
        Prepare the job (has to be run at the beginning of EACH thread)
        """
        self.running = True
        if tblock.utils.db_is_locked():
            self.total_errors = 1
            Gdk.threads_enter()
            self._application.error_window.raise_db_locked_error()
            Gdk.threads_leave()
            self.kill()
            self._stop_when_killed()
        else:
            Gdk.threads_enter()
            self.status_bar.set_fraction(0)
            self.status_label.set_text(_("Nothing to do."))
            self.count = 0
            self.total = 0
            self.total_errors = 0
            self.finish_button.hide()
            if self.return_button is not None:
                self.return_button.hide()
            Gdk.threads_leave()

    def __post(self):
        """
        Cleanup the job (has to be run at the end of EACH thread)
        """
        Gdk.threads_enter()
        self.status_bar.set_fraction(1)
        message = _("You can now press Close to close this dialog.")
        if self.total_errors == 0:
            self.status_label.set_text(
                _("Operation finished successfully.") + "\n" + message
            )
        else:
            self.status_label.set_text(
                _("Operation finished with {} error(s).").format(self.total_errors)
                + "\n"
                + message
            )
        self.finish_button.show()
        if self.return_button is not None:
            self.return_button.show()
        Gdk.threads_leave()
        self.running = False

    def manage_filter_lists(
        self,
        filter_lists: List,
        sync: bool = False,
        build_hosts: bool = True,
        unsubscribe: bool = False,
    ):
        """
        Manage filter lists
        :param filter_lists: The filter lists to subscribe/unsubscribe to
        :param sync: Whether to sync the filter list repository index
        :param build_hosts: Whether to build the hosts file at the end of the job
        :param unsubscribe: If True, subscribed lists specified in filter_lists will be removed. Defaults to False
        """
        self.thread_data = {
            "filter_lists": filter_lists,
            "sync": sync,
            "build_hosts": build_hosts,
            "unsubscribe": unsubscribe,
        }
        self._thread = threading.Thread(target=self.__thread_manage_filter_lists)
        self._thread.daemon = True
        self.running = True
        self._thread.start()

    def __thread_manage_filter_lists(self):
        """
        Manage filter lists within a thread
        """
        # pylint: disable=too-many-branches
        try:
            self.__pre()
            self.total = (len(self.thread_data["filter_lists"]) * 3) + 1
            self.total += (int(self.thread_data["sync"]) * 4) + int(
                self.thread_data["build_hosts"]
            )
            if self.thread_data["sync"]:
                self._sync()
            for i in self.thread_data["filter_lists"]:
                _f = tblock.Filter(i, quiet=True)
                if not _f.subscribing:
                    if self._download(_f):
                        if self._subscribe_to(_f):
                            self._update(_f)
                        else:
                            self.count += 1
                    else:
                        self.count += 2
                elif self.thread_data["unsubscribe"]:
                    self._unsubscribe_from(_f)
                    self._rm_cache(_f)
                    self.count += 1
                else:
                    self.count += 3
            if self.thread_data["filter_lists"]:
                self._rm_matches()
            else:
                self.count += 1
            if self.thread_data["build_hosts"]:
                self._build()
            if (
                pk_subprocess_run(["setting", GUISettings.SETUP_COMPLETED.value, "1"])
                != ExitStatusCode.NO_ERROR
            ):
                self._raise_error(_("Failed to change GUI settings."))
            self.__post()
        except ThreadWasKilledError:
            pass

    def update_filter_lists(self):
        """
        Update all filter lists
        """
        self.thread_data = {
            "filter_lists": tblock.filters.get_all_filter_lists(subscribing_only=True),
        }
        self._thread = threading.Thread(target=self.__thread_update_filter_lists)
        self._thread.daemon = True
        self.running = True
        self._thread.start()

    def __thread_update_filter_lists(self):
        """
        Update all filter lists within a thread
        """
        try:
            self.__pre()
            self.total = (len(self.thread_data["filter_lists"]) * 2) + 1
            for i in self.thread_data["filter_lists"]:
                _f = tblock.Filter(i, quiet=True)
                if self._download(_f):
                    self._update(_f)
                else:
                    self.count += 1
            self._rm_matches()
            self._build()
            self.__post()
        except ThreadWasKilledError:
            pass

    def add_custom_filter_list(
        self,
        id_: AnyStr,
        source: AnyStr,
        syntax: AnyStr = None,
    ):
        """
        Add a custom filter list & subscribe to it
        :param id_: The ID to use
        :param source: The source to use
        :param syntax: Optional. Specify the syntax used by the filter list
        """
        self.thread_data = {
            "id": id_,
            "source": source,
            "syntax": syntax,
        }
        self._thread = threading.Thread(target=self.__thread_add_custom_filter_list)
        self._thread.daemon = True
        self.running = True
        self._thread.start()

    def __thread_add_custom_filter_list(self):
        """
        Add a custom filter list & subscribe to it within a thread
        """
        try:
            self.__pre()
            self.total = 6
            _f = tblock.Filter(
                self.thread_data["id"], custom_source=self.thread_data["source"]
            )
            if self._add_custom(_f, syntax=self.thread_data["syntax"]):
                if self._download(_f):
                    if self._subscribe_to(_f):
                        self._update(_f)
                    else:
                        self.count += 1
                else:
                    self.count += 2
            else:
                self.count += 3
            self._rm_matches()
            self._build()
            self.__post()
        except ThreadWasKilledError:
            pass

    def _sync(self) -> None:
        """
        Sync the filter list repository within a thread
        """
        self.__widget_update(_("Creating database"))
        if pk_subprocess_run(["init"]) != ExitStatusCode.NO_ERROR:
            self._raise_error(_("Failed to create database"))
        self.__widget_update(_("Syncing filter list repository index"))
        if pk_subprocess_run(["sync"]) != ExitStatusCode.NO_ERROR:
            self._raise_error(_("Failed to sync filter list repository index."))
        self.__widget_update(_("Downloading icon pack"))
        if pk_subprocess_run(["icon"]) != ExitStatusCode.NO_ERROR:
            self._raise_error(_("Failed to download icon pack."))
            self.count += 1
        else:
            self.__widget_update(_("Extracting icon pack"))
            if pk_subprocess_run(["extract"]) != ExitStatusCode.NO_ERROR:
                self._raise_error(_("Failed to extract icon pack."))

    def _build(self) -> None:
        """
        Build the hosts file within a thread
        """
        self.__widget_update(_("Building hosts file"))
        if pk_subprocess_run(["build"]) != ExitStatusCode.NO_ERROR:
            self._raise_error(_("Failed to build hosts file."))

    def _add_custom(self, filter_obj: tblock.Filter, syntax: AnyStr) -> bool:
        """
        Add a custom filter list within a thread
        :return: bool
        """
        self.__widget_update(_("Adding custom filter list: {}").format(filter_obj.id))
        if (
            pk_subprocess_run(
                ["add", filter_obj.id, filter_obj.source] + ([syntax] if syntax else [])
            )
            != ExitStatusCode.NO_ERROR
        ):
            self._raise_error(
                _("Failed to add custom filter list: {}.").format(filter_obj.id)
            )
            return False
        return True

    def _download(self, filter_obj: tblock.Filter) -> bool:
        """
        Download a filter list within a thread
        :return bool:
        """
        title = filter_obj.metadata["title"] if filter_obj.on_repo else filter_obj.id
        self.__widget_update(_("Retrieving: {}").format(title))
        if pk_subprocess_run(["download", filter_obj.id]) != ExitStatusCode.NO_ERROR:
            self._raise_error(_("Failed to download: {}").format(title))
            return False
        return True

    def _subscribe_to(self, filter_obj: tblock.Filter) -> bool:
        """
        Subscribe to a filter list within a thread
        :return bool:
        """
        title = filter_obj.metadata["title"] if filter_obj.on_repo else filter_obj.id
        self.__widget_update(_("Subscribing to: {}").format(title))
        if pk_subprocess_run(["subscribe", filter_obj.id]) != ExitStatusCode.NO_ERROR:
            self._raise_error(_("Failed to subscribe to: {}").format(title))
            return False
        return True

    def _unsubscribe_from(self, filter_obj: tblock.Filter) -> bool:
        """
        Unsubscribe from a filter list within a thread
        :return bool:
        """
        title = filter_obj.metadata["title"] if filter_obj.on_repo else filter_obj.id
        self.__widget_update(_("Unsubscribing from: {}").format(title))
        if pk_subprocess_run(["remove", filter_obj.id]) != ExitStatusCode.NO_ERROR:
            self._raise_error(_("Failed to unsubscribe from: {}").format(title))
            return False
        return True

    def _rm_cache(self, filter_obj: tblock.Filter) -> None:
        """
        Remove a filter list from the cache within a thread
        """
        title = filter_obj.metadata["title"] if filter_obj.on_repo else filter_obj.id
        self.__widget_update(_("Removing from cache: {}").format(title))
        pk_subprocess_run(["rm-cache", filter_obj.id])

    def _update(self, filter_obj: tblock.Filter) -> bool:
        """
        Update a filter list within a thread
        :return bool:
        """
        title = filter_obj.metadata["title"] if filter_obj.on_repo else filter_obj.id
        self.__widget_update(_("Updating: {}").format(title))
        if pk_subprocess_run(["update", filter_obj.id]) != ExitStatusCode.NO_ERROR:
            self._raise_error(_("Failed to update rules from: {}").format(title))
            return False
        return True

    def _rm_matches(self) -> None:
        """
        Remove rules that are allowed within a thread
        """
        self.__widget_update(_("Removing allowed matches"))
        if pk_subprocess_run(["remove-matches"]) != ExitStatusCode.NO_ERROR:
            self._raise_error(_("Failed to apply allowing rules."))

    def _stop_when_killed(self) -> None:
        if self.running and self.killed:
            self.running = False
            self.killed = False
            raise ThreadWasKilledError()
