import json
from os.path import join as path_join
from typing import Dict, Optional
from urllib.parse import urljoin

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

register = template.Library()


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

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

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

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

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

# 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".
DJANGO_VITE_ASSETS_PATH = getattr(settings, "DJANGO_VITE_ASSETS_PATH")

# Path to your manifest file generated by Vite.
# Should by in "DJANGO_VITE_ASSETS_PATH".
DJANGO_VITE_MANIFEST_PATH = getattr(
    settings,
    "DJANGO_VITE_MANIFEST_PATH",
    path_join(
        DJANGO_VITE_ASSETS_PATH if settings.DEBUG else settings.STATIC_ROOT,
        "manifest.json",
    ),
)


class DjangoViteAssetLoader:
    """
    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,
        with_imports: bool = True,
    ) -> str:
        """
        Generates all assets include tags for the file in argument.
        Generates all scripts tags for this file and all its dependencies
        (JS and CSS) by reading the manifest file (for production only).
        In development Vite imports all dependencies by itself.
        Place this tag in <head> section of yout page
        (this function marks automaticaly <script> as "async" and "defer").

        Arguments:
            path {str} -- Path to a Vite asset to include.

        Returns:
            str -- All tags to import this asset in yout HTML page.

        Keyword Arguments:
            scripts_attrs {Optional[Dict[str, str]]} -- Override attributes added to scripts tags. (default: {None})
            with_imports {bool} -- If generate assets for dependant assets of this one. (default: {True})

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

        Returns:
            str -- All tags to import this asset in yout HTML page.
        """

        if DJANGO_VITE_DEV_MODE:
            return DjangoViteAssetLoader._generate_script_tag(
                DjangoViteAssetLoader._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
        if "css" in manifest_entry:
            for css_path in manifest_entry["css"]:
                tags.append(
                    DjangoViteAssetLoader._generate_stylesheet_tag(
                        urljoin(settings.STATIC_URL, css_path)
                    )
                )

        # Add dependent "vendor"
        if with_imports and "imports" in manifest_entry:
            for vendor_path in manifest_entry["imports"]:
                tags.append(
                    self.generate_vite_asset(
                        vendor_path,
                        scripts_attrs=scripts_attrs,
                        with_imports=with_imports,
                    )
                )

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

        return "\n".join(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.

        Arguments:
            path {str} -- Path to a Vite asset.

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

        Returns:
            str -- The URL of this asset.
        """

        if DJANGO_VITE_DEV_MODE:
            return DjangoViteAssetLoader._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 urljoin(settings.STATIC_URL, self._manifest[path]["file"])

    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, "r")
            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:
            DjangoViteAssetLoader -- 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:
            str -- 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.

        Arguments:
            src {str} -- Source of the script.

        Keyword Arguments:
            attrs {Optional[Dict[str, str]]} -- 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="{src}"></script>'

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

        Arguments:
            href {str} -- CSS file URL.

        Returns:
            str -- CSS link tag.
        """

        return f'<link rel="stylesheet" href="{href}" />'

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

        Keyword Arguments:
            path {Optional[str]} -- 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 ""),
        )


@register.simple_tag
@mark_safe
def vite_hmr_client() -> 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:
        str -- The script tag or an empty string.
    """

    return DjangoViteAssetLoader.generate_vite_ws_client()


@register.simple_tag
@mark_safe
def vite_asset(
    path: str, scripts_attrs: Optional[Dict[str, str]] = None, with_imports: bool = True
) -> str:
    """
    Generates all assets include tags for the file in argument.
    Generates all scripts tags for this file and all its dependencies
    (JS and CSS) by reading the manifest file (for production only).
    In development Vite imports all dependencies by itself.
    Place this tag in <head> section of yout page
    (this function marks automaticaly <script> as "async" and "defer").

    Arguments:
        path {str} -- Path to a Vite asset to include.

    Keyword Arguments:
        scripts_attrs {Optional[Dict[str, str]]} -- Override attributes added to scripts tags. (default: {None})
        with_imports {bool} -- If generate assets for dependant assets of this one. (default: {True})

    Returns:
        str -- All tags to import this asset in yout HTML page.
    """

    assert path is not None

    return DjangoViteAssetLoader.instance().generate_vite_asset(
        path, scripts_attrs=scripts_attrs, with_imports=with_imports
    )


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

    Arguments:
        path {str} -- Path to a Vite asset.

    Returns:
        [type] -- The URL of this asset.
    """

    assert path is not None

    return DjangoViteAssetLoader.instance().generate_vite_asset_url(path)
