"""
Utilities for loading the built assets bundled with the python package.

Adatpted from django-vite package, modified to accomodate the fact that it's being distributed with a library.
"""

from typing import Dict, List, Optional

import json
from os.path import dirname
from os.path import join as path_join
from urllib.parse import urljoin

from django.conf import settings
from django.utils.safestring import mark_safe

# If using in development or production mode.
DJANGO_VITE_DEV_MODE = getattr(settings, "GROUNDWORK_VITE_DEV_MODE", False)

# Default Vite server protocol (http or https)
DJANGO_VITE_DEV_SERVER_PROTOCOL = getattr(
    settings, "GROUNDWORK_VITE_DEV_SERVER_PROTOCOL", "http"
)

# Default vite server hostname.
DJANGO_VITE_DEV_SERVER_HOST = getattr(
    settings, "GROUNDWORK_VITE_DEV_SERVER_HOST", "localhost"
)

# Default Vite server port.
DJANGO_VITE_DEV_SERVER_PORT = getattr(settings, "GROUNDWORK_VITE_DEV_SERVER_PORT", 3000)

# Default Vite server path to HMR script.
DJANGO_VITE_WS_CLIENT_URL = getattr(
    settings, "GROUNDWORK_VITE_WS_CLIENT_URL", "@vite/client"
)

GROUNDWORK_CORE_PATH = dirname(dirname(__file__))
STATIC_URL = urljoin(settings.STATIC_URL, "groundwork/")

# Location of Vite compiled assets (only used in Vite production mode).
# Must be included in your "STATICFILES_DIRS".
# In Django production mode this folder need to be collected as static
# files using "python manage.py collectstatic".
if settings.DEBUG:
    DJANGO_VITE_ASSETS_PATH = path_join(GROUNDWORK_CORE_PATH, "static", "groundwork")
else:
    DJANGO_VITE_ASSETS_PATH = path_join(settings.STATIC_ROOT, "groundwork")

# Path to your manifest file generated by Vite.
# Should by in "GROUNDWORK_VITE_ASSETS_PATH".
DJANGO_VITE_MANIFEST_PATH = getattr(
    settings,
    "GROUNDWORK_VITE_MANIFEST_PATH",
    path_join(
        DJANGO_VITE_ASSETS_PATH,
        "manifest.json",
    ),
)

# Motif in the 'manifest.json' to find the polyfills generated by Vite.
DJANGO_VITE_LEGACY_POLYFILLS_MOTIF = getattr(
    settings, "GROUNDWORK_VITE_LEGACY_POLYFILLS_MOTIF", "legacy-polyfills"
)


class GroundworkAssetLoader:
    """
    Class handling Vite asset loading.
    """

    _instance = None

    def __init__(self) -> None:
        raise RuntimeError("Use the instance() method instead.")

    def generate_vite_asset(
        self,
        path: str,
        scripts_attrs: Optional[Dict[str, str]] = None,
    ) -> str:
        """
        Generates a <script> tag for this JS/TS asset and a <link> tag for
        all of its CSS dependencies by reading the manifest file (for production only).
        In development Vite loads all by itself.

        Args:
            path: Path to a Vite JS/TS asset to include.
            scripts_attrs: Override attributes added to the script tag. (default: {None})

        Raises:
            RuntimeError: If cannot find the file path in the manifest (only in production).

        Returns:
            The <script> tag and all <link> tags to import this asset in your page.
        """

        if DJANGO_VITE_DEV_MODE:
            return GroundworkAssetLoader._generate_script_tag(
                GroundworkAssetLoader._generate_vite_server_url(path),
                {"type": "module", "async": "", "defer": ""},
            )

        if path not in self._manifest:
            raise RuntimeError(
                f"Cannot find {path} in Vite manifest "
                f"at {DJANGO_VITE_MANIFEST_PATH}"
            )

        tags = []
        manifest_entry = self._manifest[path]
        scripts_attrs = scripts_attrs or {"type": "module", "async": "", "defer": ""}

        # Add dependent CSS
        tags.extend(self._generate_css_files_of_asset(path, []))

        # Add the script by itself
        tags.append(
            GroundworkAssetLoader._generate_script_tag(
                manifest_entry["file"],
                attrs=scripts_attrs,
            )
        )

        return "\n".join(tags)

    def generate_dynamic_handlers(self) -> str:
        """
        Generates a <script> tag informing the client-side js how to resolve dynamic imports based on Django's
        STATIC_URL configuration.

        Returns:
            The <script> tag.
        """

        scripts = ";".join(
            [
                "function handler(prefix){return function(name){return prefix+name}}",
                f'window.__groundwork_dynamic_handler__=handler("{STATIC_URL + "assets/"}")',
                f'window.__groundwork_dynamic_preload__=function(p){{return p.map(handler("{STATIC_URL}"))}}',
            ]
        )

        return f"<script>(function(){{{scripts}}})()</script>"

    def _generate_css_files_of_asset(
        self, path: str, already_processed: List[str]
    ) -> List[str]:
        """
        Generates all CSS tags for dependencies of an asset.

        Args:
            path: Path to an asset in the 'manifest.json'.
            already_processed: List of already processed CSS file.

        Returns:
            List of CSS tags.
        """

        tags = []
        manifest_entry = self._manifest[path]

        if "imports" in manifest_entry:
            for import_path in manifest_entry["imports"]:
                tags.extend(
                    self._generate_css_files_of_asset(import_path, already_processed)
                )

        if "css" in manifest_entry:
            for css_path in manifest_entry["css"]:
                if css_path not in already_processed:
                    tags.append(
                        GroundworkAssetLoader._generate_stylesheet_tag(css_path)
                    )

                already_processed.append(css_path)

        return tags

    def generate_vite_asset_url(self, path: str) -> str:
        """
        Generates only the URL of an asset managed by ViteJS.
        Warning, this function does not generate URLs for dependant assets.

        Args:
            path: Path to a Vite asset.

        Raises:
            RuntimeError: If cannot find the asset path in the manifest (only in production).

        Returns:
            The URL of this asset.
        """

        if DJANGO_VITE_DEV_MODE:
            return GroundworkAssetLoader._generate_vite_server_url(path)

        if path not in self._manifest:
            raise RuntimeError(
                f"Cannot find {path} in Vite manifest "
                f"at {DJANGO_VITE_MANIFEST_PATH}"
            )

        return self._manifest[path]["file"]

    def generate_vite_legacy_polyfills(
        self,
        scripts_attrs: Optional[Dict[str, str]] = None,
    ) -> str:
        """
        Generates a <script> tag to the polyfills generated by '@vitejs/plugin-legacy' if used.
        This tag must be included at end of the <body> before including other legacy scripts.

        Args:
            scripts_attrs: Override attributes added to scripts tags. (default: {None})

        Raises:
            RuntimeError: If polyfills path not found inside the 'manifest.json' (only in production).

        Returns:
            The script tag to the polyfills.
        """

        if DJANGO_VITE_DEV_MODE:
            return ""

        scripts_attrs = scripts_attrs or {"nomodule": ""}

        for path, content in self._manifest.items():
            if DJANGO_VITE_LEGACY_POLYFILLS_MOTIF in path:
                return GroundworkAssetLoader._generate_script_tag(
                    content["file"],
                    attrs=scripts_attrs,
                )

        raise RuntimeError(
            f"Vite legacy polyfills not found in manifest "
            f"at {DJANGO_VITE_MANIFEST_PATH}"
        )

    def generate_vite_legacy_asset(
        self,
        path: str,
        scripts_attrs: Optional[Dict[str, str]] = None,
    ) -> str:
        """
        Generates a <script> tag for legacy assets JS/TS generated by '@vitejs/plugin-legacy'
        (in production only, in development do nothing).

        Args:
            path: Path to a Vite asset to include (must contains '-legacy' in its name).
            scripts_attrs: Override attributes added to scripts tags. (default: {None})

        Raises:
            RuntimeError: If cannot find the asset path in the manifest (only in production).

        Returns:
            The script tag of this legacy asset .
        """

        if DJANGO_VITE_DEV_MODE:
            return ""

        if path not in self._manifest:
            raise RuntimeError(
                f"Cannot find {path} in Vite manifest "
                f"at {DJANGO_VITE_MANIFEST_PATH}"
            )

        manifest_entry = self._manifest[path]
        scripts_attrs = scripts_attrs or {"nomodule": ""}

        return GroundworkAssetLoader._generate_script_tag(
            manifest_entry["file"],
            attrs=scripts_attrs,
        )

    def _parse_manifest(self) -> None:
        """
        Read and parse the Vite manifest file.

        Raises:
            RuntimeError: if cannot load the file or JSON in file is malformed.
        """

        try:
            manifest_file = open(DJANGO_VITE_MANIFEST_PATH)
            manifest_content = manifest_file.read()
            manifest_file.close()
            self._manifest = json.loads(manifest_content)
        except Exception as error:
            raise RuntimeError(
                f"Cannot read Vite manifest file at "
                f"{DJANGO_VITE_MANIFEST_PATH} : {str(error)}"
            )

    @classmethod
    def instance(cls):
        """
        Singleton.
        Uses singleton to keep parsed manifest in memory after
        the first time it's loaded.

        Returns:
            only instance of the class.
        """

        if cls._instance is None:
            cls._instance = cls.__new__(cls)
            cls._instance._manifest = None

            # Manifest is only used in production.
            if not DJANGO_VITE_DEV_MODE:
                cls._instance._parse_manifest()

        return cls._instance

    @classmethod
    def generate_vite_ws_client(cls) -> str:
        """
        Generates the script tag for the Vite WS client for HMR.
        Only used in development, in production this method returns
        an empty string.

        Returns:
            The script tag or an empty string.
        """

        if not DJANGO_VITE_DEV_MODE:
            return ""

        return cls._generate_script_tag(
            cls._generate_vite_server_url(DJANGO_VITE_WS_CLIENT_URL),
            {"type": "module"},
        )

    @staticmethod
    def _generate_script_tag(src: str, attrs: Optional[Dict[str, str]] = None) -> str:
        """
        Generates an HTML script tag.

        Args:
            src: Source of the script.
            attrs: List of custom attributes for the tag (default: {None})

        Returns:
            str -- The script tag.
        """

        attrs_str = (
            " ".join([f'{key}="{value}"' for key, value in attrs.items()])
            if attrs is not None
            else ""
        )

        return f'<script {attrs_str} src="{urljoin(STATIC_URL, src)}"></script>'

    @staticmethod
    def _generate_stylesheet_tag(href: str) -> str:
        """
        Generates and HTML <link> stylesheet tag for CSS.

        Args:
            href: CSS file URL.

        Returns:
            CSS link tag.
        """

        return f'<link rel="stylesheet" href="{urljoin(STATIC_URL, href)}" />'

    @staticmethod
    def _generate_vite_server_url(path: Optional[str] = None) -> str:
        """
        Generates an URL to and asset served by the Vite development server.

        Args:
            path: Path to the asset. (default: {None})

        Returns:
            str -- Full URL to the asset.
        """

        return urljoin(
            f"{DJANGO_VITE_DEV_SERVER_PROTOCOL}://"
            f"{DJANGO_VITE_DEV_SERVER_HOST}:{DJANGO_VITE_DEV_SERVER_PORT}",
            urljoin(settings.STATIC_URL, path if path is not None else ""),
        )
