# Copyright 2021 IBM Corp. All Rights Reserved.
#
# 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.

"""
Commands for HMC user role management.
"""

from __future__ import absolute_import
from __future__ import print_function

import re

import click

import zhmcclient
from .zhmccli import cli
from ._helper import print_properties, print_resources, abort_if_false, \
    options_to_properties, original_options, COMMAND_OPTIONS_METAVAR, \
    click_exception, add_options, LIST_OPTIONS, ObjectByUriCache


PERMISSION_OPTIONS = [
    click.option('--task', type=str, metavar='TASK', required=False,
                 help='Permission: Task permission to the task with the '
                 'specified name.'),
    click.option('--view-only', type=bool, required=False,
                 help='If --task was specified, indicates whether the task\'s '
                 'view-only version is subject of the permission. Only certain '
                 'tasks have a view-only version. Default: True.'),
    click.option('--class', type=str, metavar='CLASS', required=False,
                 help='Permission: Object permission to all objects of the '
                 'specified resource class (= value of "class" property).'),
    click.option('--group', type=str, metavar='GROUP', required=False,
                 help='Permission: Object permission to the group with the '
                 'specified name and optionally to its members.'),
    click.option('--include-members', type=bool, required=False,
                 help='If --group was specified, indicates whether the group '
                 'members are included in the permission. Default: False.'),
    click.option('--cpc', type=str, metavar='CPC', required=False,
                 help='Permission: Object permission to the CPC with the '
                 'specified name.'),
    click.option('--partition', type=str,
                 metavar='CPC PARTITION', nargs=2, required=False,
                 help='Permission: Object permission to the partition on the '
                 'CPC with the specified names.'),
    click.option('--lpar', type=str,
                 metavar='CPC LPAR', nargs=2, required=False,
                 help='Permission: Object permission to the LPAR on the CPC '
                 'with the specified names.'),
    click.option('--adapter', type=str,
                 metavar='CPC ADAPTER', nargs=2, required=False,
                 help='Permission: Object permission to the adapter on the CPC '
                 'with the specified names.'),
    click.option('--storage-group', type=str,
                 metavar='CPC STORAGEGROUP', nargs=2, required=False,
                 help='Permission: Object permission to the storage group '
                 'associated with the CPC with the specified names.'),
    click.option('--storage-group-template', type=str,
                 metavar='CPC STORAGEGROUPTEMPLATE', nargs=2, required=False,
                 help='Permission: Object permission to the storage group '
                 'template associated with the CPC with the specified names.'),
]


def find_user_role(cmd_ctx, console, user_role_name):
    """
    Find a user role by name and return its resource object.
    """
    try:
        user_role = console.user_roles.find(name=user_role_name)
    except zhmcclient.Error as exc:
        raise click_exception(exc, cmd_ctx.error_format)
    return user_role


def permission_str(obj_cache, permission_info):
    """
    Return a permission-info item as a string for displaying
    """
    obj = permission_info['permitted-object']
    obj_type = permission_info['permitted-object-type']
    if obj_type == 'object-class':
        perm_str = 'class {}'.format(obj)
    else:
        assert obj_type == 'object'
        if obj.startswith('/api/console/tasks/'):
            task = obj_cache.task_by_uri(obj)
            perm_str = "task '{}'".format(task.name)
        elif obj.startswith('/api/groups/'):
            # TODO: Display name, once zhmcclient.Group implemented
            # group = obj_cache.group_by_uri(obj)
            # perm_str = "group '{}'".format(group.name)
            perm_str = "group '{}'".format(obj)
        elif re.match(r'^/api/cpcs/[^/]+/adapters/[^/]+$', obj):
            adapter = obj_cache.adapter_by_uri(obj)
            cpc = adapter.manager.parent
            perm_str = "adapter '{}' on cpc '{}'". \
                format(adapter.name, cpc.name)
        elif re.match(r'^/api/cpcs/[^/]+/lpars/[^/]+$', obj):
            lpar = obj_cache.lpar_by_uri(obj)
            cpc = lpar.manager.parent
            perm_str = "lpar '{}' on cpc '{}'". \
                format(lpar.name, cpc.name)
        elif re.match(r'^/api/cpcs/[^/]+/partitions/[^/]+$', obj):
            partition = obj_cache.partition_by_uri(obj)
            cpc = partition.manager.parent
            perm_str = "partition '{}' on cpc '{}'". \
                format(partition.name, cpc.name)
        elif re.match(r'^/api/cpcs/[^/]+$', obj):
            cpc = obj_cache.cpc_by_uri(obj)
            perm_str = "cpc '{}'".format(cpc.name)
        elif re.match(r'^/api/storage-groups/[^/]+$', obj):
            storage_group = obj_cache.storage_group_by_uri(obj)
            cpc = storage_group.cpc
            perm_str = "storage group '{}' associated with cpc '{}'". \
                format(storage_group.name, cpc.name)
        elif re.match(r'^/api/storage-templates/[^/]+$', obj):
            # TODO: Display name, once zhmcclient list templates implemented
            # perm_str = "group '{}'".format(obj)
            # storage_template = obj_cache.storage_template_by_uri(obj)
            # cpc = storage_template.cpc
            # perm_str = "storage group template '{}' assoc. with cpc '{}'". \
            #     format(storage_template.name, cpc.name)
            perm_str = "storage group template '{}'".format(obj)
        else:
            perm_str = 'object {}'.format(obj)
    return perm_str


def permission_options_to_kwargs(cmd_ctx, client, options):
    """
    Convert the permission options to a kwargs dict of arguments for the
    respective zhmcclient methods.
    """
    opts = dict(options)
    kwargs = {}
    kwargs['include_members'] = options.pop('include_members')
    kwargs['view_only'] = options.pop('view_only')
    num_options = \
        bool(opts['task']) + \
        bool(opts['class']) + \
        bool(opts['group']) + \
        bool(opts['cpc']) + \
        bool(opts['partition']) + \
        bool(opts['adapter']) + \
        bool(opts['storage_group']) + \
        bool(opts['storage_group_template'])

    if num_options > 1:
        raise click.ClickException(
            "More than one permission option specified: {}".
            format(', '.join(opts.keys())))

    if opts['task']:
        raise NotImplementedError  # pylint: disable=no-else-raise
        # TODO: Implement once zhmccli task is implemented
        # # pylint: disable=import-outside-toplevel,cyclic-import
        # from ._cmd_task import find_task
        # task_name = opts['task']
        # task = find_task(cmd_ctx, client, task_name)
        # kwargs['permitted_object'] = task
    if opts['class']:
        class_name = opts['class']
        kwargs['permitted_object'] = class_name
    elif opts['group']:
        raise NotImplementedError  # pylint: disable=no-else-raise
        # TODO: Implement once zhmcclient.Group is implemented
        # # pylint: disable=import-outside-toplevel,cyclic-import
        # from ._cmd_group import find_group
        # group_name = opts['group']
        # group = find_group(cmd_ctx, client, group_name)
        # kwargs['permitted_object'] = group
    elif opts['cpc']:
        # pylint: disable=import-outside-toplevel,cyclic-import
        from ._cmd_cpc import find_cpc
        cpc_name = opts['cpc']
        cpc = find_cpc(cmd_ctx, client, cpc_name)
        kwargs['permitted_object'] = cpc
    elif opts['partition']:
        # pylint: disable=import-outside-toplevel,cyclic-import
        from ._cmd_partition import find_partition
        cpc_name, partition_name = opts['partition']
        partition = find_partition(cmd_ctx, client, cpc_name, partition_name)
        kwargs['permitted_object'] = partition
    elif opts['adapter']:
        # pylint: disable=import-outside-toplevel,cyclic-import
        from ._cmd_adapter import find_adapter
        cpc_name, adapter_name = opts['adapter']
        adapter = find_adapter(cmd_ctx, client, cpc_name, adapter_name)
        kwargs['permitted_object'] = adapter
    elif opts['storage_group']:
        # pylint: disable=import-outside-toplevel,cyclic-import
        from ._cmd_storagegroup import find_storagegroup
        cpc_name, stogrp_name = opts['storage_group']
        # TODO: Fix find_storagegroup() to take cpc_name
        stogrp = find_storagegroup(cmd_ctx, client, stogrp_name)
        kwargs['permitted_object'] = stogrp
    elif opts['storage_group_template']:
        raise NotImplementedError  # pylint: disable=no-else-raise
        # TODO: Implement once zhmccli storage-group-template is implemented
        # # pylint: disable=import-outside-toplevel,cyclic-import
        # from ._cmd_storagegrouptemplate import find_storagegrouptemplate
        # cpc_name, stogrptpl_name = opts['storage_group_template']
        # stogrptpl = find_storagegrouptemplate(
        #     cmd_ctx, client, cpc_name, stogrptpl_name)
        # kwargs['permitted_object'] = stogrptpl
    else:
        raise click.ClickException("No permission option specified.")

    return kwargs


@cli.group('userrole', options_metavar=COMMAND_OPTIONS_METAVAR)
def user_role_group():
    """
    Command group for managing HMC user roles.

    In addition to the command-specific options shown in this help text, the
    general options (see 'zhmc --help') can also be specified right after the
    'zhmc' command name.
    """


@user_role_group.command('list', options_metavar=COMMAND_OPTIONS_METAVAR)
@add_options(LIST_OPTIONS)
@click.option('--permissions', is_flag=True, required=False,
              help='Show additional properties for user role permissions.')
@click.pass_obj
def user_role_list(cmd_ctx, **options):
    """
    List the HMC user roles.

    In addition to the command-specific options shown in this help text, the
    general options (see 'zhmc --help') can also be specified right after the
    'zhmc' command name.
    """
    cmd_ctx.execute_cmd(lambda: cmd_user_role_list(cmd_ctx, options))


@user_role_group.command('show', options_metavar=COMMAND_OPTIONS_METAVAR)
@click.argument('USER_ROLE', type=str, metavar='USER_ROLE')
@click.pass_obj
def user_role_show(cmd_ctx, user_role):
    """
    Show the details of an HMC user role.

    The following properties are shown in addition to those returned by the HMC:

    \b
      - 'parent-name' - Name of the parent Console.
      - 'associated-system-defined-user-role-name' - Name of the User Role
        referenced by 'associated-system-defined-user-role-uri', if present.

    In addition to the command-specific options shown in this help text, the
    general options (see 'zhmc --help') can also be specified right after the
    'zhmc' command name.
    """
    cmd_ctx.execute_cmd(lambda: cmd_user_role_show(cmd_ctx, user_role))


@user_role_group.command('create', options_metavar=COMMAND_OPTIONS_METAVAR)
@click.option('--name', type=str, required=True,
              help='The name of the new user role.')
@click.option('--description', type=str, required=False,
              help='The description of the new user role.')
@click.option('--associated-system-defined-user-role', type=str, required=False,
              help='The system-defined user role associated with the new '
              'user role. '
              'Default: Operator Tasks')
@click.option('--is-inheritance-enabled', type=bool, required=False,
              help='The indicator whether user role inheritance is enabled for '
              'the new user role. '
              'When enabled, if the user role permits access to a parent '
              'managed object, then all managed objects that are hosted by the '
              'parent managed object are also permitted by this role. '
              'Default: False')
@click.pass_obj
def user_role_create(cmd_ctx, **options):
    """
    Create a user-defined HMC user role.

    In addition to the command-specific options shown in this help text, the
    general options (see 'zhmc --help') can also be specified right after the
    'zhmc' command name.
    """
    cmd_ctx.execute_cmd(lambda: cmd_user_role_create(cmd_ctx, options))


@user_role_group.command('update', options_metavar=COMMAND_OPTIONS_METAVAR)
@click.argument('USER_ROLE', type=str, metavar='USER_ROLE')
@click.option('--description', type=str, required=False,
              help='The new description of the user role.')
@click.option('--associated-system-defined-user-role', type=str, required=False,
              help='The new system-defined user role associated with the '
              'user role.')
@click.option('--is-inheritance-enabled', type=bool, required=False,
              help='The new indicator whether user role inheritance is '
              'enabled for the user role. '
              'When enabled, if the user role permits access to a parent '
              'managed object, then all managed objects that are hosted by the '
              'parent managed object are also permitted by this role.')
@click.pass_obj
def user_role_update(cmd_ctx, user_role, **options):
    """
    Update the properties of an HMC user role.

    Note that HMC user roles cannot be renamed.

    Only the properties will be changed for which a corresponding option is
    specified, so the default for all options is not to change properties.

    In addition to the command-specific options shown in this help text, the
    general options (see 'zhmc --help') can also be specified right after the
    'zhmc' command name.
    """
    cmd_ctx.execute_cmd(lambda: cmd_user_role_update(
        cmd_ctx, user_role, options))


@user_role_group.command('delete', options_metavar=COMMAND_OPTIONS_METAVAR)
@click.argument('USER_ROLE', type=str, metavar='USER_ROLE')
@click.option('-y', '--yes', is_flag=True, callback=abort_if_false,
              expose_value=False,
              help='Skip prompt to confirm deletion of the user role.',
              prompt='Are you sure you want to delete this user role ?')
@click.pass_obj
def user_role_delete(cmd_ctx, user_role):
    """
    Delete a user-defined HMC user role.

    In addition to the command-specific options shown in this help text, the
    general options (see 'zhmc --help') can also be specified right after the
    'zhmc' command name.
    """
    cmd_ctx.execute_cmd(lambda: cmd_user_role_delete(cmd_ctx, user_role))


@user_role_group.command('add-permission',
                         options_metavar=COMMAND_OPTIONS_METAVAR)
@click.argument('USER_ROLE', type=str, metavar='USER_ROLE')
@add_options(PERMISSION_OPTIONS)
@click.pass_obj
def user_role_add_permission(cmd_ctx, user_role, **options):
    """
    Add a permission to a user-defined HMC user role.

    The permission is specified by exactly one of the permission options,
    and optionally the --view-only or --include-members options.

    In addition to the command-specific options shown in this help text, the
    general options (see 'zhmc --help') can also be specified right after the
    'zhmc' command name.
    """
    cmd_ctx.execute_cmd(lambda: cmd_user_role_add_permission(
        cmd_ctx, user_role, options))


@user_role_group.command('remove-permission',
                         options_metavar=COMMAND_OPTIONS_METAVAR)
@click.argument('USER_ROLE', type=str, metavar='USER_ROLE')
@add_options(PERMISSION_OPTIONS)
@click.pass_obj
def user_role_remove_permission(cmd_ctx, user_role, **options):
    """
    Remove a permission from a user-defined HMC user role.

    The permission is specified by exactly one of the permission options,
    and optionally the --view-only or --include-members options.

    In addition to the command-specific options shown in this help text, the
    general options (see 'zhmc --help') can also be specified right after the
    'zhmc' command name.
    """
    cmd_ctx.execute_cmd(lambda: cmd_user_role_remove_permission(
        cmd_ctx, user_role, options))


def cmd_user_role_list(cmd_ctx, options):
    # pylint: disable=missing-function-docstring

    client = zhmcclient.Client(cmd_ctx.session)
    console = client.consoles.console

    show_list = [
        'name',
    ]
    if not options['names_only']:
        show_list.extend([
            'type',
            'description',
        ])
    if options['uri']:
        show_list.extend([
            'object-uri',
        ])
    if options['permissions']:
        show_list.extend([
            'permissions',
            'is-inheritance-enabled',
        ])

    additions = {}
    additions['permissions'] = {}

    try:
        user_roles = console.user_roles.list(full_properties=False)
    except zhmcclient.Error as exc:
        raise click_exception(exc, cmd_ctx.error_format)

    if options['permissions']:
        obj_cache = ObjectByUriCache(cmd_ctx, client)
        for role in user_roles:
            permissions = []
            for item in role.get_property('permissions'):
                permissions.append(permission_str(obj_cache, item))
            additions['permissions'][role.uri] = permissions

    try:
        print_resources(cmd_ctx, user_roles, cmd_ctx.output_format, show_list,
                        additions, all=options['all'])
    except zhmcclient.Error as exc:
        raise click_exception(exc, cmd_ctx.error_format)


def cmd_user_role_show(cmd_ctx, user_role_name):
    # pylint: disable=missing-function-docstring

    client = zhmcclient.Client(cmd_ctx.session)
    console = client.consoles.console
    user_role = find_user_role(cmd_ctx, console, user_role_name)

    try:
        user_role.pull_full_properties()
    except zhmcclient.Error as exc:
        raise click_exception(exc, cmd_ctx.error_format)

    properties = dict(user_role.properties)

    # Add artificial property 'parent-name'
    properties['parent-name'] = console.name

    # Improve representation of property 'permissions'
    obj_cache = ObjectByUriCache(cmd_ctx, client)
    permissions = []
    for item in user_role.properties['permissions']:
        permissions.append(permission_str(obj_cache, item))
    properties['permissions'] = permissions

    # Add artificial property 'associated-system-defined-user-role-name'
    urole_uri = user_role.get_property(
        'associated-system-defined-user-role-uri')
    if urole_uri:
        # User-defined roles have an associated system-defined role
        urole_props = client.session.get(urole_uri)
        urole_name = urole_props['name']
        properties['associated-system-defined-user-role-name'] = urole_name
    else:
        # System-defined roles do not have an associated system-defined role
        properties['associated-system-defined-user-role-name'] = None

    print_properties(cmd_ctx, properties, cmd_ctx.output_format)


def cmd_user_role_create(cmd_ctx, options):
    # pylint: disable=missing-function-docstring

    client = zhmcclient.Client(cmd_ctx.session)
    console = client.consoles.console

    org_options = original_options(options)

    name_map = {
        'associated-system-defined-user-role': None,
    }

    properties = options_to_properties(org_options, name_map)

    if org_options['associated-system-defined-user-role'] == '':
        properties['associated-system-defined-user-role-uri'] = None
    else:
        try:
            role = console.user_roles.find_by_name(
                org_options['associated-system-defined-user-role'])
        except zhmcclient.Error as exc:
            raise click_exception(exc, cmd_ctx.error_format)
        properties['associated-system-defined-user-role-uri'] = role.uri

    try:
        new_user_role = console.user_roles.create(properties)
    except zhmcclient.Error as exc:
        raise click_exception(exc, cmd_ctx.error_format)

    cmd_ctx.spinner.stop()
    click.echo("New user role '{u}' has been created.".
               format(u=new_user_role.properties['name']))


def cmd_user_role_update(cmd_ctx, user_role_name, options):
    # pylint: disable=missing-function-docstring

    client = zhmcclient.Client(cmd_ctx.session)
    console = client.consoles.console
    user_role = find_user_role(cmd_ctx, console, user_role_name)

    org_options = original_options(options)

    name_map = {
        'associated-system-defined-user-role': None,
    }

    properties = options_to_properties(org_options, name_map)

    if org_options['associated-system-defined-user-role'] == '':
        properties['associated-system-defined-user-role-uri'] = None
    else:
        try:
            role = console.user_roles.find_by_name(
                org_options['associated-system-defined-user-role'])
        except zhmcclient.Error as exc:
            raise click_exception(exc, cmd_ctx.error_format)
        properties['associated-system-defined-user-role-uri'] = role.uri

    if not properties:
        cmd_ctx.spinner.stop()
        click.echo("No properties specified for updating user role '{u}'.".
                   format(u=user_role_name))
        return

    try:
        user_role.update_properties(properties)
    except zhmcclient.Error as exc:
        raise click_exception(exc, cmd_ctx.error_format)

    cmd_ctx.spinner.stop()
    click.echo("User role '{u}' has been updated.".format(u=user_role_name))


def cmd_user_role_delete(cmd_ctx, user_role_name):
    # pylint: disable=missing-function-docstring

    client = zhmcclient.Client(cmd_ctx.session)
    console = client.consoles.console
    user_role = find_user_role(cmd_ctx, console, user_role_name)

    try:
        user_role.delete()
    except zhmcclient.Error as exc:
        raise click_exception(exc, cmd_ctx.error_format)

    cmd_ctx.spinner.stop()
    click.echo("User role '{u}' has been deleted.".format(u=user_role_name))


def cmd_user_role_add_permission(cmd_ctx, user_role_name, options):
    # pylint: disable=missing-function-docstring

    client = zhmcclient.Client(cmd_ctx.session)
    console = client.consoles.console
    user_role = find_user_role(cmd_ctx, console, user_role_name)

    kwargs = permission_options_to_kwargs(cmd_ctx, client, options)

    try:
        user_role.add_permission(**kwargs)
    except zhmcclient.Error as exc:
        raise click_exception(exc, cmd_ctx.error_format)

    cmd_ctx.spinner.stop()
    click.echo("The permission has been added to user role '{r}'.".
               format(r=user_role_name))


def cmd_user_role_remove_permission(cmd_ctx, user_role_name, options):
    # pylint: disable=missing-function-docstring

    client = zhmcclient.Client(cmd_ctx.session)
    console = client.consoles.console
    user_role = find_user_role(cmd_ctx, console, user_role_name)

    kwargs = permission_options_to_kwargs(cmd_ctx, client, options)

    try:
        user_role.remove_permission(**kwargs)
    except zhmcclient.Error as exc:
        raise click_exception(exc, cmd_ctx.error_format)

    cmd_ctx.spinner.stop()
    click.echo("The permission has been removed from user role '{r}'.".
               format(r=user_role_name))
