import argparse
import sys
from typing import Callable
from typing import List
from typing import Optional

from . import audit
from . import baseline
from . import filters
from . import plugins
from . import scan
from ...settings import get_settings
from .common import initialize_plugin_settings
from detect_secrets.__version__ import VERSION


class ParserBuilder:
    def __init__(self) -> None:
        self._parser = argparse.ArgumentParser()
        self._post_processors: List[Callable[[argparse.Namespace], None]] = [
            initialize_plugin_settings,
        ]

        self.add_default_options()

    def add_default_options(self) -> 'ParserBuilder':
        self._parser.add_argument(
            '-v',
            '--verbose',
            action='count',
            help='Verbose mode.',
        )
        self._parser.add_argument(
            '--version',
            action='version',
            version=VERSION,
            help='Display version information.',
        )
        self._parser.add_argument(
            '-C',
            metavar='<path>',
            dest='custom_root',
            nargs=1,
            default=[''],
            help=(
                'Run as if detect-secrets was started in <path>, rather than in the current '
                'working directory.'
            ),
        )
        self._parser.add_argument(
            '-c',
            '--cores',
            dest='num_cores',
            nargs=1,
            type=int,
            default=[None],
            help=(
                'Specify the number of cores to use for parallel processing. Defaults to '
                'using the max cores on the current host.'
            ),
        )
        return self

    def add_console_use_arguments(self) -> 'ParserBuilder':
        subparser = self._parser.add_subparsers(dest='action')
        self._post_processors = [_assert_action_is_specified, *self._post_processors]

        parser = scan.add_scan_action(subparser)

        # NOTE: This ordering is important.
        #   1. Baselines will be handled accordingly, and the global settings object will
        #      be initialized with a certain state.
        #   2. Scan options can override this (e.g. --force-use-all-plugins)
        #   3. Plugin options can override this again (e.g. disabling plugins, or different configs)
        #
        # In a similar way, the filter options must come after the settings object is initialized.
        self._post_processors.append(scan.parse_args)
        self.add_plugin_options(parser, action_filter='scan')
        self.add_filter_options(parser, action_filter='scan')

        # NOTE: scan allows a baseline, but we need to override the first post_processor
        self._post_processors[1] = baseline.parse_args

        audit.add_audit_action(subparser)
        self._post_processors.append(audit.parse_args)
        return self

    def add_pre_commit_arguments(self) -> 'ParserBuilder':
        self._parser.add_argument(
            'filenames',
            nargs='*',
            help='Filenames to check.',
        )
        self._parser.add_argument(
            '--json',
            action='store_true',
            help='Print detect-secrets-hook output as JSON',
        )
        self.add_baseline_options(
            help=(
                'Explicitly ignore secrets through a baseline generated by `detect-secrets scan`'
            ),
        )
        self.add_plugin_options()
        self.add_filter_options()

        return self

    def add_baseline_options(self, help: str = '') -> 'ParserBuilder':
        baseline.add_baseline_option(self._parser, help=help)
        for index, processor in enumerate(self._post_processors):
            if processor == initialize_plugin_settings:
                self._post_processors[index] = baseline.parse_args
                break

        return self

    def add_plugin_options(
        self,
        parser: Optional[argparse.ArgumentParser] = None,
        action_filter: Optional[str] = None,
    ) -> 'ParserBuilder':
        if not parser:
            parser = self._parser

        plugins.add_plugin_options(parser)

        if action_filter:
            self._post_processors.append(
                _action_specific_post_processor(action_filter, plugins.parse_args),
            )
        else:
            self._post_processors.append(plugins.parse_args)

        return self

    def add_filter_options(
        self,
        parser: Optional[argparse.ArgumentParser] = None,
        action_filter: Optional[str] = None,
    ) -> 'ParserBuilder':
        if not parser:
            parser = self._parser

        filters.add_filter_options(parser)

        if action_filter:
            self._post_processors.append(
                _action_specific_post_processor(action_filter, filters.parse_args),
            )
        else:
            self._post_processors.append(filters.parse_args)

        return self

    def parse_args(self, argv: Optional[List[str]] = None) -> argparse.Namespace:
        args = self._parser.parse_args(argv)
        try:
            for processor in self._post_processors:
                processor(args)
        except argparse.ArgumentTypeError:
            # TODO: Better help text?
            self._parser.print_usage(sys.stderr)
            sys.exit(1)

        args.custom_root = args.custom_root[0]
        if args.custom_root:
            # This filter assumes current working directory, which will fail if we're running
            # from a different directory.
            # TODO: Maybe adjust this so that it is directory agnostic?
            get_settings().disable_filters('detect_secrets.filters.common.is_invalid_file')

            # Abide by the Principle of Least Surprise, and have the default value be the
            # custom root directory itself.
            if args.path == ['.']:
                args.path = [args.custom_root]

        args.num_cores = args.num_cores[0]

        return args


def _assert_action_is_specified(args: argparse.Namespace) -> None:
    if not args.action:
        raise argparse.ArgumentTypeError('Unspecified action.')


def _action_specific_post_processor(action: str, processor: Callable) -> Callable:
    def wrapped(args: argparse.Namespace) -> None:
        if args.action != action:
            return

        processor(args)

    return wrapped
