"""
Actions specific to the esbuild bundler
"""

import logging
from typing import Any, Dict

from aws_lambda_builders.actions import ActionFailedError, BaseAction, Purpose
from aws_lambda_builders.workflows.nodejs_npm.utils import OSUtils
from aws_lambda_builders.workflows.nodejs_npm_esbuild.esbuild import EsbuildCommandBuilder, SubprocessEsbuild
from aws_lambda_builders.workflows.nodejs_npm_esbuild.exceptions import EsbuildExecutionError

LOG = logging.getLogger(__name__)

EXTERNAL_KEY = "external"
# minimum esbuild version required to use "--external"
MINIMUM_VERSION_FOR_EXTERNAL = "0.14.13"


class EsbuildBundleAction(BaseAction):
    """
    A Lambda Builder Action that packages a Node.js package using esbuild into a single file
    optionally transpiling TypeScript
    """

    NAME = "EsbuildBundle"
    DESCRIPTION = "Packaging source using Esbuild"
    PURPOSE = Purpose.COPY_SOURCE

    def __init__(
        self,
        working_directory: str,
        output_directory: str,
        bundler_config: Dict[str, Any],
        osutils: OSUtils,
        subprocess_esbuild: SubprocessEsbuild,
        manifest: str,
        skip_deps=False,
    ):
        """
        Parameters
        ----------
        working_directory : str
            directory where esbuild is executed
        output_directory : str
            an existing (writable) directory where to store the output.
            Note that the actual result will be in the 'package' subdirectory here.
        bundler_config : Dict[str, Any]
            the bundle configuration
        osutils : OSUtils
            An instance of OS Utilities for file manipulation
        subprocess_esbuild : SubprocessEsbuild
            An instance of the Esbuild process wrapper
        manifest : str
            path to package.json file contents to read
        skip_deps : bool, optional
            if dependencies should be omitted from bundling, by default False
        """
        super(EsbuildBundleAction, self).__init__()
        self._working_directory = working_directory
        self._output_directory = output_directory
        self._bundler_config = bundler_config
        self._osutils = osutils
        self._subprocess_esbuild = subprocess_esbuild
        self._skip_deps = skip_deps
        self._manifest = manifest

    def execute(self) -> None:
        """
        Runs the action.

        Raises
        ------
        ActionFailedError
            when esbuild packaging fails
        """
        esbuild_command = EsbuildCommandBuilder(
            self._working_directory, self._output_directory, self._bundler_config, self._osutils, self._manifest
        )

        if self._should_bundle_deps_externally():
            check_minimum_esbuild_version(
                minimum_version_required=MINIMUM_VERSION_FOR_EXTERNAL,
                working_directory=self._working_directory,
                subprocess_esbuild=self._subprocess_esbuild,
            )
            esbuild_command.build_with_no_dependencies()
            if EXTERNAL_KEY in self._bundler_config:
                # Already marking everything as external,
                # shouldn't attempt to do it again when building args from config
                self._bundler_config.pop(EXTERNAL_KEY)

        args = (
            esbuild_command.build_entry_points().build_default_values().build_esbuild_args_from_config().get_command()
        )

        try:
            self._subprocess_esbuild.run(args, cwd=self._working_directory)
        except EsbuildExecutionError as ex:
            raise ActionFailedError(str(ex))

    def _should_bundle_deps_externally(self) -> bool:
        """
        Checks if all dependencies should be marked as external and not bundled with source code

        :rtype: boolean
        :return: True if all dependencies should be marked as external
        """
        return self._skip_deps or "./node_modules/*" in self._bundler_config.get(EXTERNAL_KEY, [])


def check_minimum_esbuild_version(
    minimum_version_required: str, working_directory: str, subprocess_esbuild: SubprocessEsbuild
):
    """
    Checks esbuild version against a minimum version required.

    Parameters
    ----------
    minimum_version_required: str
        minimum esbuild version required for check to pass

    working_directory: str
        directory where esbuild is executed

    subprocess_esbuild: aws_lambda_builders.workflows.nodejs_npm_esbuild.esbuild.SubprocessEsbuild
        An instance of the Esbuild process wrapper

    Raises
    ----------
    lambda_builders.actions.ActionFailedError
        when esbuild version checking fails
    """
    args = ["--version"]

    try:
        version = subprocess_esbuild.run(args, cwd=working_directory)
    except EsbuildExecutionError as ex:
        raise ActionFailedError(str(ex))

    LOG.debug("Found esbuild with version: %s", version)

    try:
        check_version = _get_version_tuple(minimum_version_required)
        esbuild_version = _get_version_tuple(version)

        if esbuild_version < check_version:
            raise ActionFailedError(
                f"Unsupported esbuild version. To use a dependency layer, the esbuild version must be at "
                f"least {minimum_version_required}. Version found: {version}"
            )
    except (TypeError, ValueError) as ex:
        raise ActionFailedError(f"Unable to parse esbuild version: {str(ex)}")


def _get_version_tuple(version_string: str):
    """
    Get an integer tuple representation of the version for comparison

    Parameters
    ----------
    version_string: str
        string containing the esbuild version

    Returns
    ----------
    tuple
        version tuple used for comparison
    """
    return tuple(map(int, version_string.split(".")))
