# Copyright 2016-2019 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 NICs.
"""

from __future__ import absolute_import

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
from ._cmd_partition import find_partition
from ._cmd_cpc import find_cpc


# Defaults for NIC creation
SSC_IP_ADDRESS_TYPES = ['ipv4', 'ipv6', 'linklocal', 'dhcp']
VLAN_TYPES = ['enforced', 'none']


def find_nic(cmd_ctx, client, cpc_or_name, partition_name, nic_name):
    """
    Find a NIC by name and return its resource object.
    """
    partition = find_partition(cmd_ctx, client, cpc_or_name, partition_name)
    try:
        nic = partition.nics.find(name=nic_name)
    except zhmcclient.Error as exc:
        raise click_exception(exc, cmd_ctx.error_format)
    return nic


@cli.group('nic', options_metavar=COMMAND_OPTIONS_METAVAR)
def nic_group():
    """
    Command group for managing NICs (DPM mode only).

    The commands in this group work only on CPCs that are in DPM mode.

    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.
    """


@nic_group.command('list', options_metavar=COMMAND_OPTIONS_METAVAR)
@click.argument('CPC', type=str, metavar='CPC')
@click.argument('PARTITION', type=str, metavar='PARTITION')
@click.option('--type', is_flag=True, required=False, hidden=True)
@add_options(LIST_OPTIONS)
@click.pass_obj
def nic_list(cmd_ctx, cpc, partition, **options):
    """
    List the NICs in a partition.

    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_nic_list(cmd_ctx, cpc, partition, options))


@nic_group.command('show', options_metavar=COMMAND_OPTIONS_METAVAR)
@click.argument('CPC', type=str, metavar='CPC')
@click.argument('PARTITION', type=str, metavar='PARTITION')
@click.argument('NIC', type=str, metavar='NIC')
@click.pass_obj
def nic_show(cmd_ctx, cpc, partition, nic):
    """
    Show the details of a NIC.

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

    \b
      - 'parent-name' - Name of the parent Partition.
      - 'virtual-switch-name' - Name of the Virtual Switch for the backing
        OSA/Hipersocket Adapter
      - 'network-adapter-name' - Name of the backing Adapter
      - 'network-adapter-port-name' - Name of the backing Adapter Port
      - 'network-adapter-port-index' - Index of the backing Adapter Port

    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_nic_show(cmd_ctx, cpc, partition, nic))


@nic_group.command('create', options_metavar=COMMAND_OPTIONS_METAVAR)
@click.argument('CPC', type=str, metavar='CPC')
@click.argument('PARTITION', type=str, metavar='PARTITION')
@click.option('--name', type=str, required=True,
              help='The name of the new NIC.')
@click.option('--description', type=str, required=False,
              help='The description of the new NIC. '
              'Default: empty')
@click.option('--adapter', type=str, required=False,
              help='The name of the network adapter with the port backing the '
              'new NIC. Required.')
@click.option('--port', type=str, required=False,
              help='The name or index of the network port backing the new NIC. '
              'Required.')
@click.option('--virtual-switch', type=str, required=False,
              help='Deprecated: The name of the virtual switch of the network '
              'port backing the new NIC. Use --adapter and --port instead.')
@click.option('--device-number', type=str, required=False,
              help='The device number to be used for the new NIC. '
              'Default: auto-generated')
@click.option('--ssc-management-nic', type=bool, required=False,
              help='Indicates that this NIC should be used as a management '
              'NIC for Secure Service Container to access the web interface. '
              'Only applicable to NICs of SSC partitions. '
              'Default: False')
@click.option('--ssc-ip-address-type', type=click.Choice(SSC_IP_ADDRESS_TYPES),
              required=False,
              help='Secure Service Container IP address type. '
              'Only applicable to and required for NICs of '
              'SSC partitions.')
@click.option('--ssc-ip-address', type=str, required=False,
              help='IP Address of the SSC management web interface. '
              'Only applicable to and required for NICs of SSC '
              'partitions when ssc-ip-address-type is ipv4 or ipv6.')
@click.option('--ssc-mask-prefix', type=str, required=False,
              help='Network Mask of the SSC management NIC. '
              'Only applicable to and required for NICs of SSC '
              'partitions when ssc-ip-address-type is ipv4 or ipv6.')
@click.option('--vlan-id', metavar='[INT|none]', type=str, required=False,
              help='VLAN ID of the NIC, or "none" for setting no VLAN ID. '
              'On z14 or later CPCs, specifying a VLAN ID requires using the '
              '--vlan-type option. '
              'Only applicable to management NICs of SSC partitions, and to '
              'OSA and Hipersocket NICs of non-SSC partitions. '
              'Default: No VLAN ID')
@click.option('--vlan-type', required=False, type=click.Choice(VLAN_TYPES),
              help='VLAN type of the NIC, or "none" for setting no VLAN type. '
              'Only supported on z14 or later CPCs, and required if specifying '
              'a VLAN ID with the --vlan-id option. '
              'Default: No VLAN type')
@click.pass_obj
def nic_create(cmd_ctx, cpc, partition, **options):
    """
    Create a NIC in a partition.

    The NIC is backed by a port (jack) on an adapter. For all types of network
    adapters, the backing adapter and port can be specified with the --adapter
    and --port options. The --virtual-switch option is deprecated but still
    supported for compatibility; it can be used only for OSA and HiperSocket
    adapters.

    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_nic_create(cmd_ctx, cpc, partition,
                                               options))


@nic_group.command('update', options_metavar=COMMAND_OPTIONS_METAVAR)
@click.argument('CPC', type=str, metavar='CPC')
@click.argument('PARTITION', type=str, metavar='PARTITION')
@click.argument('NIC', type=str, metavar='NIC')
@click.option('--name', type=str, required=False,
              help='The new name of the NIC.')
@click.option('--description', type=str, required=False,
              help='The new description of the NIC.')
@click.option('--adapter', type=str, required=False,
              help='The name of the network adapter with the port backing the '
              'NIC. Required.')
@click.option('--port', type=str, required=False,
              help='The name or index of the network port backing the NIC. '
              'Required.')
@click.option('--virtual-switch', type=str, required=False,
              help='Deprecated: The name of the virtual switch of the network '
              'port backing the NIC. Use --adapter and --port instead.')
@click.option('--device-number', type=str, required=False,
              help='The new device number to be used for the NIC.')
@click.option('--ssc-management-nic', type=bool, required=False,
              help='Indicates that this NIC should be used as a management '
              'NIC for Secure Service Container to access the web interface. '
              'Only applicable to NICs of SSC partitions. ')
@click.option('--ssc-ip-address-type', type=click.Choice(SSC_IP_ADDRESS_TYPES),
              required=False,
              help='Secure Service Container IP address type. '
              'Only applicable to NICs of SSC partitions.')
@click.option('--ssc-ip-address', type=str, required=False,
              help='IP Address of the SSC management web interface. '
              'Only applicable to NICs of SSC partitions.')
@click.option('--ssc-mask-prefix', type=str, required=False,
              help='Network Mask of the SSC management NIC. '
              'Only applicable to NICs of SSC partitions.')
@click.option('--vlan-id', metavar='[INT|none]', type=str, required=False,
              help='VLAN ID of the NIC, or "none" for setting no VLAN ID. '
              'On z14 or later CPCs, specifying a VLAN ID requires using the '
              '--vlan-type option. '
              'Only applicable to management NICs of SSC partitions, and to '
              'OSA and Hipersocket NICs of non-SSC partitions.')
@click.option('--vlan-type', required=False, type=click.Choice(VLAN_TYPES),
              help='VLAN type of the NIC, or "none" for setting no VLAN type. '
              'Only supported on z14 or later CPCs, and required if specifying '
              'a VLAN ID with the --vlan-id option.')
@click.pass_obj
def nic_update(cmd_ctx, cpc, partition, nic, **options):
    """
    Update the properties of a NIC.

    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_nic_update(cmd_ctx, cpc, partition, nic,
                                               options))


@nic_group.command('delete', options_metavar=COMMAND_OPTIONS_METAVAR)
@click.argument('CPC', type=str, metavar='CPC')
@click.argument('PARTITION', type=str, metavar='PARTITION')
@click.argument('NIC', type=str, metavar='NIC')
@click.option('-y', '--yes', is_flag=True, callback=abort_if_false,
              expose_value=False,
              help='Skip prompt to confirm deletion of the NIC.',
              prompt='Are you sure you want to delete this NIC ?')
@click.pass_obj
def nic_delete(cmd_ctx, cpc, partition, nic):
    """
    Delete a NIC.

    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_nic_delete(cmd_ctx, cpc, partition, nic))


def backing_uri(cmd_ctx, cpc, org_options, required=False):
    """
    Determine the backing adapter port or vswitch to be used from the
    --adapter, --port, and --virtual-switch options, and return a dict with the
    correct property for the URI of the backing port or vswitch to be used for
    the "Create NIC" operation.

    Returns:
      dict with the backing URI property set, if backing object specified
      None, if no backing object specified and not required

    Raises:
      click exception for various error situations.
    """

    if org_options['virtual-switch']:
        # This option is deprecated, but it is still supported for
        # backwards compatibility.

        if org_options['adapter'] or org_options['port']:
            raise click_exception(
                "The (deprecated) --virtual-switch option must not be "
                "specified together with any of the --adapter or --port "
                "options.",
                cmd_ctx.error_format)

        vswitch_name = org_options['virtual-switch']
        try:
            vswitch = cpc.virtual_switches.find(name=vswitch_name)
        except zhmcclient.NotFound:
            raise click_exception(
                "Could not find virtual switch '{s}' in CPC '{c}'.".
                format(s=vswitch_name, c=cpc.name),
                cmd_ctx.error_format)
        return {'virtual-switch-uri': vswitch.uri}

    if bool(org_options['adapter']) != bool(org_options['port']):
        raise click_exception(
            "The --adapter and --port options must be specified both or none.",
            cmd_ctx.error_format)

    if org_options['adapter']:
        adapter_name = org_options['adapter']
    else:
        if required:
            raise click_exception(
                "Required --adapter option is not specified",
                cmd_ctx.error_format)
        return None

    if org_options['port']:
        port_name = org_options['port']
    else:
        if required:
            raise click_exception(
                "Required --port option is not specified",
                cmd_ctx.error_format)
        return None

    try:
        adapter = cpc.adapters.find(name=adapter_name)
    except zhmcclient.NotFound:
        raise click_exception(
            "Could not find adapter '{a}' in CPC '{c}'.".
            format(a=adapter_name, c=cpc.name),
            cmd_ctx.error_format)

    try:
        port = adapter.ports.find(name=port_name)
    except zhmcclient.NotFound:
        # Try interpreting the --port value as a port index
        try:
            port_index = int(port_name)
        except ValueError:
            raise click_exception(
                "Could not find port with name '{p}' on "
                "adapter '{a}' in CPC '{c}'.".
                format(p=port_name, a=adapter_name, c=cpc.name),
                cmd_ctx.error_format)
        try:
            port = adapter.ports.find(index=port_index)
        except zhmcclient.NotFound:
            raise click_exception(
                "Could not find port with name or index '{p}' on "
                "adapter '{a}' in CPC '{c}'.".
                format(p=port_name, a=adapter_name, c=cpc.name),
                cmd_ctx.error_format)

    adapter_type = adapter.get_property('type')
    if adapter_type in ('roce', 'cna'):
        return {'network-adapter-port-uri': port.uri}

    if adapter_type in ('osd', 'hipersockets'):
        port_index = port.get_property('index')
        filter_args = {
            'backing-adapter-uri': adapter.uri,
            'port': port_index,
        }
        try:
            vswitch = cpc.virtual_switches.find(**filter_args)
        except zhmcclient.NotFound:
            raise click_exception(
                "Could not find virtual switch with backing adapter '{a}' "
                "and port index '{p}' in CPC '{c}'.".
                format(a=adapter.name, p=port_index, c=cpc.name),
                cmd_ctx.error_format)
        return {'virtual-switch-uri': vswitch.uri}

    raise click_exception(
        "Adapter '{a}' on CPC '{c}' has unsupported type {t} for "
        "being a backing adapter of a NIC.".
        format(a=adapter_name, c=cpc.name, t=adapter_type),
        cmd_ctx.error_format)


def cmd_nic_list(cmd_ctx, cpc_name, partition_name, options):
    # pylint: disable=missing-function-docstring

    client = zhmcclient.Client(cmd_ctx.session)
    partition = find_partition(cmd_ctx, client, cpc_name, partition_name)

    try:
        nics = partition.nics.list()
    except zhmcclient.Error as exc:
        raise click_exception(exc, cmd_ctx.error_format)

    if options['type']:
        click.echo("The --type option is deprecated and type information "
                   "is now always shown.")

    show_list = [
        'name',
        'cpc',
        'partition',
    ]
    if not options['names_only']:
        show_list.extend([
            'type',
        ])
    if options['uri']:
        show_list.extend([
            'element-uri',
        ])

    cpc_additions = {}
    partition_additions = {}
    for nic in nics:
        cpc_additions[nic.uri] = cpc_name
        partition_additions[nic.uri] = partition_name
    additions = {
        'cpc': cpc_additions,
        'partition': partition_additions,
    }

    try:
        print_resources(cmd_ctx, nics, 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_nic_show(cmd_ctx, cpc_name, partition_name, nic_name):
    # pylint: disable=missing-function-docstring

    client = zhmcclient.Client(cmd_ctx.session)
    nic = find_nic(cmd_ctx, client, cpc_name, partition_name, nic_name)

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

    properties = dict(nic.properties)

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

    # Add artificial properties in case of a vswitch-based NIC (OSA, HS)
    try:
        vswitch_uri = nic.get_property('virtual-switch-uri')
    except KeyError:
        pass
    else:
        vswitch_props = client.session.get(vswitch_uri)
        properties['virtual-switch-name'] = vswitch_props['name']

        cpc = find_cpc(cmd_ctx, client, cpc_name)
        adapter_uri = vswitch_props['backing-adapter-uri']
        adapter = cpc.adapters.resource_object(adapter_uri)
        properties['network-adapter-name'] = adapter.name

        port_index = vswitch_props['port']
        properties['network-adapter-port-index'] = port_index

        port = adapter.ports.find(index=port_index)
        properties['network-adapter-port-name'] = port.name

    # Add artificial properties in case of an adapter-based NIC (RoCE, CNA)
    try:
        port_uri = nic.get_property('network-adapter-port-uri')
    except KeyError:
        pass
    else:
        port_props = client.session.get(port_uri)
        properties['network-adapter-port-name'] = port_props['name']
        properties['network-adapter-port-index'] = port_props['index']

        adapter_props = client.session.get(port_props['parent'])
        properties['network-adapter-name'] = adapter_props['name']

    print_properties(cmd_ctx, properties, cmd_ctx.output_format)


def cmd_nic_create(cmd_ctx, cpc_name, partition_name, options):
    # pylint: disable=missing-function-docstring

    client = zhmcclient.Client(cmd_ctx.session)
    partition = find_partition(cmd_ctx, client, cpc_name, partition_name)

    name_map = {
        # The following options are handled in this function:
        'adapter': None,
        'port': None,
        'virtual-switch': None,
    }
    org_options = original_options(options)
    properties = options_to_properties(org_options, name_map)

    set_vlan_id_type(cmd_ctx, properties, org_options)

    properties.update(backing_uri(
        cmd_ctx, partition.manager.cpc, org_options, required=True))

    try:
        new_nic = partition.nics.create(properties)
    except zhmcclient.Error as exc:
        raise click_exception(exc, cmd_ctx.error_format)

    cmd_ctx.spinner.stop()
    click.echo("New NIC '{n}' has been created.".
               format(n=new_nic.properties['name']))


def cmd_nic_update(cmd_ctx, cpc_name, partition_name, nic_name, options):
    # pylint: disable=missing-function-docstring

    client = zhmcclient.Client(cmd_ctx.session)
    nic = find_nic(cmd_ctx, client, cpc_name, partition_name, nic_name)

    name_map = {
        # The following options are handled in this function:
        'adapter': None,
        'port': None,
        'virtual-switch': None,
    }
    org_options = original_options(options)
    properties = options_to_properties(org_options, name_map)

    set_vlan_id_type(cmd_ctx, properties, org_options)

    uri_prop = backing_uri(
        cmd_ctx, nic.manager.partition.manager.cpc, org_options)
    if uri_prop:
        properties.update(uri_prop)

    if not properties:
        cmd_ctx.spinner.stop()
        click.echo("No properties specified for updating NIC '{n}'.".
                   format(n=nic_name))
        return

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

    cmd_ctx.spinner.stop()
    if 'name' in properties and properties['name'] != nic_name:
        click.echo("NIC '{n}' has been renamed to '{nn}' and was updated.".
                   format(n=nic_name, nn=properties['name']))
    else:
        click.echo("NIC '{n}' has been updated.".format(n=nic_name))


def cmd_nic_delete(cmd_ctx, cpc_name, partition_name, nic_name):
    # pylint: disable=missing-function-docstring

    client = zhmcclient.Client(cmd_ctx.session)
    nic = find_nic(cmd_ctx, client, cpc_name, partition_name, nic_name)

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

    cmd_ctx.spinner.stop()
    click.echo("NIC '{n}' has been deleted.".format(n=nic_name))


def set_vlan_id_type(cmd_ctx, properties, org_options):
    """
    Set the 'vlan-id' and 'vlan-type' properties from the options.
    """
    vlan_id = org_options['vlan-id']
    if vlan_id is None:
        pass  # No change on update, use HMC default on create
    elif vlan_id == 'none':
        # Reset to no VLAN ID (important for update)
        properties['vlan-id'] = None
    else:
        try:
            properties['vlan-id'] = int(vlan_id)
        except ValueError:
            raise click_exception(
                "Invalid value for '--vlan-id': {} is not a valid integer".
                format(vlan_id), cmd_ctx.error_format)

    vlan_type = org_options['vlan-type']
    if vlan_type is None:
        pass  # No change on update, use HMC default on create
    elif vlan_type == 'none':
        # Reset to no VLAN type (important for update)
        properties['vlan-type'] = None
    else:
        properties['vlan-type'] = vlan_type
