"""
Integrate Betty with `Webpack <https://webpack.js.org/>`_.

This module is internal.
"""

from __future__ import annotations

from asyncio import to_thread
from pathlib import Path
from shutil import copytree
from typing import TYPE_CHECKING

from aiofiles.tempfile import TemporaryDirectory
from typing_extensions import override

from betty import fs
from betty._npm import NpmRequirement, NpmUnavailable
from betty.app import App
from betty.app.extension import Extension, discover_extension_types
from betty.extension.webpack import build
from betty.extension.webpack.build import webpack_build_id
from betty.extension.webpack.jinja2.filter import FILTERS
from betty.generate import Generator, GenerationContext
from betty.html import CssProvider
from betty.jinja2 import Jinja2Provider, Filters, ContextVars
from betty.job import Context
from betty.locale import Str
from betty.requirement import (
    Requirement,
    AllRequirements,
    AnyRequirement,
    RequirementError,
)

if TYPE_CHECKING:
    from collections.abc import Sequence


def _prebuilt_webpack_build_directory_path(
    entry_point_providers: Sequence[WebpackEntryPointProvider & Extension],
) -> Path:
    return (
        fs.PREBUILT_ASSETS_DIRECTORY_PATH
        / "webpack"
        / f"build-{webpack_build_id(entry_point_providers)}"
    )


async def _prebuild_webpack_assets() -> None:
    """
    Prebuild Webpack assets for inclusion in package builds.
    """
    job_context = Context()
    async with App.new_temporary() as app, app:
        app.project.configuration.extensions.enable(Webpack)
        webpack = app.extensions[Webpack]
        app.project.configuration.extensions.enable(
            *{
                extension_type
                for extension_type in discover_extension_types()
                if issubclass(extension_type, WebpackEntryPointProvider)
            }
        )
        await webpack.prebuild(job_context=job_context)


class WebpackEntryPointProvider:
    """
    An extension that provides Webpack entry points.
    """

    @classmethod
    def webpack_entry_point_directory_path(cls) -> Path:
        """
        Get the path to the directory with the entry point assets.

        The directory must include at least a ``package.json`` and ``main.ts``.
        """
        raise NotImplementedError

    def webpack_entry_point_cache_keys(self) -> Sequence[str]:
        """
        Get the keys that make a Webpack build for this provider unique.

        Providers that can be cached regardless may ``return ()``.
        """
        raise NotImplementedError


class PrebuiltAssetsRequirement(Requirement):
    """
    Check if prebuilt assets are available.
    """

    @override
    def is_met(self) -> bool:
        return (fs.PREBUILT_ASSETS_DIRECTORY_PATH / "webpack").is_dir()

    @override
    def summary(self) -> Str:
        return (
            Str._("Pre-built Webpack front-end assets are available")
            if self.is_met()
            else Str._("Pre-built Webpack front-end assets are unavailable")
        )


class Webpack(Extension, CssProvider, Jinja2Provider, Generator):
    """
    Integrate Betty with `Webpack <https://webpack.js.org/>`_.
    """

    _npm_requirement = NpmRequirement()
    _prebuilt_assets_requirement = PrebuiltAssetsRequirement()
    _requirement = AnyRequirement(
        _npm_requirement,
        _prebuilt_assets_requirement,
    )

    @override
    @classmethod
    def name(cls) -> str:
        return "betty.extension.Webpack"

    @override
    @classmethod
    def enable_requirement(cls) -> Requirement:
        return AllRequirements(super().enable_requirement(), cls._requirement)

    def build_requirement(self) -> Requirement:
        """
        Get the requirement that must be satisfied for Webpack builds to be available.
        """
        return self._npm_requirement

    @override
    @classmethod
    def assets_directory_path(cls) -> Path:
        return Path(__file__).parent / "assets"

    @override
    @property
    def public_css_paths(self) -> list[str]:
        return [
            self.app.static_url_generator.generate("css/vendor.css"),
        ]

    @override
    def new_context_vars(self) -> ContextVars:
        return {
            "webpack_js_entry_points": set(),
        }

    @override
    @property
    def filters(self) -> Filters:
        return FILTERS

    @property
    def _project_entry_point_providers(
        self,
    ) -> Sequence[WebpackEntryPointProvider & Extension]:
        return [
            extension
            for extension in self._app.extensions.flatten()
            if isinstance(extension, WebpackEntryPointProvider)
        ]

    @override
    async def generate(self, job_context: GenerationContext) -> None:
        build_directory_path = await self._generate_ensure_build_directory(
            job_context=job_context,
        )
        await self._copy_build_directory(
            build_directory_path, self._app.project.configuration.www_directory_path
        )

    async def prebuild(self, job_context: Context) -> None:
        """
        Prebuild the Webpack assets.
        """
        async with TemporaryDirectory() as working_directory_path_str:
            build_directory_path = await self._new_builder(
                Path(working_directory_path_str),
                job_context=job_context,
            ).build()
            await self._copy_build_directory(
                build_directory_path,
                _prebuilt_webpack_build_directory_path(
                    self._project_entry_point_providers
                ),
            )

    def _new_builder(
        self,
        working_directory_path: Path,
        *,
        job_context: Context,
    ) -> build.Builder:
        return build.Builder(
            working_directory_path,
            self._project_entry_point_providers,
            self._app.project.configuration.debug,
            self._app.renderer,
            job_context=job_context,
            localizer=self._app.localizer,
        )

    async def _copy_build_directory(
        self,
        build_directory_path: Path,
        destination_directory_path: Path,
    ) -> None:
        await to_thread(
            copytree,
            build_directory_path,
            destination_directory_path,
            dirs_exist_ok=True,
        )

    async def _generate_ensure_build_directory(
        self,
        *,
        job_context: Context,
    ) -> Path:
        builder = self._new_builder(
            self._app.binary_file_cache.with_scope("webpack").path,
            job_context=job_context,
        )
        try:
            # (Re)build the assets if `npm` is available.
            return await builder.build()
        except NpmUnavailable:
            pass

        # Use prebuilt assets if they exist.
        prebuilt_webpack_build_directory_path = _prebuilt_webpack_build_directory_path(
            self._project_entry_point_providers
        )
        if prebuilt_webpack_build_directory_path.exists():
            return prebuilt_webpack_build_directory_path

        raise RequirementError(self._requirement)
