#   Licensed under the Apache License, Version 2.0 (the "License"); you may
#   not use this file except in compliance with the License. You may obtain
#   a copy of the License at
#
#        http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#   WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#   License for the specific language governing permissions and limitations
#   under the License.
#

"""Authentication Library"""

import argparse
from typing import TYPE_CHECKING, TypedDict, TypeVar

from keystoneauth1.identity import base as identity_base
from keystoneauth1.identity.v3 import k2k
from keystoneauth1.loading import base

from osc_lib import exceptions as exc
from osc_lib.i18n import _
from osc_lib import utils

if TYPE_CHECKING:
    from openstack.config import cloud_region


# Initialize the list of Authentication plugins early in order
# to get the command-line options
PLUGIN_LIST: frozenset[str] | None = None


class _OptionDict(TypedDict):
    env: str
    help: str


# List of plugin command line options
OPTIONS_LIST: dict[str, _OptionDict] = {}


def get_plugin_list() -> frozenset[str]:
    """Gather plugin list and cache it"""

    global PLUGIN_LIST

    if PLUGIN_LIST is None:
        PLUGIN_LIST = base.get_available_plugin_names()
    return PLUGIN_LIST


def get_options_list() -> dict[str, _OptionDict]:
    """Gather plugin options so the help action has them available"""

    global OPTIONS_LIST

    if not OPTIONS_LIST:
        for plugin_name in get_plugin_list():
            plugin_options = base.get_plugin_options(plugin_name)
            for o in plugin_options:
                os_name = o.name.lower().replace('_', '-')
                os_env_name = 'OS_' + os_name.upper().replace('-', '_')
                OPTIONS_LIST.setdefault(
                    os_name,
                    {'env': os_env_name, 'help': ''},
                )
                # TODO(mhu) simplistic approach, would be better to only add
                # help texts if they vary from one auth plugin to another
                # also the text rendering is ugly in the CLI ...
                OPTIONS_LIST[os_name]['help'] += (
                    f'With {plugin_name}: {o.help}\n'
                )
    return OPTIONS_LIST


def check_valid_authorization_options(
    options: 'cloud_region.CloudRegion',
    auth_plugin_name: str,
) -> None:
    """Validate authorization options, and provide helpful error messages."""
    if (
        options.auth.get('project_id')
        and not options.auth.get('domain_id')
        and not options.auth.get('domain_name')
        and not options.auth.get('project_name')
        and not options.auth.get('tenant_id')
        and not options.auth.get('tenant_name')
    ):
        raise exc.CommandError(
            _(
                'Missing parameter(s): '
                'Set either a project or a domain scope, but not both. Set a '
                'project scope with --os-project-name, OS_PROJECT_NAME, or '
                'auth.project_name. Alternatively, set a domain scope with '
                '--os-domain-name, OS_DOMAIN_NAME or auth.domain_name.'
            )
        )


def check_valid_authentication_options(
    options: 'cloud_region.CloudRegion',
    auth_plugin_name: str,
) -> None:
    """Validate authentication options, and provide helpful error messages."""
    # Get all the options defined within the plugin.
    plugin_opts = {
        opt.dest: opt for opt in base.get_plugin_options(auth_plugin_name)
    }

    # NOTE(aloga): this is an horrible hack. We need a way to specify the
    # required options in the plugins. Using the "required" argument for
    # the oslo_config.cfg.Opt does not work, as it is not possible to load the
    # plugin if the option is not defined, so the error will simply be:
    # "NoMatchingPlugin: The plugin foobar could not be found"
    msgs = []

    # when no auth params are passed in, user advised to use os-cloud
    if not options.auth and auth_plugin_name != 'none':
        msgs.append(_('Set a cloud-name with --os-cloud or OS_CLOUD'))
    else:
        if 'password' in plugin_opts and not (
            options.auth.get('username') or options.auth.get('user_id')
        ):
            msgs.append(
                _(
                    'Set a username with --os-username, OS_USERNAME,'
                    ' or auth.username'
                    ' or set a user-id with --os-user-id, OS_USER_ID,'
                    ' or auth.user_id'
                )
            )
        if 'auth_url' in plugin_opts and not options.auth.get('auth_url'):
            msgs.append(
                _(
                    'Set an authentication URL, with --os-auth-url,'
                    ' OS_AUTH_URL or auth.auth_url'
                )
            )
        if 'url' in plugin_opts and not options.auth.get('url'):
            msgs.append(
                _('Set a service URL, with --os-url, OS_URL or auth.url')
            )
        if 'token' in plugin_opts and not options.auth.get('token'):
            msgs.append(
                _('Set a token with --os-token, OS_TOKEN or auth.token')
            )

    if msgs:
        raise exc.CommandError(
            _('Missing parameter(s): \n%s') % '\n'.join(msgs)
        )


_ArgumentParserT = TypeVar('_ArgumentParserT', bound=argparse.ArgumentParser)


def build_auth_plugins_option_parser(
    parser: _ArgumentParserT,
) -> _ArgumentParserT:
    """Auth plugins options builder

    Builds dynamically the list of options expected by each available
    authentication plugin.
    """
    available_plugins = list(get_plugin_list())
    parser.add_argument(
        '--os-auth-type',
        metavar='<auth-type>',
        dest='auth_type',
        default=utils.env('OS_AUTH_TYPE'),
        help=_(
            'Select an authentication type. Available types: %s.'
            ' Default: selected based on --os-username/--os-token'
            ' (Env: OS_AUTH_TYPE)'
        )
        % ', '.join(available_plugins),
        choices=available_plugins,
    )
    # Maintain compatibility with old tenant env vars
    envs = {
        'OS_PROJECT_NAME': utils.env(
            'OS_PROJECT_NAME', default=utils.env('OS_TENANT_NAME')
        ),
        'OS_PROJECT_ID': utils.env(
            'OS_PROJECT_ID', default=utils.env('OS_TENANT_ID')
        ),
    }
    for o in get_options_list():
        # Remove tenant options from KSC plugins and replace them below
        if 'tenant' not in o:
            parser.add_argument(
                '--os-' + o,
                metavar=f'<auth-{o}>',
                dest=o.replace('-', '_'),
                default=envs.get(
                    OPTIONS_LIST[o]['env'],
                    utils.env(OPTIONS_LIST[o]['env']),
                ),
                help=_('%(help)s\n(Env: %(env)s)')
                % {
                    'help': OPTIONS_LIST[o]['help'],
                    'env': OPTIONS_LIST[o]['env'],
                },
            )
    # add tenant-related options for compatibility
    # this is deprecated but still used in some tempest tests...
    parser.add_argument(
        '--os-tenant-name',
        metavar='<auth-tenant-name>',
        dest='os_project_name',
        help=argparse.SUPPRESS,
    )
    parser.add_argument(
        '--os-tenant-id',
        metavar='<auth-tenant-id>',
        dest='os_project_id',
        help=argparse.SUPPRESS,
    )
    return parser


def get_keystone2keystone_auth(
    local_auth: identity_base.BaseIdentityPlugin,
    service_provider: str,
    project_id: str | None = None,
    project_name: str | None = None,
    project_domain_id: str | None = None,
    project_domain_name: str | None = None,
) -> k2k.Keystone2Keystone:
    """Return Keystone 2 Keystone authentication for service provider.

    :param local_auth: authentication to use with the local Keystone
    :param service_provider: service provider id as registered in Keystone
    :param project_id: project id to scope to in the service provider
    :param project_name: project name to scope to in the service provider
    :param project_domain_id: id of domain in the service provider
    :param project_domain_name: name of domain to in the service provider
    :return: Keystone2Keystone auth object for service provider
    """
    return k2k.Keystone2Keystone(
        local_auth,
        service_provider,
        project_id=project_id,
        project_name=project_name,
        project_domain_id=project_domain_id,
        project_domain_name=project_domain_name,
    )
