# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

import json
import os
import signal
import subprocess
import sys

sys.path.append(os.path.join(os.path.dirname(__file__), "eslint"))
from mozbuild.nodeutil import check_node_executables_valid, find_node_executable
from mozlint import result

from eslint import prettier_utils, setup_helper

ESLINT_ERROR_MESSAGE = """
An error occurred running eslint. Please check the following error messages:

{}
""".strip()

ESLINT_NOT_FOUND_MESSAGE = """
Could not find eslint!  We looked at the --binary option, at the ESLINT
environment variable, and then at your local node_modules path. Please Install
eslint and needed plugins with:

mach eslint --setup

and try again.
""".strip()


def setup(root, **lintargs):
    setup_helper.set_project_root(root)

    if not check_node_executables_valid():
        return 1

    return setup_helper.eslint_maybe_setup()


def lint(paths, config, binary=None, fix=None, rules=[], setup=None, **lintargs):
    """Run eslint."""
    log = lintargs["log"]
    setup_helper.set_project_root(lintargs["root"])
    module_path = setup_helper.get_project_root()

    # Valid binaries are:
    #  - Any provided by the binary argument.
    #  - Any pointed at by the ESLINT environmental variable.
    #  - Those provided by |mach lint --setup|.

    if not binary:
        binary, _ = find_node_executable()

    if not binary:
        log.error(ESLINT_NOT_FOUND_MESSAGE)
        return 1

    extra_args = lintargs.get("extra_args") or []

    result = {"results": [], "fixed": 0}

    # If something passes formatonly, avoid running eslint and only run prettier.
    # This allows ./mach format and similar mechanisms to work correctly.
    if not lintargs.get("formatonly", False):
        exclude_args = []
        for path in config.get("exclude", []):
            exclude_args.extend([
                "--ignore-pattern",
                os.path.relpath(path, lintargs["root"]),
            ])

        for rule in rules:
            extra_args.extend(["--rule", rule])

        i = 0
        extra_args_len = len(extra_args)
        while i < extra_args_len:
            if extra_args[i] == "--rule":
                i += 1
                # ESLint requires that we specify any plugins. These are typically
                # named by the prefix before the first `/`. There are some cases where
                # this doesn't work, but ESLint also can't handle those cases with
                # the `--plugin` argument anyway.
                if i < extra_args_len and "/" in extra_args[i]:
                    extra_args.extend(["--plugin", extra_args[i].split("/", 1)[0]])

            i += 1

        # First run ESLint
        cmd_args = (
            [
                binary,
                os.path.join(module_path, "node_modules", "eslint", "bin", "eslint.js"),
                # This keeps ext as a single argument.
                "--format",
                "json",
                "--no-error-on-unmatched-pattern",
            ]
            + rules
            # Flat configuration doesn't understand --ignore-path, though Prettier does.
            + list(filter(lambda x: not x.startswith("--ignore-path"), extra_args))
            + exclude_args
            + paths
        )

        if fix:
            # eslint requires that --fix be set before the --ext argument.
            cmd_args.insert(2, "--fix")

        log.debug("ESLint command: {}".format(" ".join(cmd_args)))

        result = run(cmd_args, config)
        if result == 1:
            return result

    # Then run Prettier
    ignore_args = (
        []
        if any(a.startswith("--ignore-path") for a in extra_args)
        else ["--ignore-path=.prettierignore", "--ignore-path=.prettierignore-css"]
    )

    # Don't pass the configuration, plugin or rule information to Prettier,
    # as it doesn't understand those arguments.
    def bypass(arg):
        bypass_list = ["--config", "--plugin", "--rule"]
        return any(not arg.startswith(flag) for flag in bypass_list)

    cmd_args = (
        [
            binary,
            os.path.join(
                module_path, "node_modules", "prettier", "bin", "prettier.cjs"
            ),
            "--list-different",
            "--no-error-on-unmatched-pattern",
        ]
        # Don't pass the configuration, plugin or rule information to Prettier,
        # as it doesn't understand those arguments.
        + list(
            filter(
                lambda x: (
                    not x.startswith("--config")
                    and not x.startswith("--plugin")
                    and not x.startswith("--rule")
                ),
                [arg for arg in extra_args if bypass(arg)],
            )
        )
        # Prettier does not support exclude arguments.
        # + exclude_args
        + ignore_args
        + paths
    )
    log.debug("Prettier command: {}".format(" ".join(cmd_args)))

    if fix:
        cmd_args.append("--write")

    prettier_result = prettier_utils.run_prettier(cmd_args, config, fix)
    if prettier_result == 1:
        return prettier_result

    result["results"].extend(prettier_result["results"])
    result["fixed"] = result["fixed"] + prettier_result["fixed"]
    return result


def run(cmd_args, config):
    shell = False
    if (
        os.environ.get("MSYSTEM") in ("MINGW32", "MINGW64")
        or "MOZILLABUILD" in os.environ
    ):
        # The eslint binary needs to be run from a shell with msys
        shell = True
    encoding = "utf-8"

    orig = signal.signal(signal.SIGINT, signal.SIG_IGN)
    proc = subprocess.Popen(
        cmd_args, shell=shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE
    )
    signal.signal(signal.SIGINT, orig)

    try:
        output, errors = proc.communicate()
    except KeyboardInterrupt:
        proc.kill()
        return {"results": [], "fixed": 0}

    if errors:
        errors = errors.decode(encoding, "replace")
        print(ESLINT_ERROR_MESSAGE.format(errors))

    if proc.returncode >= 2:
        return 1

    if not output:
        return {"results": [], "fixed": 0}  # no output means success
    output = output.decode(encoding, "replace")
    try:
        jsonresult = json.loads(output)
    except ValueError:
        print(ESLINT_ERROR_MESSAGE.format(output))
        return 1

    results = []
    fixed = 0
    for obj in jsonresult:
        errors = obj["messages"]
        # This will return a count of files fixed, rather than issues fixed, as
        # that is the only count we have.
        if "output" in obj:
            fixed = fixed + 1

        for err in errors:
            err.update({
                "hint": err.get("fix"),
                "level": "error" if err["severity"] == 2 else "warning",
                "lineno": err.get("line") or 0,
                "path": obj["filePath"],
                "rule": err.get("ruleId"),
            })
            results.append(result.from_config(config, **err))

    return {"results": results, "fixed": fixed}
