from __future__ import annotations

import logging
import sys
from pathlib import Path
from typing import Optional

import wx
from wx.adv import TaskBarIcon

from clarabot_nano.common.i18n import EFFECTIVE_LANGUAGE
from clarabot_nano.control.about import AboutDialog
from clarabot_nano.control.account.change_password import ChangeAccountPasswordDialog
from clarabot_nano.control.awake import set_keep_awake
from clarabot_nano.control.client import WxCoreInterface
from clarabot_nano.control.configure.basic.wizard import SimpleConfigurationWizard
from clarabot_nano.control.configure.drive.manager import DriveConfigManager
from clarabot_nano.control.icon import get_wx_nano_icon
from clarabot_nano.control.nano.config import ConfigUpdateProgressDialog
from clarabot_nano.control.nano.reset import ResetProgressDialog
from clarabot_nano.control.page.login import ControlLogin
from clarabot_nano.control.page.start import ControlStart
from clarabot_nano.control.page.status import ControlStatus
from clarabot_nano.control.utility import Slots, find_children_of_type

logger = logging.getLogger(__name__)


class ControlTray(TaskBarIcon):
    def __init__(self, control_window: BaseControlWindow):
        self.control_window = control_window
        TaskBarIcon.__init__(self)

    def CreatePopupMenu(self):
        menu = wx.Menu()
        menu.Bind(wx.EVT_MENU, self.control_window._on_open, menu.Append(wx.ID_ANY, gettext("&Show Nano manager")))
        menu.Bind(wx.EVT_MENU, self.control_window._on_quit, menu.Append(wx.ID_ANY, gettext("&Quit Nano")))
        return menu


class BaseControlWindow(wx.Frame, Slots):
    """
    The simple but powerful local status and configuration interface.
    """

    def __init__(self, core_interface: WxCoreInterface, parent=None, disable_tray_icon=False):
        wx.Frame.__init__(
            self,
            parent,
            wx.ID_ANY,
            "Clarabot Nano",
            style=wx.MINIMIZE_BOX | wx.CAPTION | wx.CLOSE_BOX | wx.CLIP_CHILDREN,
        )
        Slots.__init__(self)
        self.SetSizeHints(320, 320, 320)
        self.SetSize(wx.DefaultCoord, wx.DefaultCoord, 320, 320)
        self.SetIcon(get_wx_nano_icon())
        self.core_interface = core_interface
        self._shown_at_least_once = False
        self._inhibits_sleep = False

        self._do_initial_setup = not self.core_interface.get_init_setup_path().exists()

        # self._window_settings = Settings(
        #     str(self.core_interface.get_data_dir() / "control_window.ini")
        # )

        # System tray icon initialization.
        self.tray_icon: Optional[ControlTray] = None
        # NOTE: The system tray may be unavailable temporarily. The tray-icon could be created regardless of this
        # state and it will appear when it becomes available. The bigger problem is that without a tray the window
        # may be unreachable. Basically if the tray is available and it becomes permanently unavailable that's the
        # error of the desktop system and not our problem.
        if not disable_tray_icon and TaskBarIcon.IsAvailable():
            self.tray_icon = ControlTray(self)
            if not self.tray_icon.IsOk():
                self.tray_icon = None  # TODO: do something else?
            else:
                self.tray_icon.SetIcon(get_wx_nano_icon(wx.Size(24, 24)), "Clarabot Nano")
                self.tray_icon.Bind(wx.adv.EVT_TASKBAR_LEFT_DOWN, self._on_tray_activation)

        self._configure_basic_menu_id = wx.NewEventType()
        self._configure_drive_menu_id = wx.NewEventType()
        # Window menu-bar initialization.
        self.CreateStatusBar(1, wx.STB_SHOW_TIPS | wx.STB_ELLIPSIZE_END | wx.FULL_REPAINT_ON_RESIZE)
        self._menu_bar = wx.MenuBar()
        self._menu_bar.Append(account_menu := wx.Menu(), gettext("&Account"))
        self._change_password_item = account_menu.Append(wx.ID_ANY, gettext("Change password"))
        self.Bind(wx.EVT_MENU, self._on_change_account_password, self._change_password_item)
        self._menu_bar.Append(logs_menu := wx.Menu(), gettext("&Logs"))
        self.Bind(wx.EVT_MENU, self._on_access_log, logs_menu.Append(wx.ID_ANY, gettext("Access log")))
        self.Bind(wx.EVT_MENU, self._on_service_log, logs_menu.Append(wx.ID_ANY, gettext("Service log")))
        self.Bind(wx.EVT_MENU, self._on_logs_directory, logs_menu.Append(wx.ID_ANY, gettext("Logs directory")))
        if sys.platform.startswith("darwin"):
            self._menu_bar.Append(wx.Menu(), "&Window")  # Control placement of auto-window-menu item.
        # self._settings_menu = menu_bar.addMenu("Settings")
        # self._setup_wizard_action = self._settings_menu.addAction("Setup wizard")
        # self._configure_action = self._settings_menu.addAction("Configure")
        self._menu_bar.Append(help_menu := wx.Menu(), gettext("&Help"))
        self.Bind(wx.EVT_MENU, self._on_online_help, help_menu.Append(wx.ID_HELP, gettext("Online help")))
        self.Bind(wx.EVT_MENU, self._on_about, help_menu.Append(wx.ID_ABOUT, gettext("About")))

        # NOTE: Setting menu bar produces additional items on MacOS automatically.
        self.SetMenuBar(self._menu_bar)
        if sys.platform.startswith("darwin"):
            self._nano_menu = self._menu_bar.OSXGetAppleMenu()
        else:
            self._nano_menu = wx.Menu()
            self._menu_bar.Insert(0, self._nano_menu, "&Nano")
        # menu_suspend = nano_menu.AppendCheckItem(wx.ID_ANY, gettext("Suspend"))
        self._reset_item = self._nano_menu.Insert(0, wx.ID_ANY, gettext("Reset"))
        self.Bind(wx.EVT_MENU, self._on_reset, self._reset_item)
        config_submenu = wx.Menu()
        self.Bind(
            wx.EVT_MENU,
            self._on_configure_basic,
            config_submenu.Append(self._configure_basic_menu_id, gettext("Basic setup wizard")),
        )
        self.Bind(
            wx.EVT_MENU,
            self._on_configure_drive,
            config_submenu.Append(self._configure_drive_menu_id, gettext("Drive manager")),
        )
        self._nano_config_menu_item = self._nano_menu.Insert(1, wx.ID_ANY, gettext("Configure"), config_submenu)
        self._nano_menu.InsertSeparator(2)
        if not sys.platform.startswith("darwin"):
            self._nano_menu.Append(wx.ID_EXIT, gettext("Quit"))
        self.Bind(wx.EVT_MENU, self._on_quit, id=wx.ID_EXIT)

        # "event-table"
        self.Bind(wx.EVT_CLOSE, self._on_close_event)
        self.Bind(wx.EVT_SHOW, self._on_show_event)

        self._cw = None
        self._update_menu_item = None
        self._max_update_state = 0
        if sys.platform.startswith("win"):
            wx.adv.NotificationMessage.MSWUseToasts()
        self._update_notification = wx.adv.NotificationMessage()
        self._update_notification.SetParent(self)
        self._update_notification.SetTitle(gettext("Update"))
        # TODO: implement notification click event after a wxPython release (fixed upstream)
        # self._update_notification.Bind(CLICK)
        self._update_notification_timer = wx.Timer(self)
        self.Bind(wx.EVT_TIMER, self._on_update_notification_timer, self._update_notification_timer)

        # init
        self.bind_signal(core_interface.config_changed, self._on_config_changed)
        self.bind_signal(core_interface.service_error_signal, self._on_service_error)
        self.bind_signal(core_interface.core_died, self._on_core_died)
        self._on_config_changed(core_interface.get_config())
        self._on_service_error(core_interface.get_errors(), None)
        self._restore_window_geometry()
        if not self.tray_icon or sys.platform.startswith("darwin"):
            self.smart_show()

    def _on_destroy(self, event):
        Slots._on_destroy(self, event)
        if self.tray_icon:
            self.tray_icon.RemoveIcon()
            self.tray_icon.Destroy()

    def _on_core_died(self):
        """
        Exiting the GUI is not trivial because of platform differences and some general orchestration issues.

        - self.Destroy()
            This works well, but if there is an open modal dialog it breaks
        - self.DeleteLater()
            Pending destroy objects by DestroyLater() are deleted by operator and Destroy() is not used.
            This is very un-intuitive or... I don't know the reasoning. Stick to Destroy() for now.
        - wx.App.GetInstance().ExitMainLoop()
            Same problem as self.Destroy()

        We've made a workaround cleanup utility on the App object that always works, well enough:
            wx.App.GetInstance().smart_exit_main_loop()
        """
        wx.App.GetInstance().smart_exit_main_loop()

    def central_widget(self):
        return self._cw

    def free_central_widget(self):
        if self._cw:
            self._cw.Destroy()
            self._cw = None

    def set_central_widget(self, widget):
        self.free_central_widget()
        self._cw = widget
        self.Layout()

    def _on_config_changed(self, config):
        # set the central widget
        gateway_status = config["state"].get("gateway_status")
        cw = self.central_widget()
        status_previously = isinstance(cw, ControlStatus)
        if not gateway_status or gateway_status == "starting":
            if cw is not None:
                pass  # do not flip-flop between the widgets at gateway failure; TODO: this may no longer be needed
            elif not isinstance(cw, ControlStart):
                self.free_central_widget()
                self.set_central_widget(ControlStart(self.core_interface, self))
        elif gateway_status == "waiting-config":
            if not isinstance(cw, ControlLogin):
                self.free_central_widget()
                self.set_central_widget(ControlLogin(self.core_interface, self))
                self._enable_menus_by_login_state(False)
                if not self._shown_at_least_once:
                    self.smart_show()
        else:
            if not isinstance(cw, ControlStatus):
                self.free_central_widget()
                self.set_central_widget(ControlStatus(self.core_interface, self))
                self._enable_menus_by_login_state(True)
        # manage keep-awake
        status_afterwards = isinstance(self.central_widget(), ControlStatus)
        if status_previously is not status_afterwards:
            self._manage_session_work_state(status_afterwards)
        # setup states that are not tied to login
        self._set_update_state(config["state"].get("gateway_update"))
        # check and start initial setup when appropriate
        if self._do_initial_setup and gateway_status in ("connected", "active"):
            self._do_initial_setup = False
            self.QueueEvent(wx.CommandEvent(wx.wxEVT_MENU, self._configure_basic_menu_id))

    def _manage_session_work_state(self, working: bool):
        if self._inhibits_sleep is not working:
            if set_keep_awake(working):
                self._inhibits_sleep = working

    def _enable_menus_by_login_state(self, logged_in: bool):
        self._menu_bar.EnableTop(self._menu_bar.FindMenu(gettext("&Account")), logged_in)
        self._nano_menu.Enable(self._nano_config_menu_item.GetId(), logged_in)

    def _set_update_state(self, update: Optional[int]):
        update_message = self._get_update_message_from_state(update)
        if update_message:
            # NOTE: Update state shall never "go away" so we're not implementing that.
            cw = self.central_widget()
            if isinstance(cw, ControlStatus):
                cw.display_update_link(update_message.partition(";")[0])
            if self._update_menu_item is None:
                update_menu = wx.Menu()
                self._menu_bar.Append(update_menu, gettext("Update"))
                self._update_menu_item = update_menu.Append(wx.ID_ANY, "update")  # placeholder text
                self.Bind(wx.EVT_MENU, self._on_update, self._update_menu_item)
            if self._max_update_state < update:
                # Only start popping up notifications once for each higher level than the previous one.
                self._max_update_state = update
                self._update_notification.SetMessage(update_message)
                if update >= 3:  # security update or unsupported will re-notify each day
                    self._update_notification_timer.Start(60 * 60 * 24 * 1000)
                self._on_update_notification_timer()
            self._update_menu_item.SetItemLabel(update_message)

    def _get_update_message_from_state(self, update: Optional[int]) -> Optional[str]:
        if update == 1:
            return gettext("Minor update available")
        elif update == 2:
            return gettext("Major update available")
        elif update == 3:
            return gettext("Security update available")
        elif update == 4:
            return gettext("Update required; current version is no longer supported")

    def _on_update_notification_timer(self, *args):
        self._update_notification.Show(wx.adv.NotificationMessage.Timeout_Never)

    def _on_service_error(self, error_count, msg):
        if error_count:
            self.SetStatusText(
                ngettext(
                    '%(error_count)d error occurred, see "Service log" for details',
                    '%(error_count)d errors occurred, see "Service log" for details',
                    error_count,
                )
                % {"error_count": error_count}
            )
        else:
            self.SetStatusText("")

    def _on_open(self, *args):
        self.smart_show()

    def _on_reset(self, *args):
        with wx.GenericMessageDialog(
            self,
            gettext(
                "Do you want to reset Nano?\n"
                "This instance will be logged out and all local configs will be reset to defaults.\n"
                "ALL CONFIGURATIONS WILL BE PERMANENTLY DELETED!"
            ),
            gettext("Confirm reset"),
            wx.ICON_QUESTION | wx.YES_NO | wx.NO_DEFAULT,
        ) as d:
            d.SetEscapeId(wx.ID_CANCEL)
            if d.ShowModal() != wx.ID_YES:
                return
        with ResetProgressDialog(self.core_interface, self) as d:
            d.ShowModal()
        # re-attempt initial setup after successful reset
        self._do_initial_setup = not self.core_interface.get_init_setup_path().exists()

    def _on_configure_basic(self, *args):
        if find_children_of_type(self, wx.Dialog):  # single modal protection
            return
        apply = False
        config_update_steps = []
        with SimpleConfigurationWizard(self, self.core_interface.get_config()) as w:
            if w.RunWizard(w.intro_page):
                apply = True
                config_update_steps = w.config_update_steps()
            if w.GetReturnCode() != wx.ID_ABORT:
                # only write the file if we're not exiting normally by user request (not exception or exit-main-loop)
                self.core_interface.get_init_setup_path().touch(exist_ok=True)
        if apply:
            if config_update_steps:
                with ConfigUpdateProgressDialog(self.core_interface, self, config_update_steps) as d:
                    d.ShowModal()
            else:
                with wx.GenericMessageDialog(self, gettext("No config changes to apply."), style=wx.OK) as d:
                    d.ShowModal()

    def _on_configure_drive(self, *args):
        with DriveConfigManager(self.core_interface, self) as d:
            d.ShowModal()

    def _on_quit(self, *args):
        self.confirm_quit()

    def _on_access_log(self, *args):
        self._show_log(self.core_interface.get_access_log_path())

    def _on_service_log(self, *args):
        self.core_interface.clear_errors()
        self._show_log(self.core_interface.get_service_log_path())

    def _on_logs_directory(self, *args):
        self.core_interface.clear_errors()
        self._show_log(self.core_interface.get_logs_dir())

    def _show_log(self, p: Path):
        if p.exists():
            wx.LaunchDefaultApplication(str(p))
        else:
            with wx.GenericMessageDialog(self, gettext("Log doesn't exist yet."), style=wx.OK) as d:
                d.ShowModal()

    def _on_change_account_password(self, *args):
        with ChangeAccountPasswordDialog(self.core_interface, self) as d:
            d.ShowModal()

    def _on_online_help(self, *args):
        web_doc_url = "https://www.clarabot.info/nano/"
        if EFFECTIVE_LANGUAGE in ("en", "hu"):
            web_doc_url += f"{EFFECTIVE_LANGUAGE}/"
        wx.LaunchDefaultBrowser(web_doc_url)

    def _on_update(self, *args):
        wx.LaunchDefaultBrowser("https://www.clarabot.com/download")

    def _on_about(self, *args):
        with AboutDialog(self) as d:
            d.ShowModal()

    def _on_tray_activation(self, event):  # NOTE: Is not functional on darwin.
        self.smart_show()

    def _on_show_event(self, event: wx.ShowEvent):
        self._save_window_settings()
        event.Skip(True)  # do default

    def _on_close_event(self, event: wx.CloseEvent):
        self._save_window_settings()
        event.Skip(False)  # do not process further
        if sys.platform.startswith("darwin"):
            # NOTE: Do not minify to tray on darwin. That is not usual and breaks many navigation actions.
            if event.CanVeto():  # user-close or query-end-session
                if not self.confirm_quit():
                    event.Veto()
            else:  # end-session most likely
                wx.App.GetInstance().smart_exit_main_loop()
        else:
            if self.tray_icon:
                self.Show(False)  # TODO notify first time user that this will go to tray
            else:
                self.confirm_quit()

    def confirm_quit(self):
        with wx.GenericMessageDialog(
            self, gettext("Do you want to shutdown Nano?"), gettext("Confirm exit"), wx.ICON_QUESTION | wx.YES_NO
        ) as d:
            d.SetEscapeId(wx.ID_CANCEL)
            if d.ShowModal() == wx.ID_YES:
                self.request_shutdown(explicit_user_request=True)
                return True
            return False

    def request_shutdown(self, explicit_user_request=False):
        self.core_interface.request_shutdown(explicit_user_request=explicit_user_request)

    def smart_show(self):
        self._shown_at_least_once = True
        hidden = not self.IsShown()
        if hidden:
            self.Show(True)
        # NOTE: iconized/minimized cannot be queried accurately when the window is not shown before, so just negate it
        self.Iconize(False)
        if hidden:
            self._restore_window_geometry()
        if not hidden:
            self.Raise()
            self.SetFocus()

    def _save_window_settings(self):
        pass

    def _restore_window_geometry(self):
        pass


if sys.platform.startswith("win"):

    class WindowsControlWindow(BaseControlWindow):
        def __init__(self, core_interface: WxCoreInterface, parent=None, disable_tray_icon=False):
            self._inhibits_shutdown = False
            BaseControlWindow.__init__(self, core_interface, parent=parent, disable_tray_icon=disable_tray_icon)

        def _manage_session_work_state(self, working: bool):
            BaseControlWindow._manage_session_work_state(self, working)
            # NOTE: This works, but is not useful ultimately. Windows is too eager to shutdown and does not respect
            # the denied QUERYENDSESSION. The other Nano processes are shut down regardless of this process blocking
            # shutdown using this. All of the processes would need to do the same, but we can't make the Java
            # dependencies do this easily. In general no process without a visible GUI may block the shutdown for some
            # reason according to:
            # https://docs.microsoft.com/en-us/windows/win32/shutdown/shutdown-changes-for-windows-vista
            # if self._inhibits_shutdown is not working:
            #     if working:
            #         reason = gettext("Running to serve remote requests.")
            #         if windll.user32.ShutdownBlockReasonCreate(self.GetHandle(), c_wchar_p(reason)) != 0:
            #             self._inhibits_shutdown = True
            #     else:
            #         if windll.user32.ShutdownBlockReasonDestroy(self.GetHandle()) != 0:
            #             self._inhibits_shutdown = False

        def query_session_end_veto(self):
            if self._inhibits_shutdown:
                self.smart_show()  # The window must be visible for the ShutdownBlockReason to be effective.
                return True
            return False

    ControlWindow = WindowsControlWindow

else:
    ControlWindow = BaseControlWindow
