# Copyright (c) Facebook, Inc. and its affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.

import argparse
import importlib
from enum import Enum
from pathlib import Path
from typing import List

from fixit.common.base import LintRuleT
from fixit.common.config import get_lint_config, get_rules_from_config
from fixit.common.utils import (
    find_and_import_rule,
    import_distinct_rules_from_package,
    LintRuleNotFoundError,
)


class FixtureDirNotFoundError(Exception):
    pass


def import_rule(rule_name: str) -> LintRuleT:
    # Using the rule_name or full dotted name, attempt to import the rule.
    rule_module_path, _, rule_class_name = rule_name.rpartition(".")
    if rule_module_path:
        # If user provided a dotted path, we assume it's valid and import the rule directly.
        imported_rule = getattr(
            importlib.import_module(rule_module_path),
            rule_class_name,
        )
        return imported_rule
    # Otherwise, only a class name was provided, so try to find the rule by searching each package specified in the config.
    return find_and_import_rule(rule_class_name, get_lint_config().packages)


def get_pyre_fixture_dir_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        "--fixture-dir",
        type=(lambda p: Path(p).resolve(strict=True)),
        help=("Main fixture file directory for integration testing."),
        default=get_lint_config().fixture_dir,
    )
    return parser


def get_rules_package_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        "--rules-package",
        help=("Full dotted path of a package containing lint rules."),
        default="fixit.rules",
    )
    return parser


def get_rule_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        "rule",
        type=import_rule,
        help=(
            "The name of your lint rule class or the full dotted path to your lint rule class. "
            + "(e.g. `NoAssertEqualsRule` or `fixit.rules.no_assert_equals.NoAssertEqualsRule`)"
        ),
    )
    return parser


class RuleAction(argparse.Action):
    def __call__(
        self,
        parser: argparse.ArgumentParser,
        namespace: argparse.Namespace,
        values: List[str],
        option_string: str,
    ) -> None:
        all_rules = set()
        for rule_or_package in values:
            try:
                # Try to treat as a package first.
                all_rules.update(import_distinct_rules_from_package(rule_or_package))
            except ModuleNotFoundError:
                try:
                    all_rules.add(import_rule(rule_or_package))
                except LintRuleNotFoundError:
                    raise ValueError(
                        f"Unable to import rule or package named {rule_or_package}"
                    )
        setattr(namespace, self.dest, all_rules)


def get_rules_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        "--rules",
        nargs="*",
        help=(
            "The names of lint rule classes to run, or packages containing lint rules, separated by a space. "
            + "(e.g `--rules NoAssertEqualsRule NoUnnecessaryListComprehensionRule my.custom.package`)"
        ),
        action=RuleAction,
        dest="rules",
        default=get_rules_from_config(),
    )
    return parser


def enforce_relative_to_repo_root(_path: str) -> Path:
    repo_root = get_lint_config().repo_root
    try:
        path = Path(_path).resolve(strict=True)
        path.relative_to(repo_root)
        return path
    except ValueError:
        raise argparse.ArgumentTypeError(
            f"Invalid value {_path}.\n"
            + f"Paths must be relative to the repo root `{repo_root}`"
            + " from the `repo_root` setting in the `.fixit.config.yaml` file."
        )


def get_paths_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        "paths",
        nargs="*",
        type=enforce_relative_to_repo_root,
        default=(Path(get_lint_config().repo_root),),
        help=(
            "The name of a directory (e.g. media) or file (e.g. media/views.py) "
            + "relative to the `repo_root` specified in the `fixit.config.yaml` file"
            + " on which to run this script. "
            + "If not specified, the `repo_root` value is used."
        ),
    )
    return parser


def get_use_ignore_comments_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        "--use-ignore-comments",
        action="store_true",
        default=True,
        help="Obey `# noqa`, `# lint-fixme` and `# lint-ignore` comments.",
    )
    return parser


def get_skip_ignore_comments_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        "--skip-ignore-comments",
        dest="use_ignore_comments",
        action="store_false",
        help="Ignore `# noqa`, `# lint-fixme` and `# lint-ignore` comments.",
    )
    return parser


def get_skip_ignore_byte_marker_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        "--skip-ignore-byte-markers",
        dest="use_ignore_byte_markers",
        action="store_false",
        help=f"Ignore `@gen{''}erated` and `@no{''}lint` markers in files.",
    )
    return parser


def get_metadata_cache_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        "--cache-timeout",
        type=int,
        help="Timeout (seconds) for metadata cache fetching. Default is 2 seconds.",
        default=2,
    )
    return parser


def get_compact_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        "--compact",
        action="store_true",
        help=(
            "Use a compact output that omits the message. This should be easier for "
            + "other scripts to parse."
        ),
    )
    return parser


def get_skip_autoformatter_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        "--skip-autoformatter",
        action="store_true",
        help=(
            "Skips the autoformatter (e.g. Black) that's normally run after changes "
            + "are applied."
        ),
    )
    return parser


class LintWorkers(Enum):
    # Spawn (up to) one worker process per CPU core
    CPU_COUNT = "cpu_count"
    # Disable the process pool, and compute results in the current thread and process.
    #
    # This can be useful for debugging, where the process pool may break tracebacks,
    # debuggers, or profilers.
    USE_CURRENT_THREAD = "use_current_thread"


def get_multiprocessing_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        "--no-multi",
        dest="workers",
        action="store_const",
        const=LintWorkers.USE_CURRENT_THREAD,
        default=LintWorkers.CPU_COUNT,
        help="Run the lint rule with multiprocessing disabled.",
    )
    return parser
