"""
Action to resolve NodeJS dependencies using NPM
"""

import logging
import os
from typing import Optional

from aws_lambda_builders.actions import ActionFailedError, BaseAction, Purpose
from aws_lambda_builders.utils import extract_tarfile
from aws_lambda_builders.workflows.nodejs_npm.npm import NpmExecutionError, SubprocessNpm

LOG = logging.getLogger(__name__)


class NodejsNpmPackAction(BaseAction):
    """
    A Lambda Builder Action that packages a Node.js package using NPM to extract the source and remove test resources
    """

    NAME = "NpmPack"
    DESCRIPTION = "Packaging source using NPM"
    PURPOSE = Purpose.COPY_SOURCE

    def __init__(self, artifacts_dir, scratch_dir, manifest_path, osutils, subprocess_npm):
        """
        :type artifacts_dir: str
        :param artifacts_dir: an existing (writable) directory where to store the output.
            Note that the actual result will be in the 'package' subdirectory here.

        :type scratch_dir: str
        :param scratch_dir: an existing (writable) directory for temporary files

        :type manifest_path: str
        :param manifest_path: path to package.json of an NPM project with the source to pack

        :type osutils: aws_lambda_builders.workflows.nodejs_npm.utils.OSUtils
        :param osutils: An instance of OS Utilities for file manipulation

        :type subprocess_npm: aws_lambda_builders.workflows.nodejs_npm.npm.SubprocessNpm
        :param subprocess_npm: An instance of the NPM process wrapper
        """
        super(NodejsNpmPackAction, self).__init__()
        self.artifacts_dir = artifacts_dir
        self.manifest_path = manifest_path
        self.scratch_dir = scratch_dir
        self.osutils = osutils
        self.subprocess_npm = subprocess_npm

    def execute(self):
        """
        Runs the action.

        :raises lambda_builders.actions.ActionFailedError: when NPM packaging fails
        """
        try:
            package_path = "file:{}".format(self.osutils.abspath(self.osutils.dirname(self.manifest_path)))

            LOG.debug("NODEJS packaging %s to %s", package_path, self.scratch_dir)

            tarfile_name = self.subprocess_npm.run(["pack", "-q", package_path], cwd=self.scratch_dir).splitlines()[-1]

            LOG.debug("NODEJS packed to %s", tarfile_name)

            tarfile_path = self.osutils.joinpath(self.scratch_dir, tarfile_name)

            LOG.debug("NODEJS extracting to %s", self.artifacts_dir)

            extract_tarfile(tarfile_path, self.artifacts_dir)

        except NpmExecutionError as ex:
            raise ActionFailedError(str(ex))


class NodejsNpmInstallOrUpdateBaseAction(BaseAction):
    """
    A base Lambda Builder Action that is used for installs or updating NPM project dependencies
    """

    PURPOSE = Purpose.RESOLVE_DEPENDENCIES

    def __init__(self, install_dir: str, subprocess_npm: SubprocessNpm):
        """
        Parameters
        ----------
        install_dir : str
            Dependencies will be installed in this directory.
        subprocess_npm : SubprocessNpm
            An instance of the NPM process wrapper
        """

        super().__init__()
        self.install_dir = install_dir
        self.subprocess_npm = subprocess_npm


class NodejsNpmInstallAction(NodejsNpmInstallOrUpdateBaseAction):
    """
    A Lambda Builder Action that installs NPM project dependencies
    """

    NAME = "NpmInstall"
    DESCRIPTION = "Installing dependencies from NPM"

    def execute(self):
        """
        Runs the action.

        :raises lambda_builders.actions.ActionFailedError: when NPM execution fails
        """
        try:
            LOG.debug("NODEJS installing production dependencies in: %s", self.install_dir)

            command = ["install", "-q", "--no-audit", "--no-save", "--unsafe-perm", "--production"]
            self.subprocess_npm.run(command, cwd=self.install_dir)

        except NpmExecutionError as ex:
            raise ActionFailedError(str(ex))


class NodejsNpmUpdateAction(NodejsNpmInstallOrUpdateBaseAction):
    """
    A Lambda Builder Action that installs NPM project dependencies
    """

    NAME = "NpmUpdate"
    DESCRIPTION = "Updating dependencies from NPM"

    def execute(self):
        """
        Runs the action.

        :raises lambda_builders.actions.ActionFailedError: when NPM execution fails
        """
        try:
            LOG.debug("NODEJS updating production dependencies in: %s", self.install_dir)

            command = [
                "update",
                "--no-audit",
                "--no-save",
                "--unsafe-perm",
                "--production",
                "--no-package-lock",
                "--install-links",
            ]
            self.subprocess_npm.run(command, cwd=self.install_dir)

        except NpmExecutionError as ex:
            raise ActionFailedError(str(ex))


class NodejsNpmCIAction(BaseAction):
    """
    A Lambda Builder Action that installs NPM project dependencies
    using the CI method - which is faster and better reproducible
    for CI environments, but requires a lockfile (package-lock.json
    or npm-shrinkwrap.json)
    """

    NAME = "NpmCI"
    DESCRIPTION = "Installing dependencies from NPM using the CI method"
    PURPOSE = Purpose.RESOLVE_DEPENDENCIES

    def __init__(self, install_dir: str, subprocess_npm: SubprocessNpm, install_links: Optional[bool] = False):
        """
        Parameters
        ----------
        install_dir : str
            Dependencies will be installed in this directory.
        subprocess_npm : SubprocessNpm
            An instance of the NPM process wrapper
        install_links : Optional[bool]
            Uses the --install-links npm option if True, by default False
        """

        super(NodejsNpmCIAction, self).__init__()
        self.install_dir = install_dir
        self.subprocess_npm = subprocess_npm
        self.install_links = install_links

    def execute(self):
        """
        Runs the action.

        :raises lambda_builders.actions.ActionFailedError: when NPM execution fails
        """

        try:
            LOG.debug("NODEJS installing ci in: %s", self.install_dir)

            command = ["ci"]
            if self.install_links:
                command.append("--install-links")

            self.subprocess_npm.run(command, cwd=self.install_dir)

        except NpmExecutionError as ex:
            raise ActionFailedError(str(ex))


class NodejsNpmrcAndLockfileCopyAction(BaseAction):
    """
    A Lambda Builder Action that copies lockfile and NPM config file .npmrc
    """

    NAME = "CopyNpmrcAndLockfile"
    DESCRIPTION = "Copying configuration from .npmrc and dependencies from lockfile/shrinkwrap"
    PURPOSE = Purpose.COPY_SOURCE

    def __init__(self, artifacts_dir, source_dir, osutils):
        """
        :type artifacts_dir: str
        :param artifacts_dir: an existing (writable) directory with project source files.
            Dependencies will be installed in this directory.

        :type source_dir: str
        :param source_dir: directory containing project source files.

        :type osutils: aws_lambda_builders.workflows.nodejs_npm.utils.OSUtils
        :param osutils: An instance of OS Utilities for file manipulation
        """

        super(NodejsNpmrcAndLockfileCopyAction, self).__init__()
        self.artifacts_dir = artifacts_dir
        self.source_dir = source_dir
        self.osutils = osutils

    def execute(self):
        """
        Runs the action.

        :raises lambda_builders.actions.ActionFailedError: when copying fails
        """

        try:
            for filename in [".npmrc", "package-lock.json", "npm-shrinkwrap.json"]:
                file_path = self.osutils.joinpath(self.source_dir, filename)
                if self.osutils.file_exists(file_path):
                    LOG.debug("%s copying in: %s", filename, self.artifacts_dir)
                    self.osutils.copy_file(file_path, self.artifacts_dir)

        except OSError as ex:
            raise ActionFailedError(str(ex))


class NodejsNpmrcCleanUpAction(BaseAction):
    """
    A Lambda Builder Action that cleans NPM config file .npmrc
    """

    NAME = "CleanUpNpmrc"
    DESCRIPTION = "Cleans artifacts dir"
    PURPOSE = Purpose.COPY_SOURCE

    def __init__(self, artifacts_dir, osutils):
        """
        :type artifacts_dir: str
        :param artifacts_dir: an existing (writable) directory with project source files.
            Dependencies will be installed in this directory.

        :type osutils: aws_lambda_builders.workflows.nodejs_npm.utils.OSUtils
        :param osutils: An instance of OS Utilities for file manipulation
        """

        super(NodejsNpmrcCleanUpAction, self).__init__()
        self.artifacts_dir = artifacts_dir
        self.osutils = osutils

    def execute(self):
        """
        Runs the action.

        :raises lambda_builders.actions.ActionFailedError: when deleting .npmrc fails
        """

        try:
            npmrc_path = self.osutils.joinpath(self.artifacts_dir, ".npmrc")
            if self.osutils.file_exists(npmrc_path):
                LOG.debug(".npmrc cleanup in: %s", self.artifacts_dir)
                self.osutils.remove_file(npmrc_path)

        except OSError as ex:
            raise ActionFailedError(str(ex))


class NodejsNpmLockFileCleanUpAction(BaseAction):
    """
    A Lambda Builder Action that cleans up garbage lockfile left by 7 in node_modules
    """

    NAME = "LockfileCleanUp"
    DESCRIPTION = "Cleans garbage lockfiles dir"
    PURPOSE = Purpose.COPY_SOURCE

    def __init__(self, artifacts_dir, osutils):
        """
        :type artifacts_dir: str
        :param artifacts_dir: an existing (writable) directory with project source files.
            Dependencies will be installed in this directory.

        :type osutils: aws_lambda_builders.workflows.nodejs_npm.utils.OSUtils
        :param osutils: An instance of OS Utilities for file manipulation
        """

        super(NodejsNpmLockFileCleanUpAction, self).__init__()
        self.artifacts_dir = artifacts_dir
        self.osutils = osutils

    def execute(self):
        """
        Runs the action.

        :raises lambda_builders.actions.ActionFailedError: when deleting the lockfile fails
        """

        try:
            npmrc_path = self.osutils.joinpath(self.artifacts_dir, "node_modules", ".package-lock.json")
            if self.osutils.file_exists(npmrc_path):
                LOG.debug(".package-lock cleanup in: %s", self.artifacts_dir)
                self.osutils.remove_file(npmrc_path)

        except OSError as ex:
            raise ActionFailedError(str(ex))


class NodejsNpmTestAction(NodejsNpmInstallOrUpdateBaseAction):
    """
    A Lambda Builder Action that runs tests in NPM project
    """

    NAME = "NpmTest"
    DESCRIPTION = "Running tests from NPM"

    def execute(self):
        """
        Runs the action if environment variable `SAM_NPM_RUN_TEST_WITH_BUILD` is `true`.

        :raises lambda_builders.actions.ActionFailedError: when NPM execution fails
        """
        try:
            is_run_test_with_build = os.getenv("SAM_NPM_RUN_TEST_WITH_BUILD", "False")
            if is_run_test_with_build == "true":
                LOG.debug("NODEJS running tests in: %s", self.install_dir)

                command = ["test", "--if-present"]
                self.subprocess_npm.run(command, cwd=self.install_dir)
            else:
                LOG.debug("NODEJS skipping tests")
                LOG.debug("Add env variable 'SAM_NPM_RUN_TEST_WITH_BUILD=true' to run tests with build")

        except NpmExecutionError as ex:
            raise ActionFailedError(str(ex))
