# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE
# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt

# pylint: disable=too-many-arguments, redefined-builtin, duplicate-code

"""Callback actions for various options."""

from __future__ import annotations

import abc
import argparse
import sys
import warnings
from collections.abc import Callable, Sequence
from pathlib import Path
from typing import TYPE_CHECKING, Any

from pylint import exceptions, extensions, interfaces, utils

if TYPE_CHECKING:
    from pylint.config.help_formatter import _HelpFormatter
    from pylint.lint import PyLinter
    from pylint.lint.run import Run


class _CallbackAction(argparse.Action):
    """Custom callback action."""

    @abc.abstractmethod
    def __call__(
        self,
        parser: argparse.ArgumentParser,
        namespace: argparse.Namespace,
        values: str | Sequence[Any] | None,
        option_string: str | None = None,
    ) -> None:
        raise NotImplementedError  # pragma: no cover


class _DoNothingAction(_CallbackAction):
    """Action that just passes.

    This action is used to allow pre-processing of certain options
    without erroring when they are then processed again by argparse.
    """

    def __call__(
        self,
        parser: argparse.ArgumentParser,
        namespace: argparse.Namespace,
        values: str | Sequence[Any] | None,
        option_string: str | None = None,
    ) -> None:
        return None


class _ExtendAction(argparse._AppendAction):
    """Action that adds the value to a pre-existing list.

    It is directly copied from the stdlib implementation which is only available
    on 3.8+.
    """

    def __call__(
        self,
        parser: argparse.ArgumentParser,
        namespace: argparse.Namespace,
        values: str | Sequence[Any] | None,
        option_string: str | None = None,
    ) -> None:
        assert isinstance(values, (tuple, list))
        current = getattr(namespace, self.dest, [])
        assert isinstance(current, list)
        current.extend(values)
        setattr(namespace, self.dest, current)


class _AccessRunObjectAction(_CallbackAction):
    """Action that has access to the Run object."""

    def __init__(
        self,
        option_strings: Sequence[str],
        dest: str,
        nargs: None = None,
        const: None = None,
        default: None = None,
        type: None = None,
        choices: None = None,
        required: bool = False,
        help: str = "",
        metavar: str = "",
        **kwargs: Run,
    ) -> None:
        self.run = kwargs["Run"]

        super().__init__(
            option_strings,
            dest,
            0,
            const,
            default,
            type,
            choices,
            required,
            help,
            metavar,
        )

    @abc.abstractmethod
    def __call__(
        self,
        parser: argparse.ArgumentParser,
        namespace: argparse.Namespace,
        values: str | Sequence[Any] | None,
        option_string: str | None = None,
    ) -> None:
        raise NotImplementedError  # pragma: no cover


class _MessageHelpAction(_CallbackAction):
    """Display the help message of a message."""

    def __init__(
        self,
        option_strings: Sequence[str],
        dest: str,
        nargs: None = None,
        const: None = None,
        default: None = None,
        type: None = None,
        choices: None = None,
        required: bool = False,
        help: str = "",
        metavar: str = "",
        **kwargs: Run,
    ) -> None:
        self.run = kwargs["Run"]
        super().__init__(
            option_strings,
            dest,
            "+",
            const,
            default,
            type,
            choices,
            required,
            help,
            metavar,
        )

    def __call__(
        self,
        parser: argparse.ArgumentParser,
        namespace: argparse.Namespace,
        values: str | Sequence[str] | None,
        option_string: str | None = "--help-msg",
    ) -> None:
        assert isinstance(values, (list, tuple))
        values_to_print: list[str] = []
        for msg in values:
            assert isinstance(msg, str)
            values_to_print += utils._check_csv(msg)
        self.run.linter.msgs_store.help_message(values_to_print)
        sys.exit(0)


class _ListMessagesAction(_AccessRunObjectAction):
    """Display all available messages."""

    def __call__(
        self,
        parser: argparse.ArgumentParser,
        namespace: argparse.Namespace,
        values: str | Sequence[Any] | None,
        option_string: str | None = "--list-enabled",
    ) -> None:
        self.run.linter.msgs_store.list_messages()
        sys.exit(0)


class _ListMessagesEnabledAction(_AccessRunObjectAction):
    """Display all enabled messages."""

    def __call__(
        self,
        parser: argparse.ArgumentParser,
        namespace: argparse.Namespace,
        values: str | Sequence[Any] | None,
        option_string: str | None = "--list-msgs-enabled",
    ) -> None:
        self.run.linter.list_messages_enabled()
        sys.exit(0)


class _ListCheckGroupsAction(_AccessRunObjectAction):
    """Display all the check groups that pylint knows about."""

    def __call__(
        self,
        parser: argparse.ArgumentParser,
        namespace: argparse.Namespace,
        values: str | Sequence[Any] | None,
        option_string: str | None = "--list-groups",
    ) -> None:
        for check in self.run.linter.get_checker_names():
            print(check)
        sys.exit(0)


class _ListConfidenceLevelsAction(_AccessRunObjectAction):
    """Display all the confidence levels that pylint knows about."""

    def __call__(
        self,
        parser: argparse.ArgumentParser,
        namespace: argparse.Namespace,
        values: str | Sequence[Any] | None,
        option_string: str | None = "--list-conf-levels",
    ) -> None:
        for level in interfaces.CONFIDENCE_LEVELS:
            print(f"%-18s: {level}")
        sys.exit(0)


class _ListExtensionsAction(_AccessRunObjectAction):
    """Display all extensions under pylint.extensions."""

    def __call__(
        self,
        parser: argparse.ArgumentParser,
        namespace: argparse.Namespace,
        values: str | Sequence[Any] | None,
        option_string: str | None = "--list-extensions",
    ) -> None:
        for filename in Path(extensions.__file__).parent.iterdir():
            if filename.suffix == ".py" and not filename.stem.startswith("_"):
                extension_name, _, _ = filename.stem.partition(".")
                print(f"pylint.extensions.{extension_name}")
        sys.exit(0)


class _FullDocumentationAction(_AccessRunObjectAction):
    """Display the full documentation."""

    def __call__(
        self,
        parser: argparse.ArgumentParser,
        namespace: argparse.Namespace,
        values: str | Sequence[Any] | None,
        option_string: str | None = "--full-documentation",
    ) -> None:
        utils.print_full_documentation(self.run.linter)
        sys.exit(0)


class _GenerateRCFileAction(_AccessRunObjectAction):
    """Generate a pylintrc file."""

    def __call__(
        self,
        parser: argparse.ArgumentParser,
        namespace: argparse.Namespace,
        values: str | Sequence[Any] | None,
        option_string: str | None = "--generate-rcfile",
    ) -> None:
        # TODO: 2.x: Deprecate this after the auto-upgrade functionality of
        # pylint-config is sufficient.
        with warnings.catch_warnings():
            warnings.filterwarnings("ignore", category=DeprecationWarning)
            self.run.linter.generate_config(skipsections=("Commands",))
        sys.exit(0)


class _GenerateConfigFileAction(_AccessRunObjectAction):
    """Generate a .toml format configuration file."""

    def __call__(
        self,
        parser: argparse.ArgumentParser,
        namespace: argparse.Namespace,
        values: str | Sequence[Any] | None,
        option_string: str | None = "--generate-toml-config",
    ) -> None:
        print(self.run.linter._generate_config_file())
        sys.exit(0)


class _ErrorsOnlyModeAction(_AccessRunObjectAction):
    """Turn on errors-only mode.

    Error mode:
        * disable all but error messages
        * disable the 'miscellaneous' checker which can be safely deactivated in
          debug
        * disable reports
        * do not save execution information
    """

    def __call__(
        self,
        parser: argparse.ArgumentParser,
        namespace: argparse.Namespace,
        values: str | Sequence[Any] | None,
        option_string: str | None = "--errors-only",
    ) -> None:
        self.run.linter._error_mode = True


class _LongHelpAction(_AccessRunObjectAction):
    """Display the long help message."""

    def __call__(
        self,
        parser: argparse.ArgumentParser,
        namespace: argparse.Namespace,
        values: str | Sequence[Any] | None,
        option_string: str | None = "--long-help",
    ) -> None:
        formatter: _HelpFormatter = self.run.linter._arg_parser._get_formatter()  # type: ignore[assignment]

        # Add extra info as epilog to the help message
        self.run.linter._arg_parser.epilog = formatter.get_long_description()
        print(self.run.linter.help())

        sys.exit(0)


class _AccessLinterObjectAction(_CallbackAction):
    """Action that has access to the Linter object."""

    def __init__(
        self,
        option_strings: Sequence[str],
        dest: str,
        nargs: None = None,
        const: None = None,
        default: None = None,
        type: None = None,
        choices: None = None,
        required: bool = False,
        help: str = "",
        metavar: str = "",
        **kwargs: PyLinter,
    ) -> None:
        self.linter = kwargs["linter"]

        super().__init__(
            option_strings,
            dest,
            1,
            const,
            default,
            type,
            choices,
            required,
            help,
            metavar,
        )

    @abc.abstractmethod
    def __call__(
        self,
        parser: argparse.ArgumentParser,
        namespace: argparse.Namespace,
        values: str | Sequence[Any] | None,
        option_string: str | None = None,
    ) -> None:
        raise NotImplementedError  # pragma: no cover


class _XableAction(_AccessLinterObjectAction):
    """Callback action for enabling or disabling a message."""

    def _call(
        self,
        xabling_function: Callable[[str], None],
        values: str | Sequence[Any] | None,
        option_string: str | None,
    ) -> None:
        assert isinstance(values, (tuple, list))
        for msgid in utils._check_csv(values[0]):
            try:
                xabling_function(msgid)
            except (
                exceptions.DeletedMessageError,
                exceptions.MessageBecameExtensionError,
            ) as e:
                self.linter._stashed_messages[
                    (self.linter.current_name, "useless-option-value")
                ].append((option_string, str(e)))
            except exceptions.UnknownMessageError:
                self.linter._stashed_messages[
                    (self.linter.current_name, "unknown-option-value")
                ].append((option_string, msgid))

    @abc.abstractmethod
    def __call__(
        self,
        parser: argparse.ArgumentParser,
        namespace: argparse.Namespace,
        values: str | Sequence[Any] | None,
        option_string: str | None = "--disable",
    ) -> None:
        raise NotImplementedError  # pragma: no cover


class _DisableAction(_XableAction):
    """Callback action for disabling a message."""

    def __call__(
        self,
        parser: argparse.ArgumentParser,
        namespace: argparse.Namespace,
        values: str | Sequence[Any] | None,
        option_string: str | None = "--disable",
    ) -> None:
        self._call(self.linter.disable, values, option_string)


class _EnableAction(_XableAction):
    """Callback action for enabling a message."""

    def __call__(
        self,
        parser: argparse.ArgumentParser,
        namespace: argparse.Namespace,
        values: str | Sequence[Any] | None,
        option_string: str | None = "--enable",
    ) -> None:
        self._call(self.linter.enable, values, option_string)


class _OutputFormatAction(_AccessLinterObjectAction):
    """Callback action for setting the output format."""

    def __call__(
        self,
        parser: argparse.ArgumentParser,
        namespace: argparse.Namespace,
        values: str | Sequence[Any] | None,
        option_string: str | None = "--enable",
    ) -> None:
        assert isinstance(values, (tuple, list))
        assert isinstance(
            values[0], str
        ), "'output-format' should be a comma separated string of reporters"
        self.linter._load_reporters(values[0])


class _AccessParserAction(_CallbackAction):
    """Action that has access to the ArgumentParser object."""

    def __init__(
        self,
        option_strings: Sequence[str],
        dest: str,
        nargs: None = None,
        const: None = None,
        default: None = None,
        type: None = None,
        choices: None = None,
        required: bool = False,
        help: str = "",
        metavar: str = "",
        **kwargs: argparse.ArgumentParser,
    ) -> None:
        self.parser = kwargs["parser"]

        super().__init__(
            option_strings,
            dest,
            0,
            const,
            default,
            type,
            choices,
            required,
            help,
            metavar,
        )

    @abc.abstractmethod
    def __call__(
        self,
        parser: argparse.ArgumentParser,
        namespace: argparse.Namespace,
        values: str | Sequence[Any] | None,
        option_string: str | None = None,
    ) -> None:
        raise NotImplementedError  # pragma: no cover
