#   Copyright 2017 GoDaddy
#   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.

import functools
import ipaddress

from cliff import columns
from openstackclient.identity import common as identity_common
from osc_lib import exceptions as osc_exc
from osc_lib import utils
from oslo_utils import uuidutils

from octaviaclient.api import exceptions
from octaviaclient.osc.v2 import constants


def _map_attrs(args, source_attr_map):
    res = {}
    for k, v in args.items():
        if (v is None) or (k not in source_attr_map):
            continue
        source_val = source_attr_map[k]
        # Attributes with 2 values map directly to a callable
        if len(source_val) == 2:
            res[source_val[0]] = source_val[1](v)
        # Attributes with 3 values map directly to a resource
        elif len(source_val) == 3:
            if not isinstance(v, list):
                res[source_val[0]] = get_resource_id(
                    source_val[2],
                    source_val[1],
                    v,
                )
            else:
                res[source_val[0]] = [get_resource_id(
                    source_val[2],
                    source_val[1],
                    x,
                ) for x in v]

        # Attributes with 4 values map to a resource with a parent
        elif len(source_val) == 4:
            parent = source_attr_map[source_val[2]]
            parent_id = get_resource_id(
                parent[2],
                parent[1],
                args[source_val[2]],
            )
            child = source_val
            res[child[0]] = get_resource_id(
                child[3],
                child[1],
                {child[0]: str(v), parent[0]: str(parent_id)},
            )
    return res


def _find_resource(list_funct, resource_name, root_tag, name, parent=None):
    """Search for a resource by name and ID.

    This function will search for a resource by both the name and ID,
    returning the resource once it finds a match. If no match is found,
    an exception will be raised.

    :param list_funct: The resource list method to call during searches.
    :param resource_name: The name of the resource type we are searching for.
    :param root_tag: The root tag of the resource returned from the API.
    :param name: The value we are searching for, a resource name or ID.
    :param parent: The parent resource ID, when required.
    :return: The resource found for the name or ID.
    :raises osc_exc.CommandError: If more than one match or none are found.
    """
    if parent:
        parent_args = [parent]
    else:
        parent_args = []

    resource = None
    # Optimize the API call order if we got a UUID-like name or not
    if uuidutils.is_uuid_like(name):
        # Try by ID first
        resource = get_resource_by_id(list_funct, name, resource_name,
                                      root_tag, *parent_args)

    # If the find by UUID didn't work then try by name
    if not resource:
        resource = get_resource_by_name(list_funct, name, resource_name,
                                        root_tag, *parent_args)

    # We didn't find what we were looking for, raise a consistent error.
    if not resource:
        msg = f"Unable to locate {name} in {resource_name}"
        raise osc_exc.CommandError(msg)

    return resource


def get_resource_by_id(list_func, id, resource_name, root_tag, *args):
    # Availability Zones don't support the id parameter
    if resource_name == "availability_zones":
        return None
    return get_resource_from_list_func(list_func, resource_name, root_tag,
                                       *args, id=id)


def get_resource_by_name(list_func, name, resource_name, root_tag, *args):
    return get_resource_from_list_func(list_func, resource_name, root_tag,
                                       *args, name=name)


def get_resource_from_list_func(list_func, resource_name, root_tag, *args,
                                **kwargs):
    # For some network resources we need to use the SDK which doesn't
    # return a dict (and doesn't require the root_tag)
    if resource_name in ['subnets', 'ports',
                         'networks', 'policies',
                         'security_groups']:
        resource = list(list_func(*args, **kwargs))
    else:
        resource = list_func(*args, **kwargs).get(root_tag)

    # Get the identifier for the resource
    resource_identifier = kwargs.get('id')
    if not resource_identifier:
        resource_identifier = kwargs.get('name')

    if len(resource) > 1:
        msg = (f"{len(resource)} {resource_name} found with name or ID "
               f"of {resource_identifier}. Please try again with UUID")
        raise osc_exc.CommandError(msg)

    if len(resource) == 1:
        return resource[0]

    return None


def get_resource_id(resource, resource_name, name):
    """Converts a resource name into a UUID for consumption for the API

    :param callable resource:
        A client_manager callable
    :param resource_name:
        The resource key name for the dictonary returned
    :param name:
        The name of the resource to convert to UUID
    :return:
        The UUID of the found resource
    """
    try:
        # Allow None as a value
        if resource_name in ('policies',):
            if name.lower() in ('none', 'null', 'void'):
                return None

        primary_key = 'id'
        # Availability-zones don't have an id value
        if resource_name == 'availability_zones':
            primary_key = 'name'

        # Projects can be non-uuid so we need to account for this
        if resource_name == 'project':
            if name != 'non-uuid':
                project_id = identity_common.find_project(
                    resource,
                    name
                ).id
                return project_id
            return 'non-uuid'

        if resource_name == 'members':
            member = _find_resource(resource, resource_name, 'members',
                                    name['member_id'], parent=name['pool_id'])
            return member.get('id')

        if resource_name == 'l7rules':
            l7rule = _find_resource(resource, resource_name, 'rules',
                                    name['l7rule_id'],
                                    parent=name['l7policy_id'])
            return l7rule.get('id')

        resource = _find_resource(resource, resource_name, resource_name, name)
        return resource.get(primary_key)

    except IndexError as e:
        msg = f"Unable to locate {name} in {resource_name}"
        raise osc_exc.CommandError(msg) from e


def add_tags_attr_map(attr_map):
    tags_attr_map = {
        'tags': ('tags', list),
        'any_tags': ('tags-any', list),
        'not_tags': ('not-tags', list),
        'not_any_tags': ('not-tags-any', list),
    }
    attr_map.update(tags_attr_map)


def validate_vip_dict(vip_dict, client_manager):
    # We have validation in two places -- _map_attrs checks sub-resources, and
    # later _check_attrs does further api-specific validation. We need both for
    # additional vips, so we may as well just do both here while we're at it.
    if 'subnet_id' not in vip_dict:
        raise osc_exc.CommandError(
            'Additional VIPs must include a subnet-id.')
    subnet_id = get_resource_id(
        client_manager.sdk_connection.network.subnets,
        'subnets', vip_dict['subnet_id'])
    vip_dict['subnet_id'] = subnet_id
    if 'ip_address' in vip_dict:
        try:
            ipaddress.ip_address(vip_dict['ip_address'])
        except ValueError as e:
            raise osc_exc.CommandError(str(e))


def handle_additional_vips(vips, client_manager):
    additional_vips = []
    for vip in vips:
        vip_dict = {}
        parts = vip.split(',')
        for part in parts:
            k, v = part.split('=')
            vip_dict[k.replace('-', '_')] = v
        validate_vip_dict(vip_dict, client_manager)
        additional_vips.append(vip_dict)

    return additional_vips


def handle_vip_sg_ids(vip_sgs, client_manager):
    vip_sg_ids = []
    for sg in vip_sgs:
        sg_id = get_resource_id(
            client_manager.sdk_connection.network.security_groups,
            'security_groups', sg)
        vip_sg_ids.append(sg_id)
    return vip_sg_ids


def get_loadbalancer_attrs(client_manager, parsed_args):
    attr_map = {
        'name': ('name', str),
        'description': ('description', str),
        'protocol': ('protocol', str),
        'loadbalancer': (
            'loadbalancer_id',
            'loadbalancers',
            client_manager.load_balancer.load_balancer_list
        ),
        'connection_limit': ('connection_limit', str),
        'protocol_port': ('protocol_port', int),
        'project': (
            'project_id',
            'project',
            client_manager.identity
        ),
        'vip_address': ('vip_address', str),
        'vip_port_id': (
            'vip_port_id',
            'ports',
            client_manager.sdk_connection.network.ports
        ),
        'vip_subnet_id': (
            'vip_subnet_id',
            'subnets',
            client_manager.sdk_connection.network.subnets
        ),
        'vip_network_id': (
            'vip_network_id',
            'networks',
            client_manager.sdk_connection.network.networks
        ),
        'vip_qos_policy_id': (
            'vip_qos_policy_id',
            'policies',
            client_manager.sdk_connection.network.qos_policies
        ),
        'vip_vnic_type': ('vip_vnic_type', str),
        'enable': ('admin_state_up', lambda x: True),
        'disable': ('admin_state_up', lambda x: False),
        'cascade': ('cascade', lambda x: True),
        'provisioning_status': ('provisioning_status', str),
        'operating_status': ('operating_status', str),
        'provider': ('provider', str),
        'flavor': (
            'flavor_id',
            'flavors',
            client_manager.load_balancer.flavor_list
        ),
        'availability_zone': ('availability_zone', str),
        'additional_vip': (
            'additional_vips',
            functools.partial(
                handle_additional_vips, client_manager=client_manager)
        ),
        'vip_sg_id': (
            'vip_sg_ids',
            functools.partial(
                handle_vip_sg_ids, client_manager=client_manager),
        )
    }
    add_tags_attr_map(attr_map)

    _attrs = vars(parsed_args)
    attrs = _map_attrs(_attrs, attr_map)

    return attrs


def get_listener_attrs(client_manager, parsed_args):
    attr_map = {
        'name': ('name', str),
        'description': ('description', str),
        'protocol': ('protocol', str),
        'listener': (
            'listener_id',
            'listeners',
            client_manager.load_balancer.listener_list
        ),
        'loadbalancer': (
            'loadbalancer_id',
            'loadbalancers',
            client_manager.load_balancer.load_balancer_list
        ),
        'connection_limit': ('connection_limit', str),
        'protocol_port': ('protocol_port', int),
        'default_pool': (
            'default_pool_id',
            'pools',
            client_manager.load_balancer.pool_list
        ),
        'project': (
            'project_id',
            'project',
            client_manager.identity
        ),
        'enable': ('admin_state_up', lambda x: True),
        'disable': ('admin_state_up', lambda x: False),
        'insert_headers': ('insert_headers', _format_kv),
        'default_tls_container_ref': ('default_tls_container_ref', str),
        'sni_container_refs': ('sni_container_refs', list),
        'timeout_client_data': ('timeout_client_data', int),
        'timeout_member_connect': ('timeout_member_connect', int),
        'timeout_member_data': ('timeout_member_data', int),
        'timeout_tcp_inspect': ('timeout_tcp_inspect', int),
        'client_ca_tls_container_ref': ('client_ca_tls_container_ref',
                                        _format_str_if_need_treat_unset),
        'client_authentication': ('client_authentication', str),
        'client_crl_container_ref': ('client_crl_container_ref',
                                     _format_str_if_need_treat_unset),
        'allowed_cidrs': ('allowed_cidrs', list),
        'tls_ciphers': ('tls_ciphers', str),
        'tls_versions': ('tls_versions', list),
        'alpn_protocols': ('alpn_protocols', list),
        'hsts_max_age': ('hsts_max_age', int),
        'hsts_include_subdomains': ('hsts_include_subdomains', bool),
        'hsts_preload': ('hsts_preload', bool),
    }
    add_tags_attr_map(attr_map)

    _attrs = vars(parsed_args)
    attrs = _map_attrs(_attrs, attr_map)

    return attrs


def get_pool_attrs(client_manager, parsed_args):
    attr_map = {
        'name': ('name', str),
        'description': ('description', str),
        'protocol': ('protocol', str),
        'pool': (
            'pool_id',
            'pools',
            client_manager.load_balancer.pool_list
        ),
        'loadbalancer': (
            'loadbalancer_id',
            'loadbalancers',
            client_manager.load_balancer.load_balancer_list
        ),
        'lb_algorithm': ('lb_algorithm', str),
        'listener': (
            'listener_id',
            'listeners',
            client_manager.load_balancer.listener_list
        ),
        'project': (
            'project_id',
            'project',
            client_manager.identity
        ),
        'session_persistence': ('session_persistence', _format_kv),
        'enable': ('admin_state_up', lambda x: True),
        'disable': ('admin_state_up', lambda x: False),
        'tls_container_ref': ('tls_container_ref',
                              _format_str_if_need_treat_unset),
        'ca_tls_container_ref': ('ca_tls_container_ref',
                                 _format_str_if_need_treat_unset),
        'crl_container_ref': ('crl_container_ref',
                              _format_str_if_need_treat_unset),

        'enable_tls': ('tls_enabled', lambda x: True),
        'disable_tls': ('tls_enabled', lambda x: False),
        'tls_ciphers': ('tls_ciphers', str),
        'tls_versions': ('tls_versions', list),
        'alpn_protocols': ('alpn_protocols', list),
    }
    add_tags_attr_map(attr_map)

    _attrs = vars(parsed_args)
    attrs = _map_attrs(_attrs, attr_map)

    return attrs


def get_member_attrs(client_manager, parsed_args):
    attr_map = {
        'name': ('name', str),
        'address': ('address', str),
        'protocol_port': ('protocol_port', int),
        'project_id': (
            'project_id',
            'project',
            client_manager.identity
        ),
        'pool': (
            'pool_id',
            'pools',
            client_manager.load_balancer.pool_list
        ),
        'member': (
            'member_id',
            'members',
            'pool',
            client_manager.load_balancer.member_list
        ),
        'enable_backup': ('backup', lambda x: True),
        'disable_backup': ('backup', lambda x: False),
        'weight': ('weight', int),
        'subnet_id': (
            'subnet_id',
            'subnets',
            client_manager.sdk_connection.network.subnets
        ),
        'monitor_port': ('monitor_port', int),
        'monitor_address': ('monitor_address', str),
        'enable': ('admin_state_up', lambda x: True),
        'disable': ('admin_state_up', lambda x: False),
        'request_sriov': ('request_sriov', lambda x: True),
        'vnic_type': ('vnic_type', str),
    }
    add_tags_attr_map(attr_map)

    _attrs = vars(parsed_args)
    attrs = _map_attrs(_attrs, attr_map)

    return attrs


def get_l7policy_attrs(client_manager, parsed_args):
    attr_map = {
        'name': ('name', str),
        'description': ('description', str),
        'redirect_url': ('redirect_url', str),
        'redirect_http_code': ('redirect_http_code', int),
        'redirect_prefix': ('redirect_prefix', str),
        'l7policy': (
            'l7policy_id',
            'l7policies',
            client_manager.load_balancer.l7policy_list
        ),
        'redirect_pool': (
            'redirect_pool_id',
            'pools',
            client_manager.load_balancer.pool_list
        ),
        'listener': (
            'listener_id',
            'listeners',
            client_manager.load_balancer.listener_list
        ),
        'action': ('action', str),
        'project': (
            'project_id',
            'projects',
            client_manager.identity
        ),
        'position': ('position', int),
        'enable': ('admin_state_up', lambda x: True),
        'disable': ('admin_state_up', lambda x: False)
    }
    add_tags_attr_map(attr_map)

    _attrs = vars(parsed_args)
    attrs = _map_attrs(_attrs, attr_map)

    return attrs


def get_l7rule_attrs(client_manager, parsed_args):
    attr_map = {
        'action': ('action', str),
        'project': (
            'project_id',
            'project',
            client_manager.identity
        ),
        'invert': ('invert', lambda x: True),
        'l7rule': (
            'l7rule_id',
            'l7rules',
            'l7policy',  # parent attr
            client_manager.load_balancer.l7rule_list
        ),
        'l7policy': (
            'l7policy_id',
            'l7policies',
            client_manager.load_balancer.l7policy_list
        ),
        'value': ('value', str),
        'key': ('key', str),
        'type': ('type', str),
        'compare_type': ('compare_type', str),
        'enable': ('admin_state_up', lambda x: True),
        'disable': ('admin_state_up', lambda x: False)
    }
    add_tags_attr_map(attr_map)

    _attrs = vars(parsed_args)
    attrs = _map_attrs(_attrs, attr_map)

    return attrs


def get_health_monitor_attrs(client_manager, parsed_args):
    attr_map = {
        'health_monitor': (
            'health_monitor_id',
            'healthmonitors',
            client_manager.load_balancer.health_monitor_list
        ),
        'project': (
            'project_id',
            'project',
            client_manager.identity
        ),
        'name': ('name', str),
        'pool': (
            'pool_id',
            'pools',
            client_manager.load_balancer.pool_list
        ),
        'delay': ('delay', int),
        'expected_codes': ('expected_codes', str),
        'max_retries': ('max_retries', int),
        'http_method': ('http_method', str),
        'type': ('type', str),
        'timeout': ('timeout', int),
        'max_retries_down': ('max_retries_down', int),
        'url_path': ('url_path', str),
        'enable': ('admin_state_up', lambda x: True),
        'disable': ('admin_state_up', lambda x: False),
        'http_version': ('http_version', float),
        'domain_name': ('domain_name', str)
    }
    add_tags_attr_map(attr_map)

    _attrs = vars(parsed_args)
    attrs = _map_attrs(_attrs, attr_map)

    return attrs


def get_quota_attrs(client_manager, parsed_args):
    attr_map = {
        'health_monitor': ('health_monitor', int),
        'listener': ('listener', int),
        'load_balancer': ('load_balancer', int),
        'member': ('member', int),
        'pool': ('pool', int),
        'l7policy': ('l7policy', int),
        'l7rule': ('l7rule', int),
        'project': (
            'project_id',
            'project',
            client_manager.identity
        ),
    }

    _attrs = vars(parsed_args)
    attrs = _map_attrs(_attrs, attr_map)

    return attrs


def get_amphora_attrs(client_manager, parsed_args):
    attr_map = {
        'amphora_id': (
            'amphora_id',
            'amphorae',
            client_manager.load_balancer.amphora_list,
        ),
        'loadbalancer': (
            'loadbalancer_id',
            'loadbalancers',
            client_manager.load_balancer.load_balancer_list,
        ),
        'compute_id': ('compute_id', str),
        'role': ('role', str),
        'status': ('status', str),
        'image_id': ('image_id', str),
    }

    return _map_attrs(vars(parsed_args), attr_map)


def get_provider_attrs(parsed_args):
    attr_map = {
        'provider': ('provider_name', str),
        'description': ('description', str),
        'flavor': ('flavor', bool),
        'availability_zone': ('availability_zone', bool),
    }

    return _map_attrs(vars(parsed_args), attr_map)


def get_flavor_attrs(client_manager, parsed_args):
    attr_map = {
        'name': ('name', str),
        'flavor': (
            'flavor_id',
            'flavors',
            client_manager.load_balancer.flavor_list,
        ),
        'flavorprofile': (
            'flavor_profile_id',
            'flavorprofiles',
            client_manager.load_balancer.flavorprofile_list,
        ),
        'enable': ('enabled', lambda x: True),
        'disable': ('enabled', lambda x: False),
        'description': ('description', str),
    }

    _attrs = vars(parsed_args)
    attrs = _map_attrs(_attrs, attr_map)

    return attrs


def get_flavorprofile_attrs(client_manager, parsed_args):
    attr_map = {
        'name': ('name', str),
        'flavorprofile': (
            'flavorprofile_id',
            'flavorprofiles',
            client_manager.load_balancer.flavorprofile_list,
        ),
        'provider': ('provider_name', str),
        'flavor_data': ('flavor_data', str),
    }

    _attrs = vars(parsed_args)
    attrs = _map_attrs(_attrs, attr_map)

    return attrs


def get_availabilityzone_attrs(client_manager, parsed_args):
    attr_map = {
        'name': ('name', str),
        'availabilityzone': (
            'availabilityzone_name',
            'availability_zones',
            client_manager.load_balancer.availabilityzone_list,
        ),
        'availabilityzoneprofile': (
            'availability_zone_profile_id',
            'availability_zone_profiles',
            client_manager.load_balancer.availabilityzoneprofile_list,
        ),
        'enable': ('enabled', lambda x: True),
        'disable': ('enabled', lambda x: False),
        'description': ('description', str),
    }

    _attrs = vars(parsed_args)
    attrs = _map_attrs(_attrs, attr_map)

    return attrs


def get_availabilityzoneprofile_attrs(client_manager, parsed_args):
    attr_map = {
        'name': ('name', str),
        'availabilityzoneprofile': (
            'availability_zone_profile_id',
            'availability_zone_profiles',
            client_manager.load_balancer.availabilityzoneprofile_list,
        ),
        'provider': ('provider_name', str),
        'availability_zone_data': ('availability_zone_data', str),
    }

    _attrs = vars(parsed_args)
    attrs = _map_attrs(_attrs, attr_map)

    return attrs


def format_list(data):
    if data:
        return '\n'.join(i['id'] for i in data)
    return None


class ListColumn(columns.FormattableColumn):
    def human_readable(self):
        return format_list(self._value)


def format_list_flat(data):
    if data:
        return '\n'.join(i for i in data)
    return None


class FlatListColumn(columns.FormattableColumn):
    def human_readable(self):
        return format_list_flat(self._value)


def format_hash(data):
    if data:
        return '\n'.join('{}={}'.format(k, v) for k, v in data.items())
    return None


class HashColumn(columns.FormattableColumn):
    def human_readable(self):
        return format_hash(self._value)


def _format_kv(data):
    formatted_kv = {}
    values = data.split(',')
    for value in values:
        k, v = value.split('=')
        formatted_kv[k] = v

    return formatted_kv


def _format_str_if_need_treat_unset(data):
    if data.lower() in ('none', 'null', 'void'):
        return None
    return str(data)


def get_unsets(parsed_args):
    unsets = {}
    for arg, value in vars(parsed_args).items():
        if value and arg == 'tags':
            unsets[arg] = value
        elif value is True and arg not in ('wait', 'all_tag'):
            unsets[arg] = None
    return unsets


class _Munch(dict):
    __getattr__ = dict.get


def wait_for_active(status_f, res_id):
    success = utils.wait_for_status(
        status_f=lambda x: _Munch(status_f(x)),
        res_id=res_id,
        status_field=constants.PROVISIONING_STATUS,
        sleep_time=3
    )
    if not success:
        raise exceptions.OctaviaClientException(
            code="n/a",
            message="The resource did not successfully reach ACTIVE status.")


def wait_for_delete(status_f, res_id,
                    status_field=constants.PROVISIONING_STATUS):
    class Getter(object):
        @staticmethod
        def get(id):
            return _Munch(status_f(id))

    try:
        success = utils.wait_for_delete(
            manager=Getter,
            res_id=res_id,
            status_field=status_field,
            sleep_time=3
        )
        if not success:
            raise exceptions.OctaviaClientException(
                code="n/a",
                message="The resource could not be successfully deleted.")
    except exceptions.OctaviaClientException as e:
        if e.code != 404:
            raise


def set_tags_for_set(resource_get, resource_id, attrs, clear_tags=False):
    if attrs.get('tags'):
        resource = resource_get(resource_id)
        tags = set([] if clear_tags else resource['tags'])
        tags |= set(attrs['tags'])
        attrs['tags'] = list(tags)
    elif clear_tags:
        attrs['tags'] = []


def set_tags_for_unset(resource_get, resource_id, attrs, clear_tags=False):
    if clear_tags:
        attrs['tags'] = []
    elif attrs.get('tags'):
        resource = resource_get(resource_id)
        tags = set(resource['tags'])
        tags -= set(attrs['tags'])
        attrs['tags'] = list(tags)
