"""Implementation of ThemedApp."""

import asyncio
import json
import logging
import os
import sys
from asyncio import create_task
from functools import partial
from pathlib import Path
from typing import Callable, Optional
from warnings import warn

import blinker
import sass
from mergedeep import Strategy, merge
from trame.app import get_server
from trame.assets.local import LocalFileManager
from trame.ui.vuetify3 import VAppLayout
from trame.widgets import client
from trame.widgets import vuetify3 as vuetify
from trame_client.widgets import html
from trame_server.core import Server
from trame_server.state import State

from nova.common.signals import Signal
from nova.mvvm.pydantic_utils import validate_pydantic_parameter
from nova.trame.view.theme.exit_button import ExitButton
from nova.trame.view.utilities.local_storage import LocalStorageManager

THEME_PATH = Path(__file__).parent

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)


class ThemedApp:
    """Automatically injects theming into your Trame application.

    You should always inherit from this class when you define your Trame application.
    """

    def __init__(
        self, layout: str = "default", server: Server = None, vuetify_config_overrides: Optional[dict] = None
    ) -> None:
        """Constructor for the ThemedApp class.

        Parameters
        ----------
        layout : str
            The layout to use. Current options are: :code:`default` and :code:`two-column`
        server : `trame_server.core.Server \
            <https://trame.readthedocs.io/en/latest/core.server.html#trame_server.core.Server>`_, optional
            The Trame server to use. If not provided, a new server will be created.
        vuetify_config_overrides : dict, optional
            `Vuetify Configuration <https://vuetifyjs.com/en/features/global-configuration/>`__
            that will override anything set in our default configuration. You should only use this if you don't want to
            use one of our predefined themes. If you just want to set your color palette without providing a full
            Vuetify configuration, then you can set use the following to only set the color palette used by our
            :code:`ModernTheme`:

            .. code-block:: json

                {
                    "primary": "#f00",
                    "secondary": "#0f0",
                    "accent": "#00f",
                }

        Returns
        -------
        None
        """
        self.server = get_server(server, client_type="vue3")
        self.local_storage: Optional[LocalStorageManager] = None
        self._download_file: Optional[Callable] = None
        if vuetify_config_overrides is None:
            vuetify_config_overrides = {}

        self.css = None
        try:
            with open(THEME_PATH / "assets" / "core_style.scss", "r") as scss_file:
                self.css = sass.compile(string=scss_file.read())
        except Exception as e:
            logger.warning("Could not load base scss stylesheet.")
            logger.error(e)

        theme_path = THEME_PATH / "assets" / "vuetify_config.json"
        try:
            with open(theme_path, "r") as vuetify_config:
                self.vuetify_config = json.load(vuetify_config)

                merge(
                    self.vuetify_config,
                    vuetify_config_overrides,
                    strategy=Strategy.REPLACE,
                )
        except Exception as e:
            logger.warning(f"Could not load vuetify config from {theme_path}.")
            logger.error(e)
        for shortcut in ["primary", "secondary", "accent"]:
            if shortcut in self.vuetify_config:
                self.vuetify_config["theme"]["themes"]["ModernTheme"]["colors"][shortcut] = self.vuetify_config[
                    shortcut
                ]

        self.init_lodash()

        # Since this is only intended for theming Trame apps, I don't think we need to invoke the MVVM framework here,
        # and working directly with the Trame state makes this easier for me to manage.
        self.state.nova__menu = False
        self.state.nova__defaults = self.vuetify_config["theme"]["themes"]["ModernTheme"].get("defaults", {})
        self.state.nova__theme = "ModernTheme"
        self.state.trame__favicon = LocalFileManager(__file__).url("favicon", "./assets/favicon.png")

    @property
    def state(self) -> State:
        return self.server.state

    def download_file(self, filename: str, mimetype: str, content: bytes) -> None:
        """Attempts to download a file via the browser to the user's computer.

        Note that this will do nothing if no client is connected to the Trame application when called.

        Parameters
        ----------
        filename : str
            The name of the file to be downloaded.
        mimetype : str
            The `MIME type <https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/MIME_types>`__ of the content.
        content : bytes
            The bytes to be included in the download.

        Returns
        -------
        None
        """
        if not self._download_file:
            warn(
                "You must call create_ui on this instance before download_file can be used.",
                stacklevel=1,
            )

            return

        self._download_file([filename, mimetype, content])

    def init_lodash(self) -> None:
        js_path = (Path(__file__).parent / "assets" / "js").resolve()
        self.server.enable_module(
            {
                "scripts": ["assets/js/lodash.min.js", "assets/js/delay_manager.js"],
                "serve": {"assets/js": js_path},
            }
        )

    def init_mantid(self) -> None:
        """Initializes MantidManager.

        This doesn't happen by default because Mantid is a large dependency.
        """
        pass

    async def _init_theme(self) -> None:
        if self.local_storage:
            theme = await self.local_storage.get("nova__theme")
            if theme and theme in self.vuetify_config["theme"]["themes"]:
                self.set_theme(theme, False)

    async def init_theme(self) -> None:
        create_task(self._init_theme())

    async def get_jobs_callback(self) -> None:
        get_tools_signal = blinker.signal(Signal.GET_ALL_TOOLS)
        response = get_tools_signal.send()
        if response and len(response[0]) > 1:  # Make sure that the callback had a return value
            try:
                self.server.state.nova_running_jobs = [tool.id for tool in response[0][1]]
                if len(self.server.state.nova_running_jobs) > 0:
                    self.server.state.nova_show_stop_jobs_on_exit_checkbox = True
                    self.server.state.nova_kill_jobs_on_exit = True
                else:
                    self.server.state.nova_show_stop_jobs_on_exit_checkbox = False
            except Exception as e:
                logger.warning(f"Issue getting running jobs: {e}")

    async def exit_callback(self) -> None:
        logger.info(f"Closing App. Killing jobs: {self.server.state.nova_kill_jobs_on_exit}")
        if self.server.state.nova_kill_jobs_on_exit:
            self.server.state.nova_show_exit_progress = True
            await asyncio.sleep(2)
            stop_signal = blinker.signal(Signal.EXIT_SIGNAL)
            stop_signal.send()
        sys.exit(0)

    def set_theme(self, theme: Optional[str], force: bool = True) -> None:
        """Sets the theme of the application.

        Parameters
        ----------
        theme : str, optional
            The new theme to use. If the theme is not found, the default theme will be used. The available options are:

            1. ModernTheme (default) - Leverages ORNL brand colors and a typical Vuetify appearance.
            2. CompactTheme - Similar to ModernTheme but with a smaller global font size and increased density.
        force : bool, optional
            If True, the theme will be set even if the theme selection menu is disabled.

        Returns
        -------
        None
        """
        if theme not in self.vuetify_config["theme"]["themes"]:
            raise ValueError(
                f"Theme '{theme}' not found in the Vuetify configuration. "
                "For a list of available themes, please visit "
                "https://nova-application-development.readthedocs.io/en/stable/api.html#nova.trame.ThemedApp."
            )

        # I set force to True by default as I want the user to be able to say self.set_theme('MyTheme')
        # while still blocking theme.py calls to set_theme if the selection menu is disabled.
        if self.state.nova__menu or force:
            with self.state:
                self.state.nova__defaults = self.vuetify_config["theme"]["themes"].get(theme, {}).get("defaults", {})
                self.state.nova__theme = theme

        # We only want to sync to localStorage if the user is selecting and we want to preserve the selection.
        if self.state.nova__menu and self.local_storage:
            self.local_storage.set("nova__theme", theme)

    def create_ui(self) -> VAppLayout:
        """Creates the base UI into which you will inject your content.

        You should always call this method from your application class that inherits from :code:`ThemedApp`.

        Returns
        -------
        `trame_client.ui.core.AbstractLayout <https://trame.readthedocs.io/en/latest/core.ui.html#trame_client.ui.core.AbstractLayout>`_
        """
        # This detects if Pixi is running in production mode so that we can show links to NOVA resources during
        # development.
        show_nova_resources = os.environ.get("PIXI_ENVIRONMENT_NAME", "") != "production"

        with VAppLayout(self.server, vuetify_config=self.vuetify_config) as layout:
            self.local_storage = LocalStorageManager(self.server.controller)
            self._download_file = client.JSEval(
                exec=(
                    "async ($event) => {"
                    "  const [filename, mimetype, content] = $event;"
                    "  const blob = new window.Blob([content], {type: mimetype});"
                    "  const url = window.URL.createObjectURL(blob);"
                    "  const anchor = window.document.createElement('a');"
                    "  anchor.setAttribute('href', url);"
                    "  anchor.setAttribute('download', filename);"
                    "  window.document.body.appendChild(anchor);"
                    "  anchor.click();"
                    "  window.document.body.removeChild(anchor);"
                    "  window.setTimeout(() => window.URL.revokeObjectURL(url), 1000);"
                    "}"
                )
            ).exec

            client.ClientTriggers(mounted=self.init_theme)
            client.Style(self.css)

            with vuetify.VDefaultsProvider(defaults=("nova__defaults",)) as defaults:
                layout.defaults = defaults

                with vuetify.VThemeProvider(theme=("nova__theme",)) as theme:
                    layout.theme = theme

                    with vuetify.VAppBar() as toolbar:
                        layout.toolbar = toolbar

                        with vuetify.VAppBarTitle(classes="flex-0-1") as toolbar_title:
                            layout.toolbar_title = toolbar_title

                        if show_nova_resources:
                            vuetify.VBtn(
                                "NOVA Examples",
                                classes="ml-4",
                                href="https://github.com/nova-model/nova-examples/",
                                __properties=["target"],
                                target="_blank",
                            )
                            html.Div("·", classes="mx-1")
                            vuetify.VBtn(
                                "NOVA Tutorial",
                                href="https://nova.ornl.gov/tutorial/",
                                __properties=["target"],
                                target="_blank",
                            )
                            html.Div("·", classes="mx-1")
                            vuetify.VBtn(
                                "NOVA Documentation",
                                href="https://nova-application-development.readthedocs.io/en/latest/",
                                __properties=["target"],
                                target="_blank",
                            )

                        vuetify.VSpacer()
                        with html.Div(classes="mr-2") as actions:
                            layout.actions = actions

                            with vuetify.VMenu(
                                v_if="nova__menu",
                                close_delay=10000,
                                open_on_hover=True,
                            ) as theme_menu:
                                layout.theme_menu = theme_menu

                                with vuetify.Template(v_slot_activator="{ props }"):
                                    vuetify.VBtn(
                                        v_bind="props",
                                        classes="mr-2",
                                        icon="mdi-brush-variant",
                                    )

                                with vuetify.VList(width=200):
                                    vuetify.VListSubheader("Select Theme")

                                    for theme in self.vuetify_config.get("theme", {}).get("themes", {}).values():
                                        with vuetify.VListItem(click=partial(self.set_theme, theme["value"])):
                                            vuetify.VListItemTitle(theme["title"])
                                            vuetify.VListItemSubtitle(
                                                "Selected",
                                                v_if=f"nova__theme === '{theme['value']}'",
                                            )
                            ExitButton(self.exit_callback, self.get_jobs_callback)

                    with vuetify.VMain(classes="align-stretch d-flex flex-column h-screen"):
                        # [slot override example]
                        layout.pre_content = vuetify.VSheet(classes="bg-background flex-0-1 mt-1 nova-pre-content ")
                        # [slot override example complete]
                        with vuetify.VContainer(classes="flex-1-1 overflow-hidden pt-0 pb-2", fluid=True):
                            layout.content = vuetify.VCard(
                                classes="d-flex flex-column flex-1-1 h-100 overflow-y-auto pa-1 "
                            )
                        layout.post_content = vuetify.VSheet(classes="bg-background flex-0-1 mb-1 ")

                    with vuetify.VFooter(
                        app=True,
                        classes="my-0 px-1 py-0 text-center justify-center",
                        border=True,
                    ) as footer:
                        layout.footer = footer

                        vuetify.VProgressCircular(
                            classes="mr-1",
                            color="primary",
                            indeterminate=("!!galaxy_running",),
                            size=16,
                            width=3,
                        )
                        html.A(
                            "Powered by Calvera",
                            classes="text-grey-lighten-1 text-caption text-decoration-none",
                            href=("galaxy_url",),
                            target="_blank",
                        )
                        vuetify.VSpacer()
                        footer.add_child(
                            '<a href="https://www.ornl.gov/" '
                            'class="text-grey-lighten-1 text-caption text-decoration-none" '
                            'target="_blank">© {{ new Date().getFullYear() }} ORNL</a>'
                        )

            @self.server.controller.trigger("validate_pydantic_field")
            def validate_pydantic_field(name: str, value: str, index: int) -> bool:
                if "[index]" in name:
                    name = name.replace("[index]", f"[{str(index)}]")
                return validate_pydantic_parameter(name, value)

            return layout
