"""Command line parsing for test environments."""
from __future__ import annotations

import argparse
import enum
import functools
import typing as t

from ..constants import (
    CONTROLLER_PYTHON_VERSIONS,
    REMOTE_PROVIDERS,
    SECCOMP_CHOICES,
    SUPPORTED_PYTHON_VERSIONS,
)

from ..util import (
    REMOTE_ARCHITECTURES,
)

from ..completion import (
    docker_completion,
    network_completion,
    remote_completion,
    windows_completion,
    filter_completion,
)

from ..cli.argparsing import (
    CompositeAction,
    CompositeActionCompletionFinder,
)

from ..cli.argparsing.actions import (
    EnumAction,
)

from ..cli.actions import (
    DelegatedControllerAction,
    NetworkSshTargetAction,
    NetworkTargetAction,
    OriginControllerAction,
    PosixSshTargetAction,
    PosixTargetAction,
    SanityPythonTargetAction,
    UnitsPythonTargetAction,
    WindowsSshTargetAction,
    WindowsTargetAction,
)

from ..cli.compat import (
    TargetMode,
)

from ..config import (
    TerminateMode,
)

from .completers import (
    complete_choices,
    register_completer,
)

from .converters import (
    key_value_type,
)

from .epilog import (
    get_epilog,
)

from ..ci import (
    get_ci_provider,
)


class ControllerMode(enum.Enum):
    """Type of provisioning to use for the controller."""
    NO_DELEGATION = enum.auto()
    ORIGIN = enum.auto()
    DELEGATED = enum.auto()


def add_environments(
        parser,  # type: argparse.ArgumentParser
        completer,  # type: CompositeActionCompletionFinder
        controller_mode,  # type: ControllerMode
        target_mode,  # type: TargetMode
):  # type: (...) -> None
    """Add arguments for the environments used to run ansible-test and commands it invokes."""
    no_environment = controller_mode == ControllerMode.NO_DELEGATION and target_mode == TargetMode.NO_TARGETS

    parser.set_defaults(no_environment=no_environment)

    if no_environment:
        return

    parser.set_defaults(target_mode=target_mode)

    add_global_options(parser, controller_mode)
    add_legacy_environment_options(parser, controller_mode, target_mode)
    action_types = add_composite_environment_options(parser, completer, controller_mode, target_mode)

    sections = [f'{heading}\n{content}'
                for action_type, documentation_state in CompositeAction.documentation_state.items() if action_type in action_types
                for heading, content in documentation_state.sections.items()]

    if not get_ci_provider().supports_core_ci_auth():
        sections.append('Remote provisioning options have been hidden since no Ansible Core CI API key was found.')

    sections.append(get_epilog(completer))

    parser.formatter_class = argparse.RawDescriptionHelpFormatter
    parser.epilog = '\n\n'.join(sections)


def add_global_options(
        parser,  # type: argparse.ArgumentParser
        controller_mode,  # type: ControllerMode
):
    """Add global options for controlling the test environment that work with both the legacy and composite options."""
    global_parser = t.cast(argparse.ArgumentParser, parser.add_argument_group(title='global environment arguments'))

    global_parser.add_argument(
        '--containers',
        metavar='JSON',
        help=argparse.SUPPRESS,
    )

    global_parser.add_argument(
        '--pypi-proxy',
        action='store_true',
        help=argparse.SUPPRESS,
    )

    global_parser.add_argument(
        '--pypi-endpoint',
        metavar='URI',
        help=argparse.SUPPRESS,
    )

    global_parser.add_argument(
        '--requirements',
        action='store_true',
        default=False,
        help='install command requirements',
    )

    global_parser.add_argument(
        '--no-pip-check',
        action='store_true',
        help=argparse.SUPPRESS,  # deprecated, kept for now (with a warning) for backwards compatibility
    )

    add_global_remote(global_parser, controller_mode)
    add_global_docker(global_parser, controller_mode)


def add_composite_environment_options(
        parser,  # type: argparse.ArgumentParser
        completer,  # type: CompositeActionCompletionFinder
        controller_mode,  # type: ControllerMode
        target_mode,  # type: TargetMode
):  # type: (...) -> t.List[t.Type[CompositeAction]]
    """Add composite options for controlling the test environment."""
    composite_parser = t.cast(argparse.ArgumentParser, parser.add_argument_group(
        title='composite environment arguments (mutually exclusive with "environment arguments" above)'))

    composite_parser.add_argument(
        '--host-path',
        help=argparse.SUPPRESS,
    )

    action_types = []  # type: t.List[t.Type[CompositeAction]]

    def register_action_type(action_type):  # type: (t.Type[CompositeAction]) -> t.Type[CompositeAction]
        """Register the provided composite action type and return it."""
        action_types.append(action_type)
        return action_type

    if controller_mode == ControllerMode.NO_DELEGATION:
        composite_parser.set_defaults(controller=None)
    else:
        register_completer(composite_parser.add_argument(
            '--controller',
            metavar='OPT',
            action=register_action_type(DelegatedControllerAction if controller_mode == ControllerMode.DELEGATED else OriginControllerAction),
            help='configuration for the controller',
        ), completer.completer)

    if target_mode == TargetMode.NO_TARGETS:
        composite_parser.set_defaults(targets=[])
    elif target_mode == TargetMode.SHELL:
        group = composite_parser.add_mutually_exclusive_group()

        register_completer(group.add_argument(
            '--target-posix',
            metavar='OPT',
            action=register_action_type(PosixSshTargetAction),
            help='configuration for the target',
        ), completer.completer)

        suppress = None if get_ci_provider().supports_core_ci_auth() else argparse.SUPPRESS

        register_completer(group.add_argument(
            '--target-windows',
            metavar='OPT',
            action=WindowsSshTargetAction if suppress else register_action_type(WindowsSshTargetAction),
            help=suppress or 'configuration for the target',
        ), completer.completer)

        register_completer(group.add_argument(
            '--target-network',
            metavar='OPT',
            action=NetworkSshTargetAction if suppress else register_action_type(NetworkSshTargetAction),
            help=suppress or 'configuration for the target',
        ), completer.completer)
    else:
        if target_mode.multiple_pythons:
            target_option = '--target-python'
            target_help = 'configuration for the target python interpreter(s)'
        elif target_mode == TargetMode.POSIX_INTEGRATION:
            target_option = '--target'
            target_help = 'configuration for the target'
        else:
            target_option = '--target'
            target_help = 'configuration for the target(s)'

        target_actions = {
            TargetMode.POSIX_INTEGRATION: PosixTargetAction,
            TargetMode.WINDOWS_INTEGRATION: WindowsTargetAction,
            TargetMode.NETWORK_INTEGRATION: NetworkTargetAction,
            TargetMode.SANITY: SanityPythonTargetAction,
            TargetMode.UNITS: UnitsPythonTargetAction,
        }

        target_action = target_actions[target_mode]

        register_completer(composite_parser.add_argument(
            target_option,
            metavar='OPT',
            action=register_action_type(target_action),
            help=target_help,
        ), completer.completer)

    return action_types


def add_legacy_environment_options(
        parser,  # type: argparse.ArgumentParser
        controller_mode,  # type: ControllerMode
        target_mode,  # type: TargetMode
):
    """Add legacy options for controlling the test environment."""
    environment: argparse.ArgumentParser = parser.add_argument_group(  # type: ignore[assignment]  # real type private
        title='environment arguments (mutually exclusive with "composite environment arguments" below)')

    add_environments_python(environment, target_mode)
    add_environments_host(environment, controller_mode, target_mode)


def add_environments_python(
        environments_parser,  # type: argparse.ArgumentParser
        target_mode,  # type: TargetMode
):  # type: (...) -> None
    """Add environment arguments to control the Python version(s) used."""
    python_versions: t.Tuple[str, ...]

    if target_mode.has_python:
        python_versions = SUPPORTED_PYTHON_VERSIONS
    else:
        python_versions = CONTROLLER_PYTHON_VERSIONS

    environments_parser.add_argument(
        '--python',
        metavar='X.Y',
        choices=python_versions + ('default',),
        help='python version: %s' % ', '.join(python_versions),
    )

    environments_parser.add_argument(
        '--python-interpreter',
        metavar='PATH',
        help='path to the python interpreter',
    )


def add_environments_host(
        environments_parser,  # type: argparse.ArgumentParser
        controller_mode,  # type: ControllerMode
        target_mode  # type: TargetMode
):  # type: (...) -> None
    """Add environment arguments for the given host and argument modes."""
    environments_exclusive_group: argparse.ArgumentParser = environments_parser.add_mutually_exclusive_group()  # type: ignore[assignment]  # real type private

    add_environment_local(environments_exclusive_group)
    add_environment_venv(environments_exclusive_group, environments_parser)

    if controller_mode == ControllerMode.DELEGATED:
        add_environment_remote(environments_exclusive_group, environments_parser, target_mode)
        add_environment_docker(environments_exclusive_group, environments_parser, target_mode)

    if target_mode == TargetMode.WINDOWS_INTEGRATION:
        add_environment_windows(environments_parser)

    if target_mode == TargetMode.NETWORK_INTEGRATION:
        add_environment_network(environments_parser)


def add_environment_network(
    environments_parser,  # type: argparse.ArgumentParser
):  # type: (...) -> None
    """Add environment arguments for running on a windows host."""
    register_completer(environments_parser.add_argument(
        '--platform',
        metavar='PLATFORM',
        action='append',
        help='network platform/version',
    ), complete_network_platform)

    register_completer(environments_parser.add_argument(
        '--platform-collection',
        type=key_value_type,
        metavar='PLATFORM=COLLECTION',
        action='append',
        help='collection used to test platform',
    ), complete_network_platform_collection)

    register_completer(environments_parser.add_argument(
        '--platform-connection',
        type=key_value_type,
        metavar='PLATFORM=CONNECTION',
        action='append',
        help='connection used to test platform',
    ), complete_network_platform_connection)

    environments_parser.add_argument(
        '--inventory',
        metavar='PATH',
        help='path to inventory used for tests',
    )


def add_environment_windows(
        environments_parser,  # type: argparse.ArgumentParser
):  # type: (...) -> None
    """Add environment arguments for running on a windows host."""
    register_completer(environments_parser.add_argument(
        '--windows',
        metavar='VERSION',
        action='append',
        help='windows version',
    ), complete_windows)

    environments_parser.add_argument(
        '--inventory',
        metavar='PATH',
        help='path to inventory used for tests',
    )


def add_environment_local(
        exclusive_parser,  # type: argparse.ArgumentParser
):  # type: (...) -> None
    """Add environment arguments for running on the local (origin) host."""
    exclusive_parser.add_argument(
        '--local',
        action='store_true',
        help='run from the local environment',
    )


def add_environment_venv(
        exclusive_parser,  # type: argparse.ArgumentParser
        environments_parser,  # type: argparse.ArgumentParser
):  # type: (...) -> None
    """Add environment arguments for running in ansible-test managed virtual environments."""
    exclusive_parser.add_argument(
        '--venv',
        action='store_true',
        help='run from a virtual environment',
    )

    environments_parser.add_argument(
        '--venv-system-site-packages',
        action='store_true',
        help='enable system site packages')


def add_global_docker(
        parser,  # type: argparse.ArgumentParser
        controller_mode,  # type: ControllerMode
):  # type: (...) -> None
    """Add global options for Docker."""
    if controller_mode != ControllerMode.DELEGATED:
        parser.set_defaults(
            docker_no_pull=False,
            docker_network=None,
            docker_terminate=None,
            prime_containers=False,
        )

        return

    parser.add_argument(
        '--docker-no-pull',
        action='store_true',
        help=argparse.SUPPRESS,  # deprecated, kept for now (with a warning) for backwards compatibility
    )

    parser.add_argument(
        '--docker-network',
        metavar='NET',
        help='run using the specified network',
    )

    parser.add_argument(
        '--docker-terminate',
        metavar='T',
        default=TerminateMode.ALWAYS,
        type=TerminateMode,
        action=EnumAction,
        help='terminate the container: %(choices)s (default: %(default)s)',
    )

    parser.add_argument(
        '--prime-containers',
        action='store_true',
        help='download containers without running tests',
    )


def add_environment_docker(
        exclusive_parser,  # type: argparse.ArgumentParser
        environments_parser,  # type: argparse.ArgumentParser
        target_mode,  # type: TargetMode
):  # type: (...) -> None
    """Add environment arguments for running in docker containers."""
    if target_mode in (TargetMode.POSIX_INTEGRATION, TargetMode.SHELL):
        docker_images = sorted(filter_completion(docker_completion()))
    else:
        docker_images = sorted(filter_completion(docker_completion(), controller_only=True))

    register_completer(exclusive_parser.add_argument(
        '--docker',
        metavar='IMAGE',
        nargs='?',
        const='default',
        help='run from a docker container',
    ), functools.partial(complete_choices, docker_images))

    environments_parser.add_argument(
        '--docker-privileged',
        action='store_true',
        help='run docker container in privileged mode',
    )

    environments_parser.add_argument(
        '--docker-seccomp',
        metavar='SC',
        choices=SECCOMP_CHOICES,
        help='set seccomp confinement for the test container: %(choices)s',
    )

    environments_parser.add_argument(
        '--docker-memory',
        metavar='INT',
        type=int,
        help='memory limit for docker in bytes',
    )


def add_global_remote(
        parser,  # type: argparse.ArgumentParser
        controller_mode,  # type: ControllerMode
):  # type: (...) -> None
    """Add global options for remote instances."""
    if controller_mode != ControllerMode.DELEGATED:
        parser.set_defaults(
            remote_stage=None,
            remote_endpoint=None,
            remote_terminate=None,
        )

        return

    suppress = None if get_ci_provider().supports_core_ci_auth() else argparse.SUPPRESS

    register_completer(parser.add_argument(
        '--remote-stage',
        metavar='STAGE',
        default='prod',
        help=suppress or 'remote stage to use: prod, dev',
    ), complete_remote_stage)

    parser.add_argument(
        '--remote-endpoint',
        metavar='EP',
        help=suppress or 'remote provisioning endpoint to use',
    )

    parser.add_argument(
        '--remote-terminate',
        metavar='T',
        default=TerminateMode.NEVER,
        type=TerminateMode,
        action=EnumAction,
        help=suppress or 'terminate the remote instance: %(choices)s (default: %(default)s)',
    )


def add_environment_remote(
        exclusive_parser,  # type: argparse.ArgumentParser
        environments_parser,  # type: argparse.ArgumentParser
        target_mode,  # type: TargetMode
):  # type: (...) -> None
    """Add environment arguments for running in ansible-core-ci provisioned remote virtual machines."""
    if target_mode == TargetMode.POSIX_INTEGRATION:
        remote_platforms = get_remote_platform_choices()
    elif target_mode == TargetMode.SHELL:
        remote_platforms = sorted(set(get_remote_platform_choices()) | set(get_windows_platform_choices()))
    else:
        remote_platforms = get_remote_platform_choices(True)

    suppress = None if get_ci_provider().supports_core_ci_auth() else argparse.SUPPRESS

    register_completer(exclusive_parser.add_argument(
        '--remote',
        metavar='NAME',
        help=suppress or 'run from a remote instance',
    ), functools.partial(complete_choices, remote_platforms))

    environments_parser.add_argument(
        '--remote-provider',
        metavar='PR',
        choices=REMOTE_PROVIDERS,
        help=suppress or 'remote provider to use: %(choices)s',
    )

    environments_parser.add_argument(
        '--remote-arch',
        metavar='ARCH',
        choices=REMOTE_ARCHITECTURES,
        help=suppress or 'remote arch to use: %(choices)s',
    )


def complete_remote_stage(prefix: str, **_) -> t.List[str]:
    """Return a list of supported stages matching the given prefix."""
    return [stage for stage in ('prod', 'dev') if stage.startswith(prefix)]


def complete_windows(prefix: str, parsed_args: argparse.Namespace, **_) -> t.List[str]:
    """Return a list of supported Windows versions matching the given prefix, excluding versions already parsed from the command line."""
    return [i for i in get_windows_version_choices() if i.startswith(prefix) and (not parsed_args.windows or i not in parsed_args.windows)]


def complete_network_platform(prefix: str, parsed_args: argparse.Namespace, **_) -> t.List[str]:
    """Return a list of supported network platforms matching the given prefix, excluding platforms already parsed from the command line."""
    images = sorted(filter_completion(network_completion()))

    return [i for i in images if i.startswith(prefix) and (not parsed_args.platform or i not in parsed_args.platform)]


def complete_network_platform_collection(prefix: str, parsed_args: argparse.Namespace, **_) -> t.List[str]:
    """Return a list of supported network platforms matching the given prefix, excluding collection platforms already parsed from the command line."""
    left = prefix.split('=')[0]
    images = sorted(set(image.platform for image in filter_completion(network_completion()).values()))

    return [i + '=' for i in images if i.startswith(left) and (not parsed_args.platform_collection or i not in [x[0] for x in parsed_args.platform_collection])]


def complete_network_platform_connection(prefix: str, parsed_args: argparse.Namespace, **_) -> t.List[str]:
    """Return a list of supported network platforms matching the given prefix, excluding connection platforms already parsed from the command line."""
    left = prefix.split('=')[0]
    images = sorted(set(image.platform for image in filter_completion(network_completion()).values()))

    return [i + '=' for i in images if i.startswith(left) and (not parsed_args.platform_connection or i not in [x[0] for x in parsed_args.platform_connection])]


def get_remote_platform_choices(controller=False):  # type: (bool) -> t.List[str]
    """Return a list of supported remote platforms matching the given prefix."""
    return sorted(filter_completion(remote_completion(), controller_only=controller))


def get_windows_platform_choices():  # type: () -> t.List[str]
    """Return a list of supported Windows versions matching the given prefix."""
    return sorted(f'windows/{windows.version}' for windows in filter_completion(windows_completion()).values())


def get_windows_version_choices():  # type: () -> t.List[str]
    """Return a list of supported Windows versions."""
    return sorted(windows.version for windows in filter_completion(windows_completion()).values())
