#!/usr/bin/python
# Copyright (c) 2017 Alibaba Group Holding Limited. He Guimin <heguimin36@163.com.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see http://www.gnu.org/licenses/.

from __future__ import absolute_import, division, print_function
__metaclass__ = type

ANSIBLE_METADATA = {'metadata_version': '1.1',
                    'status': ['preview'],
                    'supported_by': 'community'}

DOCUMENTATION = """
---
module: ali_slb_server
version_added: "1.5.0"
short_description: Add or remove a list of backend servers to/from a specified SLB
description:
  - Returns information about the backend servers. Will be marked changed when called only if state is changed.
options:
  state:
    description:
      - Add or remove backend server to/from a specified slb
    default: 'present'
    choices: ['present', 'absent']
  load_balancer_id:
    description:
      - The unique ID of a Server Load Balancer instance
    required: true
    aliases: [ 'lb_id']
  backend_servers:
    description:
      - List of hash/dictionary of backend servers to add or set in when C(state=present)
      - List IDs of backend servers which in the load balancer when C(state=absent)
    required: true
    aliases: [ 'servers']
    suboptions:
      server_id:
        description:
          - The ID of ecs instance which is added into load balancer.
        required: true
      weight:
        description:
          - The weight of backend server in the load balancer.
        choices: [0~100]
        default: 100
requirements:
    - "python >= 2.6"
    - "footmark >= 1.1.16"
extends_documentation_fragment:
    - alicloud
author:
  - "He Guimin (@xiaozhu36)"
"""

EXAMPLES = '''
# Provisioning new add or remove Backend Server from SLB
# Basic example to add backend server to load balancer instance
- name: add backend server
  hosts: localhost
  connection: local
  vars:
    load_balancer_id: lb-abcd1234
  tasks:
    - name: add backend server
      ali_slb_server:
        load_balancer_id: '{{ load_balancer_id }}'
        backend_servers:
          - server_id: i-abcd1234
            weight: 70
          - server_id: i-abce1245

#Basic example to set backend server of load balancer instance
- name: set backend server
  hosts: localhost
  connection: local
  vars:
    alicloud_access_key: <your-alicloud-access-key-id>
    alicloud_secret_key: <your-alicloud-access-secret-key>
  tasks:
    - name: set backend server
      ali_slb_server:
        alicloud_access_key: '{{ alicloud_access_key }}'
        alicloud_secret_key: '{{ alicloud_secret_key }}'
        load_balancer_id: lb-abcd1234
        backend_servers:
          - server_id: i-abcd1234
            weight: 50
          - server_id: i-abcd1234
            weight: 80

#Basic example to remove backend servers from load balancer instance
- name: remove backend servers
  hosts: localhost
  connection: local
  tasks:
    - name: remove backend servers
      ali_slb_server:
        load_balancer_id: lb-abcd1234
        state: absent
        backend_servers:
          - i-abcd1234
          - i-abcd1234

'''
RETURN = '''
load_balancer_id:
    description: ID of the load balancer.
    returned: when success
    type: string
    sample: "lb-2zeyfm5a14c9ffxvxmvco"
"backend_servers":
    description: Details about the backened-servers that were added.
    returned: when success
    type: list
    sample: [
        {
            "health_status": "abnormal",
            "id": "i-2zeau2evvbnwufq0fa7q"
        },
        {
            "health_status": "abnormal",
            "id": "i-2zehasnejqr6g6agys5a"
        }
    ]
'''

import time
import sys
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.alicloud_ecs import get_acs_connection_info, ecs_argument_spec, slb_connect

try:
    from footmark.exception import SLBResponseError

    HAS_FOOTMARK = True
except ImportError:
    HAS_FOOTMARK = False


def get_backen_server_weight(server):
    """
    Retrieves instance information from an instance
    ID and returns it as a dictionary
    """
    return {'id': server.id, 'weight': server.weight}


def get_backen_server_status(server):
    """
    Retrieves instance information from an instance
    ID and returns it as a dictionary
    """
    return {'id': server.id, 'health_status': server.status}


def add_set_backend_servers(module, slb, load_balancer_id, backend_servers):
    """
    Add and/or Set backend servers to an slb

    :param module: Ansible module object
    :param slb: authenticated slb connection object
    :param load_balancer_id: load balancer id to add/set backend servers to
    :param backend_servers: backend severs information to add/set
    :return: returns changed state, current added backend servers and custom message.
    """

    changed = False
    server_id_param = 'server_id'
    backend_servers_to_set = []
    backend_servers_to_add = []
    result = []
    current_backend_servers = []
    changed_after_add = False
    changed_after_set = False

    try:

        load_balancer_info = slb.describe_load_balancer_attribute(load_balancer_id)

        # Verifying if server load balancer Object is present
        if load_balancer_info:
            existing_backend_servers = str(load_balancer_info.backend_servers['backend_server'])

            # Segregating existing backend servers from new backend servers from the provided backend servers
            for backend_server in backend_servers:

                backend_server_string = backend_server[server_id_param] + '\''
                if backend_server_string in existing_backend_servers:
                    backend_servers_to_set.append(backend_server)
                else:
                    backend_servers_to_add.append(backend_server)

                    # Adding new backend servers if provided
            if len(backend_servers_to_add) > 0:
                current_backend_servers.extend(slb.add_backend_servers(load_balancer_id=load_balancer_id, backend_servers=backend_servers_to_add))
                changed = True
                # Setting exisiting backend servers if provided
            if len(backend_servers_to_set) > 0:
                backen_servers = slb.set_backend_servers(load_balancer_id=load_balancer_id, backend_servers=backend_servers_to_set)
                changed = True
                # If backend server result after set action is available then clearing actual list
                # and adding new result to it.
                if len(backen_servers) > 0:
                    # Below operation clears list using slice operation
                    current_backend_servers[:] = []
                    current_backend_servers.extend(backen_servers)

            if changed_after_add or changed_after_set:
                changed = True

        else:
            module.fail_json(msg="Could not find provided load balancer instance")

    except SLBResponseError as ex:
        module.fail_json(msg='Unable to add backend servers, error: {0}'.format(ex))

    return changed, current_backend_servers


def remove_backend_servers(module, slb, load_balancer_id, backend_servers):
    """
    Remove backend servers from an slb

    :param module: Ansible module object
    :param slb: authenticated slb connection object
    :param load_balancer_id: load balancer id to remove backend servers from
    :param backend_servers: list of backend server ids to remove from slb
    :return: returns changed state, current added backend servers and custom message.
    """

    changed = False

    try:
        backend_servers = slb.remove_backend_servers(load_balancer_id=load_balancer_id, backend_server_ids=backend_servers)
        changed = True

    except SLBResponseError as ex:
        module.fail_json(msg='Unable to remove backend servers, error: {0}'.format(ex))

    return changed, backend_servers


def describe_backend_servers_health_status(module, slb, load_balancer_id=None, listener_ports=None):
    """
    Describe health status of added backend servers of an slb

    :param module: Ansible module object
    :param slb: authenticated slb connection object
    :param load_balancer_id: load balancer id to remove backend servers from
    :param listener_ports: list of ports to for which backend server health status is required
    :return: returns backend servers health status and custom message
    """

    backend_servers_health_status = []
    try:
        if listener_ports:
            for port in listener_ports:

                backend_server = slb.describe_backend_servers_health_status(load_balancer_id=load_balancer_id, port=port)
                backend_servers_health_status.extend(backend_server)
        else:
            backend_servers_health_status = slb.describe_backend_servers_health_status(load_balancer_id=load_balancer_id)

    except SLBResponseError as ex:
        module.fail_json(msg='Unable to describe backend servers health status, error: {0}'.format(ex))

    return backend_servers_health_status


def validate_backend_server_info(module, backend_servers, default_weight):
    """
    Validate backend server information provided by user for add, set and remove action

    :param module: Ansible module object
    :param backend_servers: backend severs information to validate (list of dictionaries or string)
    :param default_weight: assigns default weight, if provided, for a backend server to set/add
    """

    server_id_param = 'server_id'
    weight_param = 'weight'
    VALID_PARAMS = (server_id_param, weight_param)

    for backend_server in backend_servers:

        if isinstance(backend_server, dict) is False:
            module.fail_json(msg='Invalid backend_servers parameter type [%s] for state=present.' % type(backend_server))

        for k in backend_server:
            if k not in VALID_PARAMS:
                module.fail_json(msg='Invalid backend_server parameter \'{}\''.format(k))

        server_id = get_sub_value(backend_server, [server_id_param])
        if server_id is None:
            module.fail_json(msg="'server_id': required field is set")

        weight = get_sub_value(backend_server, [weight_param])

        if weight is None:
            backend_server[weight_param] = default_weight
        else:
            # verifying weight parameter for non numeral string and limit validation
            try:
                w = int(weight)
                if w < 0 or w > 100:
                    module.fail_json(msg="'weight': field value is invalid. Expect to [0-100].")
            except Exception as e:
                module.fail_json(msg="'weight': field value is invalid. Expect to positive integer [0-100].")


def get_sub_value(dictionary, aliases):
    """

    :param dictionary: a dictionary to check in for aliases
    :param aliases: list of keys to check in dictionary for value retrieval
    :return: returns value if alias found else none
    """

    if (dictionary and aliases) is not None:
        for alias in aliases:
            if alias in dictionary:
                return dictionary[alias]
        return None
    else:
        return None


def get_verify_listener_ports(module, listener_ports=None):
    """
    Validate and get listener ports

    :param module: Ansible module object
    :param listener_ports: list of ports to for which backend server health status is required
    :return: formatted listener ports
    """

    if listener_ports:
        if len(listener_ports) > 0:
            for port in listener_ports:

                try:
                    port = int(port)
                except Exception as ex:
                    module.fail_json(msg='Invalid port value')
        else:
            listener_ports = None

    return listener_ports


def main():
    argument_spec = ecs_argument_spec()
    argument_spec.update(dict(
        state=dict(choices=['present', 'absent'], default='present'),
        backend_servers=dict(required=True, type='list', aliases=['servers']),
        load_balancer_id=dict(required=True, aliases=['lb_id']),
    ))

    # handling region parameter which is not required by this module
    del argument_spec['alicloud_region']

    module = AnsibleModule(argument_spec=argument_spec)

    if HAS_FOOTMARK is False:
        module.fail_json(msg="'footmark' is required for the module ali_slb_server. "
                             "Please install 'footmark' by using 'sudo pip install footmark'.")

    # handling region parameter which is required by common utils file to login but not required by this module
    module.params['alicloud_region'] = 'cn-hangzhou'
    slb = slb_connect(module)

    state = module.params['state']
    backend_servers = module.params['backend_servers']
    load_balancer_id = module.params['load_balancer_id']

    if state == 'present':

        if len(backend_servers) > 0:

            validate_backend_server_info(module, backend_servers, 100)

            changed, current_backend_servers = add_set_backend_servers(module, slb, load_balancer_id, backend_servers)

            result_servers = []
            for server in current_backend_servers:
                result_servers.append(get_backen_server_weight(server))
            module.exit_json(changed=changed, backend_servers=result_servers, load_balancer_id=load_balancer_id)
        else:
            module.fail_json(msg='backend servers information is mandatory to state=present')

    if len(backend_servers) > 0:

        if isinstance(backend_servers, list) is False:
            module.fail_json(msg='Invalid backend_server parameter type [%s] for state=absent.' % type(backend_servers))

        changed, backend_servers = remove_backend_servers(module, slb, load_balancer_id, backend_servers)
        result_servers = []
        for server in backend_servers:
            result_servers.append(get_backen_server_weight(server))
        module.exit_json(changed=changed, backend_servers=result_servers, load_balancer_id=load_balancer_id)
    else:
        module.fail_json(msg='backend server ID(s) information is mandatory to state=absent')

    # elif state == 'list':
    #
    #     if load_balancer_id:
    #
    #         listener_ports = get_verify_listener_ports(module, listener_ports)
    #
    #         backend_servers = describe_backend_servers_health_status(module, slb, load_balancer_id=load_balancer_id,
    #                                                                  listener_ports=listener_ports)
    #
    #         result_servers = []
    #         for server in backend_servers:
    #             result_servers.append(get_backen_server_status(server))
    #         module.exit_json(changed=changed, backend_servers=result_servers, load_balancer_id=load_balancer_id)
    #     else:
    #         module.fail_json(msg='load balancer id is mandatory to perform action')


if __name__ == '__main__':
    main()
