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

import time

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, FILTER_OPTIONS, \
    build_filter_args, SORT_OPTIONS, build_sort_props
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_name, partition_name, nic_name):
    """
    Find a NIC by name and return its resource object.
    """
    partition = find_partition(cmd_ctx, client, cpc_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)
@add_options(FILTER_OPTIONS)
@add_options(SORT_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.
      - '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, metavar='NAME|INDEX', required=False,
              help='The name or index of the network port backing the new NIC. '
              'Required.')
@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.

    For all types of network adapters and all CPC/SE generations, the backing
    adapter and port can be specified with the --adapter and --port 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_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.')
@click.option('--port', type=str, metavar='NAME|INDEX', required=False,
              help='The name or index of the network port backing the NIC.')
@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.

    If the backing adapter and port needs to be updated, it can be specified
    with the --adapter and --port options, for all types of network adapters
    and all CPC/SE generations.

    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 and --port 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"
    or "Update NIC Properties" operation.

    If none of the --adapter and --port options are specified, None is
    returned.

    If only one of these options is specified, or if they are required but
    not specified, an exception is raised.

    Returns:
      dict with the backing URI property set, if --adapter and --port options
      are specified. None, if these options are not required and not specified.

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

    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 not org_options['adapter'] and not org_options['port']:
        if required:
            raise click_exception(
                "The --adapter and --port options are required but were not "
                "specified.", cmd_ctx.error_format)
        return None

    adapter_name = org_options['adapter']
    port_name = org_options['port']
    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:
        # Try interpreting the --port value as a port name
        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)

    try:
        nes_feature = cpc.api_feature_enabled('network-express-support')
    except zhmcclient.Error as exc:
        raise click_exception(exc, cmd_ctx.error_format)

    adapter_family = adapter.get_property('adapter-family')
    if adapter_family in ('osa', 'hipersockets') and not nes_feature:
        # a vswitch-based NIC (OSA, HS up to z16)
        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}

    # an adapter-based NIC (RoCE, CNA up to z16 or all adapter types
    # since z17)
    return {'network-adapter-port-uri': port.uri}


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)

    filter_args = build_filter_args(cmd_ctx, options['filter'])
    try:
        nics = partition.nics.list(filter_args=filter_args)
    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',
            'description',
        ])
    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,
    }

    sort_props = build_sort_props(cmd_ctx, options['sort'], default=['name'])
    try:
        print_resources(cmd_ctx, nics, cmd_ctx.output_format, show_list,
                        additions, all=options['all'], sort_props=sort_props)
    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
    # up to z16)
    try:
        vswitch_uri = nic.get_property('virtual-switch-uri')
    except KeyError:
        pass
    else:
        vswitch_props = client.session.get(vswitch_uri)
        adapter_uri = vswitch_props['backing-adapter-uri']
        cpc = find_cpc(cmd_ctx, client, cpc_name)
        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
    # up to z16 or all adapter types since z17)
    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 nic_mitigate_name(cmd_ctx, nic, desired_name):
    """
    Mitigate the naming issue by explicitly naming the NIC, if needed.
    """
    cmd_ctx.spinner.stop()
    click.echo("Mitigating HMC defect: Updating name of NIC '{n}'.".
               format(n=desired_name))

    attempts = 3  # number of attempts, including initial one
    sleep_time = 0.5  # seconds, between retries
    while True:
        nic.update_properties({'name': desired_name})
        time.sleep(sleep_time)
        nic.pull_properties('name')
        if nic.name == desired_name:
            break
        # The name is not as desired yet, retry
        attempts -= 1
        if attempts > 0:
            click.echo("Mitigating HMC defect: Retrying after {s} s, because "
                       "NIC name is still different.".
                       format(s=sleep_time))
            continue
        raise click_exception(
            "HMC defect mitigation for updating the NIC name did not work. "
            "NIC name is: {n}".format(n=nic.name),
            cmd_ctx.error_format)


def nic_mitigate_uuid(cmd_ctx, prior_nic_uris, partition, desired_name):
    """
    Mitigate the UUID issue by finding the created NIC.
    """
    cmd_ctx.spinner.stop()
    click.echo("Mitigating HMC defect: Ignoring HTTP 500,0 "
               "'Invalid UUID string:' when creating NIC '{n}'.".
               format(n=desired_name))

    attempts = 3  # number of attempts, including initial one
    sleep_time = 0.5  # seconds, between retries
    while True:
        partition.pull_properties("nic-uris")
        new_nics = [nic for nic in partition.nics.list()
                    if nic.uri not in prior_nic_uris]
        if len(new_nics) == 1:
            break
        if len(new_nics) > 1:
            new_nic_names = [nic.name for nic in new_nics]
            raise click_exception(
                "HMC defect mitigation for 500,0 did not work, because there "
                "is more than one new NIC: {n}".format(n=new_nic_names),
                cmd_ctx.error_format)
        # We got no new NICs yet, retry
        attempts -= 1
        if attempts > 0:
            click.echo("Mitigating HMC defect: Retrying after {s} s, because "
                       "no new NICs were returned.".format(s=sleep_time))
            time.sleep(sleep_time)
            continue
        raise click_exception(
            "HMC defect mitigation for 500,0 did not work, because no new "
            "NICs were returned after retrying.", cmd_ctx.error_format)

    new_nic = new_nics[0]
    return new_nic


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)

    # Flags to control whether HMC defects are mitigated
    cpc = partition.manager.parent
    nes_feature = cpc.api_feature_enabled('network-express-support')
    mitigate_uuid = nes_feature  # HTTP 500,0 "Invalid UUID string:"
    mitigate_name = nes_feature  # NIC name is not set

    name_map = {
        # The following options are handled in this function:
        'adapter': None,
        'port': 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))

    if mitigate_uuid:
        prior_nics = partition.nics.list()
        prior_nic_uris = [nic.uri for nic in prior_nics]

    try:
        new_nic = partition.nics.create(properties)
    except zhmcclient.HTTPError as exc:
        if mitigate_uuid and exc.http_status == 500 and exc.reason == 0 and \
                "Invalid UUID string:" in exc.stack:
            # pylint: disable=possibly-used-before-assignment
            new_nic = nic_mitigate_uuid(
                cmd_ctx, prior_nic_uris, partition, options['name'])
        else:
            raise click_exception(exc, cmd_ctx.error_format)
    except zhmcclient.Error as exc:
        raise click_exception(exc, cmd_ctx.error_format)

    if mitigate_name:
        new_nic.pull_properties('name')
        if new_nic.name != options['name']:
            nic_mitigate_name(cmd_ctx, new_nic, options['name'])

    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)

    # Flags to control whether HMC defects are mitigated
    partition = nic.manager.parent
    cpc = partition.manager.parent
    nes_feature = cpc.api_feature_enabled('network-express-support')
    mitigate_name = nes_feature  # NIC name is not set

    name_map = {
        # The following options are handled in this function:
        'adapter': None,
        'port': 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)

    if mitigate_name and options['name']:
        nic.pull_properties('name')
        if nic.name != options['name']:
            nic_mitigate_name(cmd_ctx, nic, options['name'])

    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(f"NIC '{nic_name}' has been updated.")


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(f"NIC '{nic_name}' has been deleted.")


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
