import logging
import threading

from typing import Any, TYPE_CHECKING, Union

import dzgui.util._json as JSON  # noqa


from dzgui.const.enum import (
    FilterMode,
    Preferences,
    NotebookPage,
    ButtonType,
    ContextMenu,
)

from dzgui.config.userprefs import UserPrefs
from dzgui.const.constants import APP_NAME
from dzgui.controllers.emitter import Emitter
from dzgui.managers.config import ConfigManager
from dzgui.managers.connection import ConnectionManager
from dzgui.managers.contextmenu import ContextMenuManager
from dzgui.managers.mods import ModManager
from dzgui.managers.notes import NoteManager
from dzgui.model.servers import ServerModelManager
from dzgui.util.diag import write_diagnostic
from dzgui.util.format import format_player_count
from dzgui.util.open_links import open_user_workshop, open_workshop_page
from dzgui.views.dialogs.filepicker import FilePicker
from dzgui.views.dialogs.generic import ExceptionDialog

import gi

gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gdk, GLib, GObject  # noqa E402

logger = logging.getLogger(APP_NAME)

if TYPE_CHECKING:
    from dzgui.api.servers import Record
    from dzgui.const.enum import ServerTab
    from dzgui.managers.connection import Prerequisites
    from dzgui.managers.filter import FilterManager
    from dzgui.util.dist import Haversine
    from dzgui.views.base import Notebook, Grid, OuterWindow
    from dzgui.views.components.buttonbox import ContextualButton
    from dzgui.views.components.filter_panel import FilterPanel
    from dzgui.views.components.right_panel import RightPanel
    from dzgui.views.components.statusbar import Statusbar
    from dzgui.views.pages.options import Options
    from dzgui.views.pages.preconnect import PreConnectionAssistant
    from dzgui.views.pages.servers import ServerNotebook
    from dzgui.views.trees.tree_log import LogTreeView
    from dzgui.views.trees.tree_menu import MenuTreeView
    from dzgui.views.trees.tree_mods import ModTreeView
    from dzgui.views.trees.tree_servers import ServerTreeView


class AppNavigation:
    window: "OuterWindow"
    grid: "Grid"
    right_panel: "RightPanel"
    statusbar: "Statusbar"
    notebook: "Notebook"
    modtreeview: "ModTreeView"
    menu: "MenuTreeView"
    servers: "ServerNotebook"
    saved: "ServerTreeView"
    recent: "ServerTreeView"
    lan: "ServerTreeView"
    logtreeview: "LogTreeView"
    filters: "FilterPanel"
    preconnect: "PreConnectionAssistant"
    options: "Options"


class Controller(GObject.GObject):
    def __init__(self) -> None:
        self.dist_cache: dict[str, "Haversine"] = {}
        self.mediator = AppNavigation()

        self.prefs: UserPrefs

        self.emitter = Emitter()
        self.emitter.connect("map_selection_changed", self._on_map_selection_changed)
        self.emitter.connect("check_toggled", self._on_check_toggled)

        # NOTE: suppress requests until entire UI is loaded
        self.loaded = False
        self.pending_jobs = 1

        self.exit_event = threading.Event()
        self.cancel_event = threading.Event()
        self.connection_man: ConnectionManager

    def get_emitter(self) -> Emitter:
        return self.emitter

    def register_widget(self, attr: str, widget: Gtk.Widget) -> None:
        try:
            setattr(self.mediator, attr, widget)
        except AttributeError:
            logger.critical(f"{attr} is not a valid AppNavigation attribute.")

    def get_prefs(self) -> UserPrefs:
        return self.prefs

    def set_prefs(self, prefs: UserPrefs) -> None:
        self.config_man = ConfigManager(prefs, self)
        self.notes_man = NoteManager(self, prefs.paths.notes)

        self.prefs = prefs

    def query_config(self, key: Preferences) -> Any:
        return self.config_man.lookup(key)

    def is_auto_install(self) -> bool:
        return bool(self.query_config(Preferences.INSTALL))

    def suppress_signal(
        self, owner: Any, child: Any, func_name: str, state: bool
    ) -> None:

        func = getattr(owner, func_name)
        if state:
            child.handler_block_by_func(func)
        else:
            child.handler_unblock_by_func(func)

    def get_active_context(self) -> "ServerTab":
        return self.get_active_treeview().get_enum()

    def get_active_treeview(self) -> "ServerTreeView":
        return self.mediator.notebook.servers.get_active_treeview()

    def grab_active_treeview(self) -> None:
        self.get_active_treeview().grab_focus()

    def save_res_and_quit(self, *args: Any) -> None:
        treeview = self.get_active_treeview()
        window = self.get_window()
        self.config_man.save_res_and_quit(treeview, window)

    def set_statusbar(self, enum: "NotebookPage | ServerTab", text: str) -> None:
        self.mediator.statusbar.set_by_context(enum, text)

    def remove_statusbar(self, context: "NotebookPage | ServerTab") -> None:
        self.mediator.statusbar.pop(context)

    def set_statusbar_dist(
        self, haversine: Union["Haversine", None], enum: "ServerTab"
    ) -> None:
        """
        NOTE: prevents race condition when server tab changed,
        but allows caching the distance in the background
        """
        context = self.get_active_context()
        page = self.mediator.notebook.get_page_by_enum()
        if page != NotebookPage.SERVERS:
            self.emitter.emit("distcalc_ended", None, context)
            return
        if enum != context:
            self.emitter.emit("distcalc_ended", None, context)
            return

        if haversine is None:
            self.emitter.emit("distcalc_ended", None, context)
            return
        dist = haversine.get_rounded(self.prefs.use_miles)
        self.emitter.emit("distcalc_ended", dist, context)

    def toggle_config(self, key: Preferences) -> None:
        # NOTE: Preferences.DIST is dynamic
        if key == Preferences.DIST:
            self.prefs.use_miles = not self.prefs.use_miles
        self.config_man.toggle_config(key)

    def update_config(self, key: Preferences, value: str) -> None:
        self.config_man.update_config(key, value)

    def open_keybindings(self) -> None:
        notebook = self.mediator.grid.notebook
        notebook.toggle_keybindings()

    def focus_notebook(self) -> None:
        notebook = self.mediator.grid.notebook
        notebook.focus_current()

    def show_developers_page(self) -> None:
        self.open_page(NotebookPage.DEVELOPERS)

    def open_page(self, page: NotebookPage) -> None:
        self.mediator.grid.notebook.set_page_by_enum(page)

    def open_page_by_button(self, button: "ContextualButton") -> None:
        # TODO: consolidate methods with set_page_by_enum
        match button.context:
            case ButtonType.EXIT:
                self.save_res_and_quit()
                return
            case ButtonType.OPTIONS:
                try:
                    # TODO: where to put config file check
                    self.mediator.grid.notebook.settings.populate_settings()
                except Exception:
                    return
            case ButtonType.MODS:
                self.load_mods()

        self.open_page(button.opens)

    def get_help_row(self) -> str:
        return self.mediator.menu.get_row_enum()

    def open_user_workshop(self, uid: str) -> None:
        # NOTE: uid may contain leading zeroes, not a real integer
        client = self.query_config(Preferences.CLIENT)
        open_user_workshop(uid, client)

    def load_mods(self) -> None:
        self.mod_man = ModManager(self)

    def uncolorize_mods(self) -> None:
        self.mod_man.uncolorize_mods()

    def highlight_stale(self) -> None:
        self.mod_man.highlight_stale()

    def toggle_mod_selection(self, state: bool) -> None:
        self.mod_man.toggle_mod_selection(state)

    def delete_mods(self) -> None:
        self.mod_man.delete_mods()

    def get_mod_store(self) -> Gtk.TreeModel | None:
        return self.mediator.modtreeview.get_model()

    def set_fav(self, name: str, record: str, simple_ip: str) -> None:
        try:
            self.config_man.set_fav(name, record)
            self.emitter.emit("fav_server_changed", name, simple_ip)
        except Exception as e:
            logger.critical(e)
            return

    def menu_action(self, action: ContextMenu, tree: "ServerTreeView") -> None:
        context_man = ContextMenuManager(tree, self)
        context_man.process(action)

    def populate_log(self) -> None:
        log = self.prefs.paths.debug
        try:
            self.mediator.logtreeview.populate_log(log)
            self.open_page(NotebookPage.LOG)
        except Exception as e:
            dialog = ExceptionDialog(self, str(e))
            dialog.run()

    def select_colorized(self) -> None:
        self.mod_man.select_colorized()

    def get_all_tree_filters(self) -> dict[str, dict[str, bool]]:
        servers = self.get_servers()
        all_filters: dict[str, dict[str, bool]] = {}
        trees = (
            servers.get_browser(),
            servers.get_saved(),
            servers.get_recent(),
            servers.get_lan(),
        )
        for tree in trees:
            enum = str(tree.get_enum())
            filters = tree.get_filter_man().get_filters()
            all_filters[enum] = filters
        return all_filters

    def get_filter_man(self) -> "FilterManager":
        """Each ServerTreeView has an atomic FilterManager"""
        return self.get_active_treeview().get_filter_man()

    def dump_diagnostics(self) -> None:
        picker = FilePicker(self.mediator.window)
        file = picker.pick_file()
        if file is not None:
            try:
                write_diagnostic(self.prefs.paths.config, file)
            except Exception as e:
                dialog = ExceptionDialog(self, str(e))
                dialog.run()

    def update_api_key(self, key: Preferences, text: str) -> None:
        self.config_man.update_api_key(key, text)

    def set_resolution(self, window: "OuterWindow") -> None:
        self.config_man.set_resolution(window)

    def propagate_column_width(self, col: Gtk.TreeViewColumn) -> None:
        GLib.idle_add(self.mediator.servers.update_tab_widths, col)

    def refresh_tree(self) -> None:
        treeview = self.get_active_treeview()
        treeview.set_loaded(False)
        ServerModelManager(self, treeview).refresh()

    def get_player_count(self) -> str:
        treeview = self.get_active_treeview()
        model, control = treeview.get_model_and_control_model()
        count = format_player_count(model, control)
        return count

    def get_statusbar(self) -> "Statusbar":
        return self.mediator.statusbar

    def populate_model(self, tv: "ServerTreeView") -> None:
        # NOTE: skip on previously loaded tabs
        if tv.is_loaded():
            self.emitter.emit("servers_loaded", tv.get_enum())
            return
        self.mediator.statusbar.set_by_context(tv.get_enum(), "")
        self.mediator.filters.button_grid.block_toggles(True)
        self.populate_filter_prefs()
        self.mediator.filters.button_grid.block_toggles(False)
        self.emitter.emit("lan_page_initialized")
        ServerModelManager(self, tv).load()

    def populate_filter_prefs(self) -> None:
        tv = self.get_active_treeview()
        filters = self.config_man.get_filters(tv)
        if filters is None:
            return
        filter_man = self.get_filter_man()
        for filt, state in filters.items():
            filter_man.set_filter(filt, state)
        self.mediator.filters.set_filters(filters)

    def get_dist_cache(self) -> dict[str, "Haversine"]:
        return self.dist_cache

    def set_dist_cache(self, addr: str, haversine: "Haversine") -> None:
        self.dist_cache[addr] = haversine

    def get_map_store(self) -> Gtk.ListStore:
        filter_man = self.get_filter_man()
        return filter_man.get_map_store()

    def get_enabled_filters(self) -> dict:
        filter_man = self.get_filter_man()
        return filter_man.get_filters()

    # FIXME: wipe maps store when changing tabs if model is none

    def has_server_model(self) -> bool:
        treeview = self.get_active_treeview()
        proxy_man = treeview.get_proxy_man()
        control = proxy_man.get_control()
        if control is None:
            return False
        if len(control) < 1:
            return False
        return True

    def _on_check_toggled(self, emitter: Emitter, label: str, state: bool) -> None:
        filter_man = self.get_filter_man()
        filter_man.set_filter(label, state)

        mode = FilterMode.TOGGLE_ON if state else FilterMode.TOGGLE_OFF
        ServerModelManager(self, self.get_active_treeview()).refilter(mode)

    def _on_map_selection_changed(self, emitter: Emitter, selection: str) -> None:
        smm = ServerModelManager(self, self.get_active_treeview())
        smm.refilter(FilterMode.TOGGLE_ON)

    def get_notebook(self) -> "Notebook":
        return self.mediator.notebook

    def get_prior_page(self) -> NotebookPage:
        return self.mediator.notebook.get_prior_page()

    def get_servers(self) -> "ServerNotebook":
        return self.mediator.servers

    def get_server_notebook(self) -> Gtk.Notebook:
        return self.mediator.servers.notebook

    def get_window(self) -> "OuterWindow":
        return self.mediator.window

    def get_menu(self) -> "MenuTreeView":
        return self.mediator.menu

    def has_favorites(self) -> bool:
        favs = self.config_man.get_favorites()
        if len(favs) < 1:
            return False
        return True

    def get_config_man(self) -> ConfigManager:
        return self.config_man

    def add_by_str(self, addr: str) -> None:
        saved_tree = self.get_servers().get_saved()
        ServerModelManager(self, saved_tree).add_by_str(addr)

    def add_by_record(self, record: "Record") -> None:
        saved_tree = self.get_servers().get_saved()
        ServerModelManager(self, saved_tree).add_by_record(record)

    def remove_by_record(self, record: "Record") -> None:
        saved_tree = self.get_servers().get_saved()
        ServerModelManager(self, saved_tree).remove_by_record(record)

    def add_to_history(self, record: dict[str, Any]) -> None:
        tv = self.get_servers().get_recent()
        ServerModelManager(self, tv).add_to_history(record)

    def remove_from_history(self, record: "Record") -> None:
        # NOTE: remove action is only possible from history tree context menu,
        # i.e., the active tree
        tv = self.get_active_treeview()
        ServerModelManager(self, tv).remove_from_history(record)

    def connect_by_str(self, addr: str) -> None:
        if addr.isdigit():
            config_man = self.get_config_man()
            key = config_man.lookup(Preferences.BM)
            self.connection_man = ConnectionManager(self)
            self.connection_man.connect_by_id(int(addr), key)
        else:
            self.connection_man = ConnectionManager(self)
            self.connection_man.connect_by_ip(addr)

    def connect_by_record(self, record: "Record") -> None:
        self.connection_man = ConnectionManager(self)
        self.connection_man.connect_by_record(record)

    def get_details(self, record: "Record") -> None:
        ConnectionManager(self).query_details(record)

    def get_modlist(self, record: "Record") -> None:
        ConnectionManager(self).query_modlist_and_present(record)

    def get_server_name(self) -> str:
        tv = self.get_active_treeview()
        return tv.get_name()

    def open_workshop_page(self, mod: str) -> None:
        cmd = self.query_config(Preferences.CLIENT)
        open_workshop_page(mod, cmd)

    def has_note(self) -> bool:
        note = self.get_note()
        if len(note) > 0:
            return True
        return False

    def get_note_by_record(self, record: str) -> str:
        return self.notes_man.get_note(record)

    def get_note(self) -> str:
        tv = self.get_active_treeview()
        record = tv.get_record_string()
        return self.notes_man.get_note(record)

    def add_note(self, note: str) -> None:
        tv = self.get_active_treeview()
        record = tv.get_record_string()
        self.notes_man.add_note(record, note)

    def delete_note(self) -> None:
        tv = self.get_active_treeview()
        record = tv.get_record_string()
        self.notes_man.delete_note(record)

    def refresh_players(self, record: "Record") -> None:
        treeview = self.get_active_treeview()
        model, treeiter = treeview.get_selection().get_selected()
        ServerModelManager(self, treeview).update_playercount(treeiter, record)

    def get_active_keyword(self) -> str:
        return self.get_filter_man().get_active_keyword()

    def set_active_keyword(self, keyword: str) -> None:
        self.get_filter_man().set_active_keyword(keyword)

    def get_modtreeview(self) -> "ModTreeView":
        return self.mediator.modtreeview

    def get_exit_event(self) -> threading.Event:
        return self.exit_event

    def set_exit_event(self) -> None:
        self.exit_event.set()

    def set_cancel_event(self) -> None:
        self.cancel_event.set()

    def get_cancel_event(self) -> threading.Event:
        return self.cancel_event

    def clear_cancel_event(self) -> None:
        self.cancel_event.clear()

    def open_connection_assistant(self, prereqs: "Prerequisites") -> None:
        self.open_page(NotebookPage.CONNECTION)
        self.mediator.preconnect.populate(prereqs)

    def set_start_tab(self) -> None:
        ind = self.config_man.get_start_tab()
        self.get_servers().notebook.set_current_page(ind)

    def update_and_connect(self, raise_window: bool) -> None:
        self.connection_man.update_and_connect(raise_window)

    def update_status(self) -> None:
        self.mediator.preconnect.mark_finished()

    def get_steam_client_name(self) -> str:
        return self.mediator.options.get_client_name()

    def present_window(self) -> None:
        self.mediator.window.present_with_time(Gdk.CURRENT_TIME)
