# Copyright 2015 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).

import json
import os

from pants.base.exceptions import TaskError
from pants.base.workunit import WorkUnitLabel
from pants.subsystem.subsystem import Subsystem
from pants.util.contextutil import pushd
from pants.util.dirutil import relative_symlink
from pants.util.fileutil import safe_temp_edit

from pants.contrib.node.subsystems.package_managers import (
    PACKAGE_MANAGER_NPM,
    PACKAGE_MANAGER_YARNPKG,
)
from pants.contrib.node.subsystems.resolvers.node_resolver_base import NodeResolverBase
from pants.contrib.node.targets.node_module import NodeModule
from pants.contrib.node.tasks.node_resolve import NodeResolve


class NpmResolver(Subsystem, NodeResolverBase):
    options_scope = "npm-resolver"

    @classmethod
    def register_options(cls, register):
        super().register_options(register)
        register(
            "--install-optional",
            type=bool,
            default=False,
            fingerprint=True,
            help="If enabled, install optional dependencies.",
        )
        register(
            "--install-production",
            type=bool,
            default=False,
            fingerprint=True,
            help="If enabled, do not install devDependencies.",
        )
        register(
            "--force",
            type=bool,
            default=False,
            fingerprint=True,
            help="If enabled, refetch and resolve dependencies even if they are already built.",
        )
        register(
            "--frozen-lockfile",
            type=bool,
            default=True,
            fingerprint=True,
            help="If enabled, disallow automatic update of lock files.",
        )
        # There are cases where passed through options does not override hard-coded options.
        # One example is for node-install, --frozen-lockfile=False is the dominate configuration
        # as it allows the user to modify dependencies and generate a new lockfile.
        # By turning on --force-option-override, the user accepts full responsibilities.
        register(
            "--force-option-override",
            type=bool,
            default=False,
            fingerprint=True,
            advanced=True,
            help="If enabled, options will override hard-coded values. Be aware of default values.",
        )

        NodeResolve.register_resolver_for_type(NodeModule, cls)

    def resolve_target(
        self,
        node_task,
        target,
        results_dir,
        node_paths,
        resolve_locally=False,
        install_optional=None,
        production_only=None,
        force=None,
        frozen_lockfile=None,
        **kwargs,
    ):
        """Installs the node_package target in the results directory copying sources if necessary.

        :param Task node_task: The task executing this method.
        :param Target target: The target being resolve.
        :param String results_dir: The output location where this target will be resolved.
        :param NodePaths node_paths: A mapping of targets and their resolved location, if resolved.
        :param Boolean resolve_locally: If true, the sources do not have to be copied.
        :param Boolean install_optional: If true, install optional dependencies.
        :param Boolean force: If true, rebuild dependencies even if already built.
        :param Boolean frozen_lockfile: Preserve lock file and fails if a change is detected.
        """
        if self.get_options().force_option_override:
            install_optional = self.get_options().install_optional
            production_only = self.get_options().install_production
            force = self.get_options().force
            frozen_lockfile = self.get_options().frozen_lockfile
        else:
            install_optional = (
                install_optional
                if install_optional is not None
                else self.get_options().install_optional
            )
            production_only = (
                production_only
                if production_only is not None
                else self.get_options().install_production
            )
            force = force if force is not None else self.get_options().force
            frozen_lockfile = (
                frozen_lockfile
                if frozen_lockfile is not None
                else self.get_options().frozen_lockfile
            )

        if not resolve_locally:
            self._copy_sources(target, results_dir)
        with pushd(results_dir):
            if not os.path.exists("package.json"):
                raise TaskError(
                    "Cannot find package.json. Did you forget to put it in target sources?"
                )
            # TODO: remove/remodel the following section when node_module dependency is fleshed out.
            package_manager = node_task.get_package_manager(target=target).name
            if package_manager == PACKAGE_MANAGER_NPM:
                if resolve_locally:
                    raise TaskError(
                        "Resolving node package modules locally is not supported for NPM."
                    )
                if os.path.exists("npm-shrinkwrap.json"):
                    node_task.context.log.info(
                        "Found npm-shrinkwrap.json, will not inject package.json"
                    )
                else:
                    node_task.context.log.warn(
                        "Cannot find npm-shrinkwrap.json. Did you forget to put it in target sources? "
                        "This package will fall back to inject package.json with pants BUILD dependencies "
                        "including node_remote_module and other node dependencies. However, this is "
                        "not fully supported."
                    )
                    self._emit_package_descriptor(node_task, target, results_dir, node_paths)
            elif package_manager == PACKAGE_MANAGER_YARNPKG:
                if not os.path.exists("yarn.lock") and frozen_lockfile:
                    raise TaskError(
                        "Cannot find yarn.lock. Did you forget to put it in target sources?"
                    )
            # Install all dependencies except for `file:` dependencies
            # `file:` dependencies are special dependencies that point to a local path to a pants node target
            # `file:` dependencies are already in the build graph and should be already be installed by this point

            # Copy the package.json and then remove the file: dependencies from package.json
            # Run the install and symlink the file: dependencies using their node_paths
            # Afterwards, restore the original package.json to not cause diff changes when resolve_locally=True
            # The file mutation is occurring in place and the package.json may potentially not be restored here
            # if the process is closed.
            with safe_temp_edit("package.json") as package_json:
                with open(package_json, "r") as package_json_file:
                    json_data = json.load(package_json_file)
                    source_deps = {