#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright: (c) 2017, F5 Networks Inc.
# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

from __future__ import absolute_import, division, print_function

__metaclass__ = type

DOCUMENTATION = r'''
---
module: bigip_pool
short_description: Manages F5 BIG-IP LTM pools
description:
  - Manages F5 BIG-IP LTM pools via iControl REST API.
version_added: "1.0.0"
options:
  description:
    description:
      - Specifies descriptive text that identifies the pool.
    type: str
  name:
    description:
      - Pool name
    type: str
    aliases:
      - pool
  lb_method:
    description:
      - Load balancing method. When creating a new pool, if this value is not
        specified, the default of C(round-robin) is used.
    type: str
    choices:
      - dynamic-ratio-member
      - dynamic-ratio-node
      - fastest-app-response
      - fastest-node
      - least-connections-member
      - least-connections-node
      - least-sessions
      - observed-member
      - observed-node
      - predictive-member
      - predictive-node
      - ratio-least-connections-member
      - ratio-least-connections-node
      - ratio-member
      - ratio-node
      - ratio-session
      - round-robin
      - weighted-least-connections-member
      - weighted-least-connections-node
  monitor_type:
    description:
      - Monitor rule type when C(monitors) is specified.
      - When creating a new pool, if this value is not specified, the default
        of C(and_list) is used.
      - When C(single), ensures all specified monitors are checked, but
        additionally includes checks to make sure you only specified a single
        monitor.
      - When C(and_list), ensures B(all) monitors are checked.
      - When C(m_of_n), ensures C(quorum) of C(monitors) are checked. C(m_of_n)
        B(requires) a C(quorum) of 1 or greater be set either in the playbook,
        or already exist on the device.
      - Both C(single) and C(and_list) are functionally identical, as BIG-IP
        considers all monitors as "a list".
    type: str
    aliases:
      - availability_requirements_type
    choices:
      - and_list
      - m_of_n
      - single
  quorum:
    description:
      - Monitor quorum value when C(monitor_type) is C(m_of_n).
      - Quorum must be a value of 1 or greater when C(monitor_type) is C(m_of_n).
    type: int
    aliases:
      - availability_requirements_at_least
  monitors:
    description:
      - Monitor template name list. If the partition is not provided as part of
        the monitor name, the C(partition) option is used instead.
    type: list
    elements: str
  slow_ramp_time:
    description:
      - Sets the ramp-up time (in seconds) to gradually ramp up the load on
        newly added or freshly detected up pool members.
    type: int
  reselect_tries:
    description:
      - Sets the number of times the system tries to contact a pool member
        after a passive failure.
    type: int
  service_down_action:
    description:
      - Sets the action to take when node goes down in pool.
    type: str
    choices:
      - none
      - reset
      - drop
      - reselect
  partition:
    description:
      - Device partition on which to manage resources.
    type: str
    default: Common
  state:
    description:
      - When C(present), guarantees the pool exists with the provided
        attributes.
      - When C(absent), removes the pool from the system.
    type: str
    choices:
      - absent
      - present
    default: present
  metadata:
    description:
      - Arbitrary key/value pairs you can attach to a pool. This is useful in
        situations where you might want to annotate a pool to be managed by Ansible.
      - Key names are stored as strings; this includes names that are numbers.
      - Values for all of the keys are stored as strings; this includes values
        that are numbers.
      - Data will be persisted, not ephemeral.
    type: raw
  priority_group_activation:
    description:
      - Specifies whether the system load balances traffic according to the priority
        number assigned to the pool member.
      - When creating a new pool, if this parameter is not specified, the default of
        C(0) is used.
      - To disable this setting, provide the value C(0).
      - Once you enable this setting, you can specify pool member priority when you
        create a new pool or on a pool member's properties screen.
      - The system treats same-priority pool members as a group.
      - To enable priority group activation, provide a number from C(0) to C(65535)
        that represents the minimum number of members that must be available in one
        priority group before the system directs traffic to members in a lower
        priority group.
      - When a sufficient number of members become available in the higher priority
        group, the system again directs traffic to the higher priority group.
    type: int
    aliases:
      - minimum_active_members
  min_up_members:
    description:
      - Specifies the minimum number of pool members that must be up,
      - otherwise, the system takes the action specified in the C(min-up-members-action) option.
      - Use this option for gateway pools in a redundant system where a unit number is applied to the pool.
      - This indicates the pool is configured only on the specified unit.
      - When creating a new pool, if this parameter is not specified, the default is C(0).
    type: int
  min_up_members_action:
    description:
      - Specifies the action to take if C(min_up_members_checking) is C(enabled) and the number of active pool members
        falls below the number specified in the C(min_up_members) option.
      - When creating a new pool, if this parameter is not specified, the default is C(failover).
    type: str
    choices:
      - failover
      - reboot
      - restart-all
  min_up_members_checking:
    description:
      - Enables or disables the C(min_up_members) feature.
      - If you enable this feature, you must also specify a value for both the C(min_up_members) and
        C(min_up_members_action) options.
      - When creating a new pool, if this parameter is not specified, the default is C(disabled).
    type: str
    choices:
      - enabled
      - disabled
  aggregate:
    description:
      - List of pool definitions to be created, modified, or removed.
      - When using C(aggregates), if one of the aggregate definitions is invalid, the aggregate run will fail,
        indicating the error it last encountered.
      - The module will B(NOT) rollback any changes it has made prior to encountering the error.
      - The module also will not indicate which changes were made prior to failure. Therefore we strongly advise
        you run the module in C(check) mode to ensure basic validation prior to executing this module.
    type: list
    elements: dict
    suboptions:
      description:
        description:
          - Specifies descriptive text that identifies the pool.
        type: str
      name:
        description:
          - Pool name
        type: str
        aliases:
          - pool
      lb_method:
        description:
          - Load balancing method. When creating a new pool, if this value is not
            specified, the default of C(round-robin) is used.
        type: str
        choices:
          - dynamic-ratio-member
          - dynamic-ratio-node
          - fastest-app-response
          - fastest-node
          - least-connections-member
          - least-connections-node
          - least-sessions
          - observed-member
          - observed-node
          - predictive-member
          - predictive-node
          - ratio-least-connections-member
          - ratio-least-connections-node
          - ratio-member
          - ratio-node
          - ratio-session
          - round-robin
          - weighted-least-connections-member
          - weighted-least-connections-node
      monitor_type:
        description:
          - Monitor rule type when C(monitors) is specified.
          - When creating a new pool, if this value is not specified, the default
            of C(and_list) is used.
          - When C(single), ensures all specified monitors are checked, but
            additionally includes checks to make sure you only specified a single
            monitor.
          - When C(and_list), ensures B(all) monitors are checked.
          - When C(m_of_n), ensures C(quorum) of C(monitors) are checked. C(m_of_n)
            B(requires) a C(quorum) of 1 or greater be set either in the playbook,
            or already exist on the device.
          - Both C(single) and C(and_list) are functionally identical, as BIG-IP
            considers all monitors as "a list".
        type: str
        aliases:
          - availability_requirements_type
        choices:
          - and_list
          - m_of_n
          - single
      quorum:
        description:
          - Monitor quorum value when C(monitor_type) is C(m_of_n).
          - Quorum must be a value of 1 or greater when C(monitor_type) is C(m_of_n).
        type: int
        aliases:
          - availability_requirements_at_least
      monitors:
        description:
          - Monitor template name list. If the partition is not provided as part of
            the monitor name, the C(partition) option is used instead.
        type: list
        elements: str
      slow_ramp_time:
        description:
          - Sets the ramp-up time (in seconds) to gradually ramp up the load on
            newly added or freshly detected up pool members.
        type: int
      reselect_tries:
        description:
          - Sets the number of times the system tries to contact a pool member
            after a passive failure.
        type: int
      service_down_action:
        description:
          - Sets the action to take when node goes down in pool.
        type: str
        choices:
          - none
          - reset
          - drop
          - reselect
      partition:
        description:
          - Device partition on which to manage resources.
        type: str
        default: Common
      state:
        description:
          - When C(present), guarantees the pool exists with the provided
            attributes.
          - When C(absent), removes the pool from the system.
        type: str
        choices:
          - absent
          - present
        default: present
      metadata:
        description:
          - Arbitrary key/value pairs you can attach to a pool. This is useful in
            situations where you might want to annotate a pool to be managed by Ansible.
          - Key names are stored as strings; this includes names that are numbers.
          - Values for all of the keys are stored as strings; this includes values
            that are numbers.
          - Data will be persisted, not ephemeral.
        type: raw
      priority_group_activation:
        description:
          - Specifies whether the system load balances traffic according to the priority
            number assigned to the pool member.
          - When creating a new pool, if this parameter is not specified, the default of
            C(0) is used.
          - To disable this setting, provide the value C(0).
          - Once you enable this setting, you can specify pool member priority when you
            create a new pool or on a pool member's properties screen.
          - The system treats same-priority pool members as a group.
          - To enable priority group activation, provide a number from C(0) to C(65535)
            that represents the minimum number of members that must be available in one
            priority group before the system directs traffic to members in a lower
            priority group.
          - When a sufficient number of members become available in the higher priority
            group, the system again directs traffic to the higher priority group.
        type: int
        aliases:
          - minimum_active_members
      min_up_members:
        description:
          - Specifies the minimum number of pool members that must be up,
          - otherwise, the system takes the action specified in the C(min-up-members-action) option.
          - Use this option for gateway pools in a redundant system where a unit number is applied to the pool.
          - This indicates the pool is configured only on the specified unit.
          - When creating a new pool, if this parameter is not specified, the default is C(0).
        type: int
      min_up_members_action:
        description:
          - Specifies the action to take if C(min_up_members_checking) is C(enabled) and the number of active pool members
            falls below the number specified in the C(min_up_members) option.
          - When creating a new pool, if this parameter is not specified, the default is C(failover).
        type: str
        choices:
          - failover
          - reboot
          - restart-all
      min_up_members_checking:
        description:
          - Enables or disables the C(min_up_members) feature.
          - If you enable this feature, you must also specify a value for both the C(min_up_members) and
            C(min_up_members_action) options.
          - When creating a new pool, if this parameter is not specified, the default is C(disabled).
        type: str
        choices:
          - enabled
          - disabled
    aliases:
      - pools
  replace_all_with:
    description:
      - Removes pools not defined in the C(aggregate) parameter.
      - This operation is all or none, meaning it will stop if there are some pools
        that cannot be removed.
    type: bool
    default: false
    aliases:
      - purge
extends_documentation_fragment: f5networks.f5_modules.f5
author:
  - Tim Rupp (@caphrim007)
  - Wojciech Wypior (@wojtek0806)
'''

EXAMPLES = r'''
- name: Create pool
  bigip_pool:
    state: present
    name: my-pool
    partition: Common
    lb_method: least-connections-member
    slow_ramp_time: 120
    provider:
      server: lb.mydomain.com
      user: admin
      password: secret
  delegate_to: localhost

- name: Modify load balancer method
  bigip_pool:
    state: present
    name: my-pool
    partition: Common
    lb_method: round-robin
    provider:
      server: lb.mydomain.com
      user: admin
      password: secret
  delegate_to: localhost

- name: Set a single monitor (with enforcement)
  bigip_pool:
    state: present
    name: my-pool
    partition: Common
    monitor_type: single
    monitors:
      - http
    provider:
      server: lb.mydomain.com
      user: admin
      password: secret
  delegate_to: localhost

- name: Set a single monitor (without enforcement)
  bigip_pool:
    state: present
    name: my-pool
    partition: Common
    monitors:
      - http
    provider:
      server: lb.mydomain.com
      user: admin
      password: secret
  delegate_to: localhost

- name: Set multiple monitors (all must succeed)
  bigip_pool:
    state: present
    name: my-pool
    partition: Common
    monitor_type: and_list
    monitors:
      - http
      - tcp
    provider:
      server: lb.mydomain.com
      user: admin
      password: secret
  delegate_to: localhost

- name: Set multiple monitors (at least 1 must succeed)
  bigip_pool:
    state: present
    name: my-pool
    partition: Common
    monitor_type: m_of_n
    quorum: 1
    monitors:
      - http
      - tcp
    provider:
      server: lb.mydomain.com
      user: admin
      password: secret
  delegate_to: localhost

- name: Set multiple monitors (at least 2 must succeed)
  bigip_pool:
    state: present
    name: my-pool
    partition: Common
    availability_requirements_type: m_of_n
    availability_requirements_at_least: 2
    monitors:
      - http
      - tcp
    provider:
      server: lb.mydomain.com
      user: admin
      password: secret
  delegate_to: localhost

- name: Delete pool
  bigip_pool:
    state: absent
    name: my-pool
    partition: Common
    provider:
      server: lb.mydomain.com
      user: admin
      password: secret
  delegate_to: localhost

- name: Add metadata to pool
  bigip_pool:
    state: present
    name: my-pool
    partition: Common
    metadata:
      ansible: 2.4
      updated_at: 2017-12-20T17:50:46Z
    provider:
      server: lb.mydomain.com
      user: admin
      password: secret
  delegate_to: localhost

- name: Add pools Aggregate
  bigip_pool:
    aggregate:
      - name: my-pool
        partition: Common
        lb_method: least-connections-member
        slow_ramp_time: 120
      - name: my-pool2
        partition: Common
        lb_method: least-sessions
        slow_ramp_time: 120
      - name: my-pool3
        partition: Common
        lb_method: round-robin
        slow_ramp_time: 120
    provider:
      server: lb.mydomain.com
      user: admin
      password: secret
  delegate_to: localhost

- name: Add pools Aggregate, purge others
  bigip_pool:
    aggregate:
      - name: my-pool
        partition: Common
        lb_method: least-connections-member
        slow_ramp_time: 120
      - name: my-pool2
        partition: Common
        lb_method: least-sessions
        slow_ramp_time: 120
      - name: my-pool3
        partition: Common
        lb_method: round-robin
        slow_ramp_time: 120
    replace_all_with: true
    provider:
      server: lb.mydomain.com
      user: admin
      password: secret
  delegate_to: localhost
'''

RETURN = r'''
monitor_type:
  description:
    - Changed value for the monitor_type of the pool.
  returned: changed
  type: str
  sample: m_of_n
quorum:
  description: The quorum that was set on the pool.
  returned: changed
  type: int
  sample: 2
monitors:
  description: Monitors set on the pool.
  returned: changed
  type: list
  sample: ['/Common/http', '/Common/gateway_icmp']
service_down_action:
  description: Service down action that is set on the pool.
  returned: changed
  type: str
  sample: reset
description:
  description: Description set on the pool.
  returned: changed
  type: str
  sample: Pool of web servers
lb_method:
  description: The load balancing method set for the pool.
  returned: changed
  type: str
  sample: round-robin
slow_ramp_time:
  description: The new value set for the slow ramp-up time.
  returned: changed
  type: int
  sample: 500
reselect_tries:
  description: The new value set for the number of tries to contact member.
  returned: changed
  type: int
  sample: 10
metadata:
  description: The new value of the pool.
  returned: changed
  type: dict
  sample: {'key1': 'foo', 'key2': 'bar'}
priority_group_activation:
  description: The new minimum number of members to activate the priority group.
  returned: changed
  type: int
  sample: 10
replace_all_with:
  description: Purges all non-aggregate pools from device
  returned: changed
  type: bool
  sample: true
'''

import re
from copy import deepcopy
from datetime import datetime

from ansible.module_utils.urls import urlparse
from ansible.module_utils.basic import (
    AnsibleModule, env_fallback
)
from ansible.module_utils.six import iteritems

from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import remove_default_spec

from ..module_utils.bigip import F5RestClient
from ..module_utils.common import (
    F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, fq_name
)
from ..module_utils.compare import cmp_str_with_none
from ..module_utils.icontrol import (
    TransactionContextManager, tmos_version
)
from ..module_utils.teem import send_teem


class Parameters(AnsibleF5Parameters):
    api_map = {
        'loadBalancingMode': 'lb_method',
        'slowRampTime': 'slow_ramp_time',
        'reselectTries': 'reselect_tries',
        'serviceDownAction': 'service_down_action',
        'monitor': 'monitors',
        'minActiveMembers': 'priority_group_activation',
        'minUpMembers': 'min_up_members',
        'minUpMembersAction': 'min_up_members_action',
        'minUpMembersChecking': 'min_up_members_checking',
    }
    api_attributes = [
        'description',
        'name',
        'loadBalancingMode',
        'monitor',
        'slowRampTime',
        'reselectTries',
        'serviceDownAction',
        'metadata',
        'minActiveMembers',
        'minUpMembers',
        'minUpMembersAction',
        'minUpMembersChecking',
    ]

    returnables = [
        'monitor_type',
        'quorum',
        'monitors',
        'service_down_action',
        'description',
        'lb_method',
        'slow_ramp_time',
        'reselect_tries',
        'monitor',
        'name',
        'partition',
        'metadata',
        'priority_group_activation',
        'min_up_members',
        'min_up_members_action',
        'min_up_members_checking',
    ]

    updatables = [
        'monitor_type',
        'quorum',
        'monitors',
        'service_down_action',
        'description',
        'lb_method',
        'slow_ramp_time',
        'reselect_tries',
        'metadata',
        'priority_group_activation',
        'min_up_members',
        'min_up_members_action',
        'min_up_members_checking',
    ]

    @property
    def lb_method(self):
        lb_method = self._values['lb_method']
        if lb_method is None:
            return None

        spec = ArgumentSpec()
        if lb_method not in spec.lb_choice:
            raise F5ModuleError('Provided lb_method is unknown')
        return lb_method

    def _verify_quorum_type(self, quorum):
        try:
            if quorum is None:
                return None
            return int(quorum)
        except ValueError:
            raise F5ModuleError(
                "The specified 'quorum' must be an integer."
            )

    @property
    def monitors(self):
        if self._values['monitors'] is None:
            return None
        monitors = [fq_name(self.partition, x) for x in self.monitors_list]
        if self.monitor_type == 'm_of_n':
            monitors = ' '.join(monitors)
            result = 'min %s of { %s }' % (self.quorum, monitors)
        else:
            result = ' and '.join(monitors).strip()
        return result

    @property
    def priority_group_activation(self):
        if self._values['priority_group_activation'] is None:
            return None
        return int(self._values['priority_group_activation'])

    @property
    def min_up_members(self):
        if self._values['min_up_members'] is None:
            return None
        return int(self._values['min_up_members'])

    @property
    def min_up_members_action(self):
        if self._values['min_up_members_action'] is None:
            return None
        return self._values['min_up_members_action']

    @property
    def min_up_members_checking(self):
        if self._values['min_up_members_checking'] is None:
            return None
        return self._values['min_up_members_checking']


class ApiParameters(Parameters):
    @property
    def description(self):
        if self._values['description'] in [None, 'none']:
            return None
        return self._values['description']

    @property
    def quorum(self):
        if self._values['monitors'] is None:
            return None
        pattern = r'min\s+(?P<quorum>\d+)\s+of'
        matches = re.search(pattern, self._values['monitors'])
        if matches:
            quorum = matches.group('quorum')
        else:
            quorum = None
        result = self._verify_quorum_type(quorum)
        return result

    @property
    def monitor_type(self):
        if self._values['monitors'] is None:
            return None
        pattern = r'min\s+\d+\s+of'
        matches = re.search(pattern, self._values['monitors'])
        if matches:
            return 'm_of_n'
        else:
            return 'and_list'

    @property
    def monitors_list(self):
        if self._values['monitors'] is None:
            return []
        try:
            result = re.findall(r'/[\w-]+/[^\s}]+', self._values['monitors'])
            return result
        except Exception:
            return self._values['monitors']

    @property
    def metadata(self):
        if self._values['metadata'] is None:
            return None
        result = []
        for md in self._values['metadata']:
            tmp = dict(name=str(md['name']))
            if 'value' in md:
                tmp['value'] = str(md['value'])
            else:
                tmp['value'] = ''
            result.append(tmp)
        return result


class ModuleParameters(Parameters):
    @property
    def description(self):
        if self._values['description'] is None:
            return None
        elif self._values['description'] in ['none', '']:
            return ''
        return self._values['description']

    @property
    def monitors_list(self):
        if self._values['monitors'] is None:
            return []
        return self._values['monitors']

    @property
    def quorum(self):
        if self._values['quorum'] is None:
            return None
        result = self._verify_quorum_type(self._values['quorum'])
        return result

    @property
    def monitor_type(self):
        if self._values['monitor_type'] is None:
            return None
        return self._values['monitor_type']

    @property
    def metadata(self):
        if self._values['metadata'] is None:
            return None
        if self._values['metadata'] == '':
            return []
        result = []
        try:
            for k, v in iteritems(self._values['metadata']):
                tmp = dict(name=str(k))
                if v:
                    tmp['value'] = str(v)
                else:
                    tmp['value'] = ''
                result.append(tmp)
        except AttributeError:
            raise F5ModuleError(
                "The 'metadata' parameter must be a dictionary of key/value pairs."
            )
        return result


class Changes(Parameters):
    def to_return(self):
        result = {}
        for returnable in self.returnables:
            try:
                result[returnable] = getattr(self, returnable)
            except Exception:
                pass
            result = self._filter_params(result)
        return result

    @property
    def monitors(self):
        if self._values['monitors'] is None:
            return None
        return self._values['monitors']


class UsableChanges(Changes):
    @property
    def monitors(self):
        monitor_string = self._values['monitors']
        if monitor_string is None:
            return None

        if '{' in monitor_string and '}' in monitor_string:
            tmp = monitor_string.strip('}').split('{')
            monitor = ''.join(tmp).rstrip()
            return monitor

        return monitor_string


class ReportableChanges(Changes):
    @property
    def monitors(self):
        result = sorted(re.findall(r'/[\w-]+/[^\s}]+', self._values['monitors']))
        return result

    @property
    def monitor_type(self):
        pattern = r'min\s+\d+\s+of'
        matches = re.search(pattern, self._values['monitors'])
        if matches:
            return 'm_of_n'
        else:
            return 'and_list'

    @property
    def metadata(self):
        result = dict()
        for x in self._values['metadata']:
            result[x['name']] = x['value']
        return result


class Difference(object):
    def __init__(self, want, have=None):
        self.want = want
        self.have = have

    def compare(self, param):
        try:
            result = getattr(self, param)
            return result
        except AttributeError:
            return self.__default(param)

    def __default(self, param):
        attr1 = getattr(self.want, param)
        try:
            attr2 = getattr(self.have, param)
            if attr1 != attr2:
                return attr1
        except AttributeError:
            return attr1

    def to_tuple(self, items):
        result = []
        for x in items:
            tmp = [(str(k), str(v)) for k, v in iteritems(x)]
            result += tmp
        return result

    def _diff_complex_items(self, want, have):
        if want == [] and have is None:
            return None
        if want is None:
            return None
        w = self.to_tuple(want)
        h = self.to_tuple(have)
        if set(w).issubset(set(h)):
            return None
        else:
            return want

    @property
    def description(self):
        return cmp_str_with_none(self.want.description, self.have.description)

    def _monitors_and_quorum(self):
        if self.want.monitor_type is None:
            self.want.update(dict(monitor_type=self.have.monitor_type))
        if self.want.monitor_type == 'm_of_n':
            if self.want.quorum is None:
                self.want.update(dict(quorum=self.have.quorum))
            if self.want.quorum is None or self.want.quorum < 1:
                raise F5ModuleError(
                    "Quorum value must be specified with monitor_type 'm_of_n'."
                )
            if self.want.monitors != self.have.monitors:
                if self.want.monitors is None or not self.want.monitors_list:
                    return None
                return dict(
                    monitors=self.want.monitors
                )
        elif self.want.monitor_type == 'and_list':
            if self.want.quorum is not None and self.want.quorum > 0:
                raise F5ModuleError(
                    "Quorum values have no effect when used with 'and_list'."
                )
            if self.want.monitors != self.have.monitors:
                if self.want.monitors is None or not self.want.monitors_list:
                    return None
                return dict(
                    monitors=self.want.monitors
                )
        elif self.want.monitor_type == 'single':
            if len(self.want.monitors_list) > 1:
                raise F5ModuleError(
                    "When using a 'monitor_type' of 'single', only one monitor may be provided."
                )
            elif len(self.have.monitors_list) > 1 and len(self.want.monitors_list) == 0:
                # Handle instances where there already exists many monitors, and the
                # user runs the module again specifying that the monitor_type should be
                # changed to 'single'
                raise F5ModuleError(
                    "A single monitor must be specified if more than one monitor currently exists on your pool."
                )
            # Update to 'and_list' here because the above checks are all that need
            # to be done before we change the value back to what is expected by
            # BIG-IP.
            #
            # Remember that 'single' is nothing more than a fancy way of saying
            # "and_list plus some extra checks"
            self.want.update(dict(monitor_type='and_list'))
        if self.want.monitors != self.have.monitors:
            if self.want.monitors is None or not self.want.monitors_list:
                return None
            return dict(
                monitors=self.want.monitors
            )

    @property
    def monitor_type(self):
        return self._monitors_and_quorum()

    @property
    def quorum(self):
        return self._monitors_and_quorum()

    @property
    def monitors(self):
        if self.want.monitors is None:
            return None
        if not self.want.monitors_list and self.have.monitors is None:
            # Idempotency check - removing monitors from a device where no monitors exists
            return None
        # when monitors_list is [], remove all the monitors
        if not self.want.monitors_list:
            # monitors is '' in the case of monitor_type and_list and min <quorum> of {  } in case monitor_type m_of_n
            return {'monitors': ''}
        if self.want.monitor_type is None:
            self.want.update(dict(monitor_type=self.have.monitor_type))
        if not self.want.monitors and self.want.monitor_type is not None:
            raise F5ModuleError(
                "The 'monitors' parameter cannot be empty when 'monitor_type' parameter is specified"
            )
        if self.want.monitors != self.have.monitors:
            return self.want.monitors

    @property
    def metadata(self):
        if self.want.metadata is None:
            return None
        elif len(self.want.metadata) == 0 and self.have.metadata is None:
            return None
        elif len(self.want.metadata) == 0:
            return []
        elif self.have.metadata is None:
            return self.want.metadata
        result = self._diff_complex_items(self.want.metadata, self.have.metadata)
        return result


class ModuleManager(object):
    def __init__(self, *args, **kwargs):
        self.module = kwargs.get('module', None)
        self.client = F5RestClient(**self.module.params)
        self.want = None
        self.have = None
        self.changes = None
        self.replace_all_with = None
        self.purge_links = list()

    def exec_module(self):
        start = datetime.now().isoformat()
        version = tmos_version(self.client)
        wants = None
        if self.module.params['replace_all_with']:
            self.replace_all_with = True

        if self.module.params['aggregate']:
            wants = self.merge_defaults_for_aggregate(self.module.params)

        result = dict()
        changed = False

        if self.replace_all_with and self.purge_links:
            self.purge()
            changed = True

        if self.module.params['aggregate']:
            result['aggregate'] = list()
            for want in wants:
                output = self.execute(want)
                if output['changed']:
                    changed = output['changed']
                result['aggregate'].append(output)
        else:
            output = self.execute(self.module.params)
            if output['changed']:
                changed = output['changed']
            result.update(output)
        if changed:
            result['changed'] = True
        send_teem(start, self.client, self.module, version)
        return result

    def merge_defaults_for_aggregate(self, params):
        defaults = deepcopy(params)
        aggregate = defaults.pop('aggregate')

        for i, j in enumerate(aggregate):
            for k, v in iteritems(defaults):
                if k != 'replace_all_with':
                    if j.get(k, None) is None and v is not None:
                        aggregate[i][k] = v

        if self.replace_all_with:
            self.compare_aggregate_names(aggregate)

        return aggregate

    def compare_aggregate_names(self, items):
        on_device = self._read_purge_collection()
        if not on_device:
            return False
        aggregates = [item['name'] for item in items]
        collection = [item['name'] for item in on_device]

        diff = set(collection) - set(aggregates)

        if diff:
            to_purge = [item['selfLink'] for item in on_device if item['name'] in diff]
            self.purge_links.extend(to_purge)

    def execute(self, params=None):
        self.want = ModuleParameters(params=params)
        self.have = ApiParameters()
        self.changes = UsableChanges()

        changed = False
        result = dict()
        state = params['state']

        if state == "present":
            changed = self.present()
        elif state == "absent":
            changed = self.absent()

        reportable = ReportableChanges(params=self.changes.to_return())
        changes = reportable.to_return()
        result.update(**changes)
        result.update(dict(changed=changed))
        self._announce_deprecations(result)
        return result

    def _announce_deprecations(self, result):
        warnings = result.pop('__warnings', [])
        for warning in warnings:
            self.module.deprecate(
                msg=warning['msg'],
                version=warning['version']
            )

    def _set_changed_options(self):
        changed = {}
        for key in Parameters.returnables:
            if getattr(self.want, key) is not None:
                changed[key] = getattr(self.want, key)
        if changed:
            self.changes = UsableChanges(params=changed)

    def _update_changed_options(self):
        diff = Difference(self.want, self.have)
        updatables = Parameters.updatables
        changed = dict()
        for k in updatables:
            change = diff.compare(k)
            if change is None:
                continue
            else:
                if isinstance(change, dict):
                    changed.update(change)
                else:
                    changed[k] = change
        if changed:
            self.changes = UsableChanges(params=changed)
            return True
        return False

    def present(self):
        if self.exists():
            return self.update()
        else:
            return self.create()

    def absent(self):
        if self.exists():
            return self.remove()
        return False

    def should_update(self):
        result = self._update_changed_options()
        if result:
            return True
        return False

    def update(self):
        self.have = self.read_current_from_device()
        if not self.should_update():
            return False
        if self.module.check_mode:
            return True
        self.update_on_device()
        return True

    def remove(self):
        if self.module.check_mode:
            return True
        self.remove_from_device()
        if self.exists():
            raise F5ModuleError("Failed to delete the Pool")
        return True

    def purge(self):
        if self.module.check_mode:
            return True
        self.purge_from_device()
        return True

    def create(self):
        if self.want.monitor_type is not None:
            if not self.want.monitors_list:
                raise F5ModuleError(
                    "The 'monitors' parameter cannot be empty when 'monitor_type' parameter is specified"
                )
        else:
            if self.want.monitor_type is None:
                self.want.update(dict(monitor_type='and_list'))

        if self.want.monitor_type == 'm_of_n' and (self.want.quorum is None or self.want.quorum < 1):
            raise F5ModuleError(
                "Quorum value must be specified with monitor_type 'm_of_n'."
            )
        elif self.want.monitor_type == 'and_list' and self.want.quorum is not None and self.want.quorum > 0:
            raise F5ModuleError(
                "Quorum values have no effect when used with 'and_list'."
            )
        elif self.want.monitor_type == 'single' and len(self.want.monitors_list) > 1:
            raise F5ModuleError(
                "When using a 'monitor_type' of 'single', only one monitor may be provided"
            )
        if self.want.priority_group_activation is None:
            self.want.update({'priority_group_activation': 0})

        if self.want.min_up_members is None:
            self.want.update({'min_up_members': 0})

        if self.want.min_up_members_action is None:
            self.want.update({'min_up_members_action': 'failover'})

        if self.want.min_up_members_checking is None:
            self.want.update({'min_up_members_checking': 'disabled'})

        self._set_changed_options()
        if self.module.check_mode:
            return True
        self.create_on_device()
        return True

    def _read_purge_collection(self):
        uri = "https://{0}:{1}/mgmt/tm/ltm/pool/".format(
            self.client.provider['server'],
            self.client.provider['server_port'],
        )

        query = "?$select=name,selfLink"
        resp = self.client.api.get(uri + query)

        try:
            response = resp.json()
        except ValueError as ex:
            raise F5ModuleError(str(ex))

        if 'code' in response and response['code'] == 400:
            if 'message' in response:
                raise F5ModuleError(response['message'])
            else:
                raise F5ModuleError(resp.content)
        if 'items' in response:
            return response['items']
        return []

    def create_on_device(self):
        params = self.changes.api_params()
        params['name'] = self.want.name
        params['partition'] = self.want.partition
        uri = "https://{0}:{1}/mgmt/tm/ltm/pool/".format(
            self.client.provider['server'],
            self.client.provider['server_port']
        )
        resp = self.client.api.post(uri, json=params)
        try:
            response = resp.json()
        except ValueError as ex:
            raise F5ModuleError(str(ex))

        if 'code' in response and response['code'] in [400, 403]:
            if 'message' in response:
                raise F5ModuleError(response['message'])
            else:
                raise F5ModuleError(resp.content)

    def update_on_device(self):
        params = self.changes.api_params()
        uri = "https://{0}:{1}/mgmt/tm/ltm/pool/{2}".format(
            self.client.provider['server'],
            self.client.provider['server_port'],
            transform_name(self.want.partition, self.want.name)
        )
        resp = self.client.api.patch(uri, json=params)
        try:
            response = resp.json()
        except ValueError as ex:
            raise F5ModuleError(str(ex))

        if 'code' in response and response['code'] == 400:
            if 'message' in response:
                raise F5ModuleError(response['message'])
            else:
                raise F5ModuleError(resp.content)

    def exists(self):
        uri = "https://{0}:{1}/mgmt/tm/ltm/pool/{2}".format(
            self.client.provider['server'],
            self.client.provider['server_port'],
            transform_name(self.want.partition, self.want.name)
        )
        resp = self.client.api.get(uri)
        try:
            response = resp.json()
        except ValueError as ex:
            raise F5ModuleError(str(ex))

        if resp.status == 404 or 'code' in response and response['code'] == 404:
            return False
        if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]:
            return True

        errors = [401, 403, 409, 500, 501, 502, 503, 504]

        if resp.status in errors or 'code' in response and response['code'] in errors:
            if 'message' in response:
                raise F5ModuleError(response['message'])
            else:
                raise F5ModuleError(resp.content)

    def remove_from_device(self):
        uri = "https://{0}:{1}/mgmt/tm/ltm/pool/{2}".format(
            self.client.provider['server'],
            self.client.provider['server_port'],
            transform_name(self.want.partition, self.want.name)
        )
        response = self.client.api.delete(uri)
        if response.status == 200:
            return True
        raise F5ModuleError(response.content)

    def read_current_from_device(self):
        uri = "https://{0}:{1}/mgmt/tm/ltm/pool/{2}".format(
            self.client.provider['server'],
            self.client.provider['server_port'],
            transform_name(self.want.partition, self.want.name)
        )
        query = '?expandSubcollections=true'
        resp = self.client.api.get(uri + query)
        try:
            response = resp.json()
        except ValueError as ex:
            raise F5ModuleError(str(ex))

        if 'code' in response and response['code'] == 400:
            if 'message' in response:
                raise F5ModuleError(response['message'])
            else:
                raise F5ModuleError(resp.content)
        return ApiParameters(params=response)

    def _prepare_links(self, collection):
        purge_links = list()
        purge_paths = [urlparse(link).path for link in collection]

        for path in purge_paths:
            link = "https://{0}:{1}{2}".format(
                self.client.provider['server'],
                self.client.provider['server_port'],
                path
            )
            purge_links.append(link)
        return purge_links

    def purge_from_device(self):
        links = self._prepare_links(self.purge_links)

        with TransactionContextManager(self.client) as transact:
            for link in links:
                resp = transact.api.delete(link)

                try:
                    response = resp.json()
                except ValueError as ex:
                    raise F5ModuleError(str(ex))

                if 'code' in response and response['code'] == 400:
                    if 'message' in response:
                        raise F5ModuleError(response['message'])
                    else:
                        raise F5ModuleError(resp.content)
        return True


class ArgumentSpec(object):
    def __init__(self):
        self.lb_choice = [
            'dynamic-ratio-member',
            'dynamic-ratio-node',
            'fastest-app-response',
            'fastest-node',
            'least-connections-member',
            'least-connections-node',
            'least-sessions',
            'observed-member',
            'observed-node',
            'predictive-member',
            'predictive-node',
            'ratio-least-connections-member',
            'ratio-least-connections-node',
            'ratio-member',
            'ratio-node',
            'ratio-session',
            'round-robin',
            'weighted-least-connections-member',
            'weighted-least-connections-node'
        ]
        self.supports_check_mode = True
        element_spec = dict(
            name=dict(
                aliases=['pool']
            ),
            lb_method=dict(
                choices=self.lb_choice
            ),
            monitor_type=dict(
                choices=[
                    'and_list', 'm_of_n', 'single'
                ],
                aliases=['availability_requirements_type']
            ),
            quorum=dict(
                type='int',
                aliases=['availability_requirements_at_least']
            ),
            monitors=dict(
                type='list',
                elements='str',
            ),
            slow_ramp_time=dict(
                type='int'
            ),
            reselect_tries=dict(
                type='int'
            ),
            service_down_action=dict(
                choices=[
                    'none', 'reset',
                    'drop', 'reselect'
                ]
            ),
            description=dict(),
            metadata=dict(type='raw'),
            priority_group_activation=dict(
                type='int',
                aliases=['minimum_active_members']
            ),
            min_up_members=dict(
                type='int'
            ),
            min_up_members_action=dict(
                choices=['failover', 'reboot', 'restart-all']
            ),
            min_up_members_checking=dict(
                choices=['enabled', 'disabled']
            ),
            state=dict(
                default='present',
                choices=['present', 'absent']
            ),
            partition=dict(
                default='Common',
                fallback=(env_fallback, ['F5_PARTITION'])
            )
        )

        aggregate_spec = deepcopy(element_spec)

        remove_default_spec(aggregate_spec)
        aggregate_spec["state"].update(default="present")
        aggregate_spec["partition"].update(default="Common")

        argument_spec = dict(
            aggregate=dict(
                type='list',
                elements='dict',
                options=aggregate_spec,
                aliases=['pools']
            ),
            partition=dict(
                default='Common',
                fallback=(env_fallback, ['F5_PARTITION'])
            ),
            replace_all_with=dict(
                default='no',
                type='bool',
                aliases=['purge']
            )
        )

        self.mutually_exclusive = [
            ['name', 'aggregate']
        ]
        self.required_one_of = [
            ['name', 'aggregate']
        ]

        self.argument_spec = {}
        self.argument_spec.update(element_spec)
        self.argument_spec.update(f5_argument_spec)
        self.argument_spec.update(argument_spec)


def main():
    spec = ArgumentSpec()

    module = AnsibleModule(
        argument_spec=spec.argument_spec,
        supports_check_mode=spec.supports_check_mode,
        mutually_exclusive=spec.mutually_exclusive,
        required_one_of=spec.required_one_of
    )

    try:
        mm = ModuleManager(module=module)
        results = mm.exec_module()
        module.exit_json(**results)
    except F5ModuleError as ex:
        module.fail_json(msg=str(ex))


if __name__ == '__main__':
    main()
