# 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 re
import signal
import subprocess
import sys

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

STYLELINT_ERROR_MESSAGE = """
An error occurred running stylelint. Please check the following error messages:

{}
""".strip()

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

mach eslint --setup

and try again.
""".strip()

FILE_EXT_REGEX = re.compile(r"\.[a-z0-9_]{2,10}$", re.IGNORECASE)


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 stylelint."""
    log = lintargs["log"]
    setup_helper.set_project_root(lintargs["root"])
    module_path = setup_helper.get_project_root()

    modified_paths = []
    exts = "*.{" + ",".join(config["extensions"]) + "}"

    for path in paths:
        filepath, fileext = os.path.splitext(path)
        if fileext:
            modified_paths += [path]
        else:
            joined_path = os.path.join(path, "**", exts)
            if prettier_utils.is_windows():
                joined_path = joined_path.replace("\\", "/")
            modified_paths.append(joined_path)

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

    if not binary:
        binary, _ = find_node_executable()

    if not binary:
        print(STYLELINT_NOT_FOUND_MESSAGE)
        return 1

    extra_args = lintargs.get("extra_args") or []
    result = {"results": [], "fixed": 0}

    # If something passes formatonly, avoid running stylelint 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"]),
            ])

        # Default to $topsrcdir/.stylelintrc.js, but allow override in stylelint.yml
        stylelint_rc = config.get("stylelint-rc", ".stylelintrc.js")

        # First run Stylelint
        cmd_args = (
            [
                binary,
                os.path.join(
                    module_path, "node_modules", "stylelint", "bin", "stylelint.mjs"
                ),
                "--formatter",
                "json",
                "--allow-empty-input",
                "--config",
                os.path.join(lintargs["root"], stylelint_rc),
            ]
            + extra_args
            + exclude_args
            + modified_paths
        )

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

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

        # Set up environment for stylelint subprocess
        env = os.environ.copy()
        if lintargs.get("skip_rollouts", False):
            env["STYLELINT_SKIP_ROLLOUTS"] = "1"

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

    # Then run Prettier
    cmd_args = (
        [
            binary,
            os.path.join(
                module_path, "node_modules", "prettier", "bin", "prettier.cjs"
            ),
            "--list-different",
            "--no-error-on-unmatched-pattern",
            "--ignore-path=.prettierignore",
            "--ignore-path=.prettierignore-non-css",
        ]
        # Prettier does not support exclude arguments.
        # + exclude_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, fix, env):
    shell = False
    if prettier_utils.is_windows():
        # The stylelint 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, env=env
    )
    signal.signal(signal.SIGINT, orig)

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

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

    # 0 is success, 2 is there was at least 1 rule violation. Anything else
    # is more serious.
    if proc.returncode not in {0, 2}:
        if proc.returncode == 78:
            print("Stylelint reported an issue with its configuration file.")
            print(errors)
        return 1

    if not errors:
        return {"results": [], "fixed": 0}

    try:
        jsonresult = json.loads(errors)
    except ValueError:
        print(STYLELINT_ERROR_MESSAGE.format(errors))
        return 1

    results = []
    fixed = 0
    for obj in jsonresult:
        errors = obj["warnings"] + obj["parseErrors"]
        # This will return a number of fixed files, as that's the only thing
        # stylelint gives us. Note that it also seems to sometimes list files
        # like this where it finds nothing and fixes nothing. It's not clear
        # why... but this is why we also check if we were even trying to fix
        # anything.
        if fix and not errors and not obj.get("ignored"):
            fixed += 1

        for err in errors:
            msg = err.get("text")
            if err.get("rule"):
                # stylelint includes the rule id in the error message.
                # All mozlint formatters that include the error message also already
                # separately include the rule id, so that leads to duplication. Fix:
                msg = msg.replace("(" + err.get("rule") + ")", "").strip()
            err.update({
                "message": msg,
                "level": err.get("severity") or "error",
                "lineno": err.get("line") or 0,
                "path": obj["source"],
                "rule": err.get("rule") or "parseError",
            })
            results.append(result.from_config(config, **err))

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