#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright (c) 2017 Ansible Project
# 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)

DOCUMENTATION = '''
---
module: spotinst_aws_elastigroup
short_description: Create, update or delete Spotinst AWS Elastigroups
author: Spotinst (@talzur)
description:
  - Can create, update, or delete Spotinst AWS Elastigroups
    Launch configuration is part of the elastigroup configuration,
    so no additional modules are necessary for handling the launch configuration.
    You will have to have a credentials file in this location - <home>/.spotinst/credentials
    The credentials file must contain a row that looks like this
    token = <YOUR TOKEN>
    Full documentation available at https://help.spotinst.com/hc/en-us/articles/115003530285-Ansible-
requirements:
  - python >= 2.7
  - spotinst_sdk >= 1.0.38
options:

  credentials_path:
    description:
      - Optional parameter that allows to set a non-default credentials path.
    default: ~/.spotinst/credentials
    type: path

  account_id:
    description:
      - Optional parameter that allows to set an account-id inside the module configuration.
        By default this is retrieved from the credentials path.
    type: str

  availability_vs_cost:
    description:
      - The strategy orientation.
      - "The choices available are: C(availabilityOriented), C(costOriented), C(balanced)."
    required: true
    type: str

  availability_zones:
    description:
      - A list of hash/dictionaries of Availability Zones that are configured in the elastigroup;
        '[{"key":"value", "key":"value"}]';
        keys allowed are
        name (String),
        subnet_id (String),
        placement_group_name (String),
    required: true
    type: list
    elements: dict

  block_device_mappings:
    description:
      - A list of hash/dictionaries of Block Device Mappings for elastigroup instances;
        You can specify virtual devices and EBS volumes.;
        '[{"key":"value", "key":"value"}]';
        keys allowed are
        device_name (List of Strings),
        virtual_name (String),
        no_device (String),
        ebs (Object, expects the following keys-
        delete_on_termination(Boolean),
        encrypted(Boolean),
        iops (Integer),
        snapshot_id(Integer),
        volume_type(String),
        volume_size(Integer))
    type: list
    elements: dict

  chef:
    description:
      - The Chef integration configuration.;
        Expects the following keys - chef_server (String),
        organization (String),
        user (String),
        pem_key (String),
        chef_version (String)
    type: dict

  draining_timeout:
    description:
      - Time for instance to be drained from incoming requests and deregistered from ELB before termination.
    type: int

  ebs_optimized:
    description:
      - Enable EBS optimization for supported instances which are not enabled by default.;
        Note - additional charges will be applied.
    type: bool

  ebs_volume_pool:
    description:
      - A list of hash/dictionaries of EBS devices to reattach to the elastigroup when available;
        '[{"key":"value", "key":"value"}]';
        keys allowed are -
        volume_ids (List of Strings),
        device_name (String)
    type: list
    elements: dict

  ecs:
    description:
      - The ECS integration configuration.;
        Expects the following key -
        cluster_name (String)
    type: dict

  elastic_ips:
    description:
      - List of ElasticIps Allocation Ids (Example C(eipalloc-9d4e16f8)) to associate to the group instances
    type: list
    elements: str

  fallback_to_od:
    description:
      - In case of no spots available, Elastigroup will launch an On-demand instance instead
    type: bool

  health_check_grace_period:
    description:
      - The amount of time, in seconds, after the instance has launched to start and check its health.
      - If not specified, it defaults to C(300).
    type: int

  health_check_unhealthy_duration_before_replacement:
    description:
      - Minimal mount of time instance should be unhealthy for us to consider it unhealthy.
    type: int

  health_check_type:
    description:
      - The service to use for the health check.
      - "The choices available are: C(ELB), C(HCS), C(TARGET_GROUP), C(MLB), C(EC2)."
    type: str

  iam_role_name:
    description:
      - The instance profile iamRole name
      - Only use iam_role_arn, or iam_role_name
    type: str

  iam_role_arn:
    description:
      - The instance profile iamRole arn
      - Only use iam_role_arn, or iam_role_name
    type: str

  id:
    description:
      - The group id if it already exists and you want to update, or delete it.
        This will not work unless the uniqueness_by field is set to id.
        When this is set, and the uniqueness_by field is set, the group will either be updated or deleted, but not created.
    type: str

  image_id:
    description:
      - The image Id used to launch the instance.;
        In case of conflict between Instance type and image type, an error will be returned
    required: true
    type: str

  key_pair:
    description:
      - Specify a Key Pair to attach to the instances
    type: str

  kubernetes:
    description:
      - The Kubernetes integration configuration.
        Expects the following keys -
        api_server (String),
        token (String)
    type: dict

  lifetime_period:
    description:
      - Lifetime period
    type: int

  load_balancers:
    description:
      - List of classic ELB names
    type: list
    elements: str

  max_size:
    description:
      - The upper limit number of instances that you can scale up to
    required: true
    type: int

  mesosphere:
    description:
      - The Mesosphere integration configuration.
        Expects the following key -
        api_server (String)
    type: dict

  min_size:
    description:
      - The lower limit number of instances that you can scale down to
    required: true
    type: int

  monitoring:
    description:
      - Describes whether instance Enhanced Monitoring is enabled
    type: str

  name:
    description:
      - Unique name for elastigroup to be created, updated or deleted
    required: true
    type: str

  network_interfaces:
    description:
      - A list of hash/dictionaries of network interfaces to add to the elastigroup;
        '[{"key":"value", "key":"value"}]';
        keys allowed are -
        description (String),
        device_index (Integer),
        secondary_private_ip_address_count (Integer),
        associate_public_ip_address (Boolean),
        delete_on_termination (Boolean),
        groups (List of Strings),
        network_interface_id (String),
        private_ip_address (String),
        subnet_id (String),
        associate_ipv6_address (Boolean),
        private_ip_addresses (List of Objects, Keys are privateIpAddress (String, required) and primary (Boolean))
    type: list
    elements: dict

  on_demand_count:
    description:
      - Required if risk is not set
      - Number of on demand instances to launch. All other instances will be spot instances.;
        Either set this parameter or the risk parameter
    type: int

  on_demand_instance_type:
    description:
      - On-demand instance type that will be provisioned
    type: str

  opsworks:
    description:
      - The elastigroup OpsWorks integration configration.;
        Expects the following key -
        layer_id (String)
    type: dict

  persistence:
    description:
      - The Stateful elastigroup configration.;
        Accepts the following keys -
        should_persist_root_device (Boolean),
        should_persist_block_devices (Boolean),
        should_persist_private_ip (Boolean)
    type: dict

  product:
    description:
      - Operation system type.
      - "Available choices are: C(Linux/UNIX), C(SUSE Linux), C(Windows), C(Linux/UNIX (Amazon VPC)), C(SUSE Linux (Amazon VPC))."
    required: true
    type: str

  rancher:
    description:
      - The Rancher integration configuration.;
        Expects the following keys -
        version (String),
        access_key (String),
        secret_key (String),
        master_host (String)
    type: dict

  right_scale:
    description:
      - The Rightscale integration configuration.;
        Expects the following keys -
        account_id (String),
        refresh_token (String)
    type: dict

  risk:
    description:
      - Required if on demand is not set. The percentage of Spot instances to launch (0 - 100).
    type: int

  roll_config:
    description:
      - Roll configuration.;
        If you would like the group to roll after updating, please use this feature.
        Accepts the following keys -
        batch_size_percentage(Integer, Required),
        grace_period - (Integer, Required),
        health_check_type(String, Optional)
    type: dict

  scheduled_tasks:
    description:
      - A list of hash/dictionaries of scheduled tasks to configure in the elastigroup;
        '[{"key":"value", "key":"value"}]';
        keys allowed are -
        adjustment (Integer),
        scale_target_capacity (Integer),
        scale_min_capacity (Integer),
        scale_max_capacity (Integer),
        adjustment_percentage (Integer),
        batch_size_percentage (Integer),
        cron_expression (String),
        frequency (String),
        grace_period (Integer),
        task_type (String, required),
        is_enabled (Boolean)
    type: list
    elements: dict

  security_group_ids:
    description:
      - One or more security group IDs. ;
        In case of update it will override the existing Security Group with the new given array
    required: true
    type: list
    elements: str

  shutdown_script:
    description:
      - The Base64-encoded shutdown script that executes prior to instance termination.
        Encode before setting.
    type: str

  signals:
    description:
      - A list of hash/dictionaries of signals to configure in the elastigroup;
        keys allowed are -
        name (String, required),
        timeout (Integer)
    type: list
    elements: dict

  spin_up_time:
    description:
      - Spin up time, in seconds, for the instance
    type: int

  spot_instance_types:
    description:
      - Spot instance type that will be provisioned.
    required: true
    type: list
    elements: str

  state:
    choices:
      - present
      - absent
    description:
      - Create or delete the elastigroup
    default: present
    type: str

  tags:
    description:
      - A list of tags to configure in the elastigroup. Please specify list of keys and values (key colon value);
    type: list
    elements: dict

  target:
    description:
      - The number of instances to launch
    required: true
    type: int

  target_group_arns:
    description:
      - List of target group arns instances should be registered to
    type: list
    elements: str

  tenancy:
    description:
      - Dedicated vs shared tenancy.
      - "The available choices are: C(default), C(dedicated)."
    type: str

  terminate_at_end_of_billing_hour:
    description:
      - Terminate at the end of billing hour
    type: bool

  unit:
    description:
      - The capacity unit to launch instances by.
      - "The available choices are: C(instance), C(weight)."
    type: str

  up_scaling_policies:
    description:
      - A list of hash/dictionaries of scaling policies to configure in the elastigroup;
        '[{"key":"value", "key":"value"}]';
        keys allowed are -
        policy_name (String, required),
        namespace (String, required),
        metric_name (String, required),
        dimensions (List of Objects, Keys allowed are name (String, required) and value (String)),
        statistic (String, required)
        evaluation_periods (String, required),
        period (String, required),
        threshold (String, required),
        cooldown (String, required),
        unit (String, required),
        operator (String, required),
        action_type (String, required),
        adjustment (String),
        min_target_capacity (String),
        target (String),
        maximum (String),
        minimum (String)
    type: list
    elements: dict

  down_scaling_policies:
    description:
      - A list of hash/dictionaries of scaling policies to configure in the elastigroup;
        '[{"key":"value", "key":"value"}]';
        keys allowed are -
        policy_name (String, required),
        namespace (String, required),
        metric_name (String, required),
        dimensions ((List of Objects), Keys allowed are name (String, required) and value (String)),
        statistic (String, required),
        evaluation_periods (String, required),
        period (String, required),
        threshold (String, required),
        cooldown (String, required),
        unit (String, required),
        operator (String, required),
        action_type (String, required),
        adjustment (String),
        max_target_capacity (String),
        target (String),
        maximum (String),
        minimum (String)
    type: list
    elements: dict

  target_tracking_policies:
    description:
      - A list of hash/dictionaries of target tracking policies to configure in the elastigroup;
        '[{"key":"value", "key":"value"}]';
        keys allowed are -
        policy_name (String, required),
        namespace (String, required),
        source (String, required),
        metric_name (String, required),
        statistic (String, required),
        unit (String, required),
        cooldown (String, required),
        target (String, required)
    type: list
    elements: dict

  uniqueness_by:
    choices:
      - id
      - name
    description:
      - If your group names are not unique, you may use this feature to update or delete a specific group.
        Whenever this property is set, you must set a group_id in order to update or delete a group, otherwise a group will be created.
    default: name
    type: str

  user_data:
    description:
      - Base64-encoded MIME user data. Encode before setting the value.
    type: str

  utilize_reserved_instances:
    description:
      - In case of any available Reserved Instances,
         Elastigroup will utilize your reservations before purchasing Spot instances.
    type: bool

  wait_for_instances:
    description:
      - Whether or not the elastigroup creation / update actions should wait for the instances to spin
    type: bool
    default: false

  wait_timeout:
    description:
      - How long the module should wait for instances before failing the action.;
        Only works if wait_for_instances is True.
    type: int

'''
EXAMPLES = '''
# Basic configuration YAML example

- hosts: localhost
  tasks:
    - name: Create elastigroup
      community.general.spotinst_aws_elastigroup:
          state: present
          risk: 100
          availability_vs_cost: balanced
          availability_zones:
            - name: us-west-2a
              subnet_id: subnet-2b68a15c
          image_id: ami-f173cc91
          key_pair: spotinst-oregon
          max_size: 15
          min_size: 0
          target: 0
          unit: instance
          monitoring: True
          name: ansible-group
          on_demand_instance_type: c3.large
          product: Linux/UNIX
          load_balancers:
            - test-lb-1
          security_group_ids:
            - sg-8f4b8fe9
          spot_instance_types:
            - c3.large
          do_not_update:
            - image_id
            - target
      register: result
    - ansible.builtin.debug: var=result

# In this example, we create an elastigroup and wait 600 seconds to retrieve the instances, and use their private ips

- hosts: localhost
  tasks:
    - name: Create elastigroup
      community.general.spotinst_aws_elastigroup:
          state: present
          account_id: act-1a9dd2b
          risk: 100
          availability_vs_cost: balanced
          availability_zones:
            - name: us-west-2a
              subnet_id: subnet-2b68a15c
          tags:
            - Environment: someEnvValue
            - OtherTagKey: otherValue
          image_id: ami-f173cc91
          key_pair: spotinst-oregon
          max_size: 5
          min_size: 0
          target: 0
          unit: instance
          monitoring: True
          name: ansible-group-tal
          on_demand_instance_type: c3.large
          product: Linux/UNIX
          security_group_ids:
            - sg-8f4b8fe9
          block_device_mappings:
            - device_name: '/dev/sda1'
              ebs:
                volume_size: 100
                volume_type: gp2
          spot_instance_types:
            - c3.large
          do_not_update:
            - image_id
          wait_for_instances: True
          wait_timeout: 600
      register: result

    - name: Store private ips to file
      ansible.builtin.shell: echo {{ item.private_ip }}\\n >> list-of-private-ips
      with_items: "{{ result.instances }}"
    - ansible.builtin.debug: var=result

# In this example, we create an elastigroup with multiple block device mappings, tags, and also an account id
# In organizations with more than one account, it is required to specify an account_id

- hosts: localhost
  tasks:
    - name: Create elastigroup
      community.general.spotinst_aws_elastigroup:
          state: present
          account_id: act-1a9dd2b
          risk: 100
          availability_vs_cost: balanced
          availability_zones:
            - name: us-west-2a
              subnet_id: subnet-2b68a15c
          tags:
            - Environment: someEnvValue
            - OtherTagKey: otherValue
          image_id: ami-f173cc91
          key_pair: spotinst-oregon
          max_size: 5
          min_size: 0
          target: 0
          unit: instance
          monitoring: True
          name: ansible-group-tal
          on_demand_instance_type: c3.large
          product: Linux/UNIX
          security_group_ids:
            - sg-8f4b8fe9
          block_device_mappings:
            - device_name: '/dev/xvda'
              ebs:
                volume_size: 60
                volume_type: gp2
            - device_name: '/dev/xvdb'
              ebs:
                volume_size: 120
                volume_type: gp2
          spot_instance_types:
            - c3.large
          do_not_update:
            - image_id
          wait_for_instances: True
          wait_timeout: 600
      register: result

    - name: Store private ips to file
      ansible.builtin.shell: echo {{ item.private_ip }}\\n >> list-of-private-ips
      with_items: "{{ result.instances }}"
    - ansible.builtin.debug: var=result

# In this example we have set up block device mapping with ephemeral devices

- hosts: localhost
  tasks:
    - name: Create elastigroup
      community.general.spotinst_aws_elastigroup:
          state: present
          risk: 100
          availability_vs_cost: balanced
          availability_zones:
            - name: us-west-2a
              subnet_id: subnet-2b68a15c
          image_id: ami-f173cc91
          key_pair: spotinst-oregon
          max_size: 15
          min_size: 0
          target: 0
          unit: instance
          block_device_mappings:
            - device_name: '/dev/xvda'
              virtual_name: ephemeral0
            - device_name: '/dev/xvdb/'
              virtual_name: ephemeral1
          monitoring: True
          name: ansible-group
          on_demand_instance_type: c3.large
          product: Linux/UNIX
          load_balancers:
            - test-lb-1
          security_group_ids:
            - sg-8f4b8fe9
          spot_instance_types:
            - c3.large
          do_not_update:
            - image_id
            - target
      register: result
    - ansible.builtin.debug: var=result

# In this example we create a basic group configuration with a network interface defined.
# Each network interface must have a device index

- hosts: localhost
  tasks:
    - name: Create elastigroup
      community.general.spotinst_aws_elastigroup:
          state: present
          risk: 100
          availability_vs_cost: balanced
          network_interfaces:
            - associate_public_ip_address: true
              device_index: 0
          availability_zones:
            - name: us-west-2a
              subnet_id: subnet-2b68a15c
          image_id: ami-f173cc91
          key_pair: spotinst-oregon
          max_size: 15
          min_size: 0
          target: 0
          unit: instance
          monitoring: True
          name: ansible-group
          on_demand_instance_type: c3.large
          product: Linux/UNIX
          load_balancers:
            - test-lb-1
          security_group_ids:
            - sg-8f4b8fe9
          spot_instance_types:
            - c3.large
          do_not_update:
            - image_id
            - target
      register: result
    - ansible.builtin.debug: var=result


# In this example we create a basic group configuration with a target tracking scaling policy defined

- hosts: localhost
  tasks:
    - name: Create elastigroup
      community.general.spotinst_aws_elastigroup:
          account_id: act-92d45673
          state: present
          risk: 100
          availability_vs_cost: balanced
          availability_zones:
            - name: us-west-2a
              subnet_id: subnet-79da021e
          image_id: ami-f173cc91
          fallback_to_od: true
          tags:
            - Creator: ValueOfCreatorTag
            - Environment: ValueOfEnvironmentTag
          key_pair: spotinst-labs-oregon
          max_size: 10
          min_size: 0
          target: 2
          unit: instance
          monitoring: True
          name: ansible-group-1
          on_demand_instance_type: c3.large
          product: Linux/UNIX
          security_group_ids:
            - sg-46cdc13d
          spot_instance_types:
            - c3.large
          target_tracking_policies:
            - policy_name: target-tracking-1
              namespace: AWS/EC2
              metric_name: CPUUtilization
              statistic: average
              unit: percent
              target: 50
              cooldown: 120
          do_not_update:
            - image_id
      register: result
    - ansible.builtin.debug: var=result
'''

RETURN = '''
---
instances:
    description: List of active elastigroup instances and their details.
    returned: success
    type: dict
    sample: [
         {
            "spotInstanceRequestId": "sir-regs25zp",
            "instanceId": "i-09640ad8678234c",
            "instanceType": "m4.large",
            "product": "Linux/UNIX",
            "availabilityZone": "us-west-2b",
            "privateIp": "180.0.2.244",
            "createdAt": "2017-07-17T12:46:18.000Z",
            "status": "fulfilled"
        }
    ]
group_id:
    description: Created / Updated group's ID.
    returned: success
    type: str
    sample: "sig-12345"

'''

HAS_SPOTINST_SDK = False
__metaclass__ = type

import os
import time
from ansible.module_utils.basic import AnsibleModule

try:
    import spotinst_sdk as spotinst
    from spotinst_sdk import SpotinstClientException

    HAS_SPOTINST_SDK = True

except ImportError:
    pass

eni_fields = ('description',
              'device_index',
              'secondary_private_ip_address_count',
              'associate_public_ip_address',
              'delete_on_termination',
              'groups',
              'network_interface_id',
              'private_ip_address',
              'subnet_id',
              'associate_ipv6_address')

private_ip_fields = ('private_ip_address',
                     'primary')

capacity_fields = (dict(ansible_field_name='min_size',
                        spotinst_field_name='minimum'),
                   dict(ansible_field_name='max_size',
                        spotinst_field_name='maximum'),
                   'target',
                   'unit')

lspec_fields = ('user_data',
                'key_pair',
                'tenancy',
                'shutdown_script',
                'monitoring',
                'ebs_optimized',
                'image_id',
                'health_check_type',
                'health_check_grace_period',
                'health_check_unhealthy_duration_before_replacement',
                'security_group_ids')

iam_fields = (dict(ansible_field_name='iam_role_name',
                   spotinst_field_name='name'),
              dict(ansible_field_name='iam_role_arn',
                   spotinst_field_name='arn'))

scheduled_task_fields = ('adjustment',
                         'adjustment_percentage',
                         'batch_size_percentage',
                         'cron_expression',
                         'frequency',
                         'grace_period',
                         'task_type',
                         'is_enabled',
                         'scale_target_capacity',
                         'scale_min_capacity',
                         'scale_max_capacity')

scaling_policy_fields = ('policy_name',
                         'namespace',
                         'metric_name',
                         'dimensions',
                         'statistic',
                         'evaluation_periods',
                         'period',
                         'threshold',
                         'cooldown',
                         'unit',
                         'operator')

tracking_policy_fields = ('policy_name',
                          'namespace',
                          'source',
                          'metric_name',
                          'statistic',
                          'unit',
                          'cooldown',
                          'target',
                          'threshold')

action_fields = (dict(ansible_field_name='action_type',
                      spotinst_field_name='type'),
                 'adjustment',
                 'min_target_capacity',
                 'max_target_capacity',
                 'target',
                 'minimum',
                 'maximum')

signal_fields = ('name',
                 'timeout')

multai_lb_fields = ('balancer_id',
                    'project_id',
                    'target_set_id',
                    'az_awareness',
                    'auto_weight')

persistence_fields = ('should_persist_root_device',
                      'should_persist_block_devices',
                      'should_persist_private_ip')

strategy_fields = ('risk',
                   'utilize_reserved_instances',
                   'fallback_to_od',
                   'on_demand_count',
                   'availability_vs_cost',
                   'draining_timeout',
                   'spin_up_time',
                   'lifetime_period')

ebs_fields = ('delete_on_termination',
              'encrypted',
              'iops',
              'snapshot_id',
              'volume_type',
              'volume_size')

bdm_fields = ('device_name',
              'virtual_name',
              'no_device')

kubernetes_fields = ('api_server',
                     'token')

right_scale_fields = ('account_id',
                      'refresh_token')

rancher_fields = ('access_key',
                  'secret_key',
                  'master_host',
                  'version')

chef_fields = ('chef_server',
               'organization',
               'user',
               'pem_key',
               'chef_version')

az_fields = ('name',
             'subnet_id',
             'placement_group_name')

opsworks_fields = ('layer_id',)

scaling_strategy_fields = ('terminate_at_end_of_billing_hour',)

mesosphere_fields = ('api_server',)

ecs_fields = ('cluster_name',)

multai_fields = ('multai_token',)


def handle_elastigroup(client, module):
    has_changed = False
    group_id = None
    message = 'None'

    name = module.params.get('name')
    state = module.params.get('state')
    uniqueness_by = module.params.get('uniqueness_by')
    external_group_id = module.params.get('id')

    if uniqueness_by == 'id':
        if external_group_id is None:
            should_create = True
        else:
            should_create = False
            group_id = external_group_id
    else:
        groups = client.get_elastigroups()
        should_create, group_id = find_group_with_same_name(groups, name)

    if should_create is True:
        if state == 'present':
            eg = expand_elastigroup(module, is_update=False)
            module.debug(str(" [INFO] " + message + "\n"))
            group = client.create_elastigroup(group=eg)
            group_id = group['id']
            message = 'Created group Successfully.'
            has_changed = True

        elif state == 'absent':
            message = 'Cannot delete non-existent group.'
            has_changed = False
    else:
        eg = expand_elastigroup(module, is_update=True)

        if state == 'present':
            group = client.update_elastigroup(group_update=eg, group_id=group_id)
            message = 'Updated group successfully.'

            try:
                roll_config = module.params.get('roll_config')
                if roll_config:
                    eg_roll = spotinst.aws_elastigroup.Roll(
                        batch_size_percentage=roll_config.get('batch_size_percentage'),
                        grace_period=roll_config.get('grace_period'),
                        health_check_type=roll_config.get('health_check_type')
                    )
                    roll_response = client.roll_group(group_roll=eg_roll, group_id=group_id)
                    message = 'Updated and started rolling the group successfully.'

            except SpotinstClientException as exc:
                message = 'Updated group successfully, but failed to perform roll. Error:' + str(exc)
            has_changed = True

        elif state == 'absent':
            try:
                client.delete_elastigroup(group_id=group_id)
            except SpotinstClientException as exc:
                if "GROUP_DOESNT_EXIST" in exc.message:
                    pass
                else:
                    module.fail_json(msg="Error while attempting to delete group : " + exc.message)

            message = 'Deleted group successfully.'
            has_changed = True

    return group_id, message, has_changed


def retrieve_group_instances(client, module, group_id):
    wait_timeout = module.params.get('wait_timeout')
    wait_for_instances = module.params.get('wait_for_instances')

    health_check_type = module.params.get('health_check_type')

    if wait_timeout is None:
        wait_timeout = 300

    wait_timeout = time.time() + wait_timeout
    target = module.params.get('target')
    state = module.params.get('state')
    instances = list()

    if state == 'present' and group_id is not None and wait_for_instances is True:

        is_amount_fulfilled = False
        while is_amount_fulfilled is False and wait_timeout > time.time():
            instances = list()
            amount_of_fulfilled_instances = 0

            if health_check_type is not None:
                healthy_instances = client.get_instance_healthiness(group_id=group_id)

                for healthy_instance in healthy_instances:
                    if healthy_instance.get('healthStatus') == 'HEALTHY':
                        amount_of_fulfilled_instances += 1
                        instances.append(healthy_instance)

            else:
                active_instances = client.get_elastigroup_active_instances(group_id=group_id)

                for active_instance in active_instances:
                    if active_instance.get('private_ip') is not None:
                        amount_of_fulfilled_instances += 1
                        instances.append(active_instance)

            if amount_of_fulfilled_instances >= target:
                is_amount_fulfilled = True

            time.sleep(10)

    return instances


def find_group_with_same_name(groups, name):
    for group in groups:
        if group['name'] == name:
            return False, group.get('id')

    return True, None


def expand_elastigroup(module, is_update):
    do_not_update = module.params['do_not_update']
    name = module.params.get('name')

    eg = spotinst.aws_elastigroup.Elastigroup()
    description = module.params.get('description')

    if name is not None:
        eg.name = name
    if description is not None:
        eg.description = description

    # Capacity
    expand_capacity(eg, module, is_update, do_not_update)
    # Strategy
    expand_strategy(eg, module)
    # Scaling
    expand_scaling(eg, module)
    # Third party integrations
    expand_integrations(eg, module)
    # Compute
    expand_compute(eg, module, is_update, do_not_update)
    # Multai
    expand_multai(eg, module)
    # Scheduling
    expand_scheduled_tasks(eg, module)

    return eg


def expand_compute(eg, module, is_update, do_not_update):
    elastic_ips = module.params['elastic_ips']
    on_demand_instance_type = module.params.get('on_demand_instance_type')
    spot_instance_types = module.params['spot_instance_types']
    ebs_volume_pool = module.params['ebs_volume_pool']
    availability_zones_list = module.params['availability_zones']
    product = module.params.get('product')

    eg_compute = spotinst.aws_elastigroup.Compute()

    if product is not None:
        # Only put product on group creation
        if is_update is not True:
            eg_compute.product = product

    if elastic_ips is not None:
        eg_compute.elastic_ips = elastic_ips

    if on_demand_instance_type or spot_instance_types is not None:
        eg_instance_types = spotinst.aws_elastigroup.InstanceTypes()

        if on_demand_instance_type is not None:
            eg_instance_types.spot = spot_instance_types
        if spot_instance_types is not None:
            eg_instance_types.ondemand = on_demand_instance_type

        if eg_instance_types.spot is not None or eg_instance_types.ondemand is not None:
            eg_compute.instance_types = eg_instance_types

    expand_ebs_volume_pool(eg_compute, ebs_volume_pool)

    eg_compute.availability_zones = expand_list(availability_zones_list, az_fields, 'AvailabilityZone')

    expand_launch_spec(eg_compute, module, is_update, do_not_update)

    eg.compute = eg_compute


def expand_ebs_volume_pool(eg_compute, ebs_volumes_list):
    if ebs_volumes_list is not None:
        eg_volumes = []

        for volume in ebs_volumes_list:
            eg_volume = spotinst.aws_elastigroup.EbsVolume()

            if volume.get('device_name') is not None:
                eg_volume.device_name = volume.get('device_name')
            if volume.get('volume_ids') is not None:
                eg_volume.volume_ids = volume.get('volume_ids')

            if eg_volume.device_name is not None:
                eg_volumes.append(eg_volume)

        if len(eg_volumes) > 0:
            eg_compute.ebs_volume_pool = eg_volumes


def expand_launch_spec(eg_compute, module, is_update, do_not_update):
    eg_launch_spec = expand_fields(lspec_fields, module.params, 'LaunchSpecification')

    if module.params['iam_role_arn'] is not None or module.params['iam_role_name'] is not None:
        eg_launch_spec.iam_role = expand_fields(iam_fields, module.params, 'IamRole')

    tags = module.params['tags']
    load_balancers = module.params['load_balancers']
    target_group_arns = module.params['target_group_arns']
    block_device_mappings = module.params['block_device_mappings']
    network_interfaces = module.params['network_interfaces']

    if is_update is True:
        if 'image_id' in do_not_update:
            delattr(eg_launch_spec, 'image_id')

    expand_tags(eg_launch_spec, tags)

    expand_load_balancers(eg_launch_spec, load_balancers, target_group_arns)

    expand_block_device_mappings(eg_launch_spec, block_device_mappings)

    expand_network_interfaces(eg_launch_spec, network_interfaces)

    eg_compute.launch_specification = eg_launch_spec


def expand_integrations(eg, module):
    rancher = module.params.get('rancher')
    mesosphere = module.params.get('mesosphere')
    ecs = module.params.get('ecs')
    kubernetes = module.params.get('kubernetes')
    right_scale = module.params.get('right_scale')
    opsworks = module.params.get('opsworks')
    chef = module.params.get('chef')

    integration_exists = False

    eg_integrations = spotinst.aws_elastigroup.ThirdPartyIntegrations()

    if mesosphere is not None:
        eg_integrations.mesosphere = expand_fields(mesosphere_fields, mesosphere, 'Mesosphere')
        integration_exists = True

    if ecs is not None:
        eg_integrations.ecs = expand_fields(ecs_fields, ecs, 'EcsConfiguration')
        integration_exists = True

    if kubernetes is not None:
        eg_integrations.kubernetes = expand_fields(kubernetes_fields, kubernetes, 'KubernetesConfiguration')
        integration_exists = True

    if right_scale is not None:
        eg_integrations.right_scale = expand_fields(right_scale_fields, right_scale, 'RightScaleConfiguration')
        integration_exists = True

    if opsworks is not None:
        eg_integrations.opsworks = expand_fields(opsworks_fields, opsworks, 'OpsWorksConfiguration')
        integration_exists = True

    if rancher is not None:
        eg_integrations.rancher = expand_fields(rancher_fields, rancher, 'Rancher')
        integration_exists = True

    if chef is not None:
        eg_integrations.chef = expand_fields(chef_fields, chef, 'ChefConfiguration')
        integration_exists = True

    if integration_exists:
        eg.third_parties_integration = eg_integrations


def expand_capacity(eg, module, is_update, do_not_update):
    eg_capacity = expand_fields(capacity_fields, module.params, 'Capacity')

    if is_update is True:
        delattr(eg_capacity, 'unit')

        if 'target' in do_not_update:
            delattr(eg_capacity, 'target')

    eg.capacity = eg_capacity


def expand_strategy(eg, module):
    persistence = module.params.get('persistence')
    signals = module.params.get('signals')

    eg_strategy = expand_fields(strategy_fields, module.params, 'Strategy')

    terminate_at_end_of_billing_hour = module.params.get('terminate_at_end_of_billing_hour')

    if terminate_at_end_of_billing_hour is not None:
        eg_strategy.eg_scaling_strategy = expand_fields(scaling_strategy_fields,
                                                        module.params, 'ScalingStrategy')

    if persistence is not None:
        eg_strategy.persistence = expand_fields(persistence_fields, persistence, 'Persistence')

    if signals is not None:
        eg_signals = expand_list(signals, signal_fields, 'Signal')

        if len(eg_signals) > 0:
            eg_strategy.signals = eg_signals

    eg.strategy = eg_strategy


def expand_multai(eg, module):
    multai_load_balancers = module.params.get('multai_load_balancers')

    eg_multai = expand_fields(multai_fields, module.params, 'Multai')

    if multai_load_balancers is not None:
        eg_multai_load_balancers = expand_list(multai_load_balancers, multai_lb_fields, 'MultaiLoadBalancer')

        if len(eg_multai_load_balancers) > 0:
            eg_multai.balancers = eg_multai_load_balancers
            eg.multai = eg_multai


def expand_scheduled_tasks(eg, module):
    scheduled_tasks = module.params.get('scheduled_tasks')

    if scheduled_tasks is not None:
        eg_scheduling = spotinst.aws_elastigroup.Scheduling()

        eg_tasks = expand_list(scheduled_tasks, scheduled_task_fields, 'ScheduledTask')

        if len(eg_tasks) > 0:
            eg_scheduling.tasks = eg_tasks
            eg.scheduling = eg_scheduling


def expand_load_balancers(eg_launchspec, load_balancers, target_group_arns):
    if load_balancers is not None or target_group_arns is not None:
        eg_load_balancers_config = spotinst.aws_elastigroup.LoadBalancersConfig()
        eg_total_lbs = []

        if load_balancers is not None:
            for elb_name in load_balancers:
                eg_elb = spotinst.aws_elastigroup.LoadBalancer()
                if elb_name is not None:
                    eg_elb.name = elb_name
                    eg_elb.type = 'CLASSIC'
                    eg_total_lbs.append(eg_elb)

        if target_group_arns is not None:
            for target_arn in target_group_arns:
                eg_elb = spotinst.aws_elastigroup.LoadBalancer()
                if target_arn is not None:
                    eg_elb.arn = target_arn
                    eg_elb.type = 'TARGET_GROUP'
                    eg_total_lbs.append(eg_elb)

        if len(eg_total_lbs) > 0:
            eg_load_balancers_config.load_balancers = eg_total_lbs
            eg_launchspec.load_balancers_config = eg_load_balancers_config


def expand_tags(eg_launchspec, tags):
    if tags is not None:
        eg_tags = []

        for tag in tags:
            eg_tag = spotinst.aws_elastigroup.Tag()
            if tag:
                eg_tag.tag_key, eg_tag.tag_value = list(tag.items())[0]

            eg_tags.append(eg_tag)

        if len(eg_tags) > 0:
            eg_launchspec.tags = eg_tags


def expand_block_device_mappings(eg_launchspec, bdms):
    if bdms is not None:
        eg_bdms = []

        for bdm in bdms:
            eg_bdm = expand_fields(bdm_fields, bdm, 'BlockDeviceMapping')

            if bdm.get('ebs') is not None:
                eg_bdm.ebs = expand_fields(ebs_fields, bdm.get('ebs'), 'EBS')

            eg_bdms.append(eg_bdm)

        if len(eg_bdms) > 0:
            eg_launchspec.block_device_mappings = eg_bdms


def expand_network_interfaces(eg_launchspec, enis):
    if enis is not None:
        eg_enis = []

        for eni in enis:
            eg_eni = expand_fields(eni_fields, eni, 'NetworkInterface')

            eg_pias = expand_list(eni.get('private_ip_addresses'), private_ip_fields, 'PrivateIpAddress')

            if eg_pias is not None:
                eg_eni.private_ip_addresses = eg_pias

            eg_enis.append(eg_eni)

        if len(eg_enis) > 0:
            eg_launchspec.network_interfaces = eg_enis


def expand_scaling(eg, module):
    up_scaling_policies = module.params['up_scaling_policies']
    down_scaling_policies = module.params['down_scaling_policies']
    target_tracking_policies = module.params['target_tracking_policies']

    eg_scaling = spotinst.aws_elastigroup.Scaling()

    if up_scaling_policies is not None:
        eg_up_scaling_policies = expand_scaling_policies(up_scaling_policies)
        if len(eg_up_scaling_policies) > 0:
            eg_scaling.up = eg_up_scaling_policies

    if down_scaling_policies is not None:
        eg_down_scaling_policies = expand_scaling_policies(down_scaling_policies)
        if len(eg_down_scaling_policies) > 0:
            eg_scaling.down = eg_down_scaling_policies

    if target_tracking_policies is not None:
        eg_target_tracking_policies = expand_target_tracking_policies(target_tracking_policies)
        if len(eg_target_tracking_policies) > 0:
            eg_scaling.target = eg_target_tracking_policies

    if eg_scaling.down is not None or eg_scaling.up is not None or eg_scaling.target is not None:
        eg.scaling = eg_scaling


def expand_list(items, fields, class_name):
    if items is not None:
        new_objects_list = []
        for item in items:
            new_obj = expand_fields(fields, item, class_name)
            new_objects_list.append(new_obj)

        return new_objects_list


def expand_fields(fields, item, class_name):
    class_ = getattr(spotinst.aws_elastigroup, class_name)
    new_obj = class_()

    # Handle primitive fields
    if item is not None:
        for field in fields:
            if isinstance(field, dict):
                ansible_field_name = field['ansible_field_name']
                spotinst_field_name = field['spotinst_field_name']
            else:
                ansible_field_name = field
                spotinst_field_name = field
            if item.get(ansible_field_name) is not None:
                setattr(new_obj, spotinst_field_name, item.get(ansible_field_name))

    return new_obj


def expand_scaling_policies(scaling_policies):
    eg_scaling_policies = []

    for policy in scaling_policies:
        eg_policy = expand_fields(scaling_policy_fields, policy, 'ScalingPolicy')
        eg_policy.action = expand_fields(action_fields, policy, 'ScalingPolicyAction')
        eg_scaling_policies.append(eg_policy)

    return eg_scaling_policies


def expand_target_tracking_policies(tracking_policies):
    eg_tracking_policies = []

    for policy in tracking_policies:
        eg_policy = expand_fields(tracking_policy_fields, policy, 'TargetTrackingPolicy')
        eg_tracking_policies.append(eg_policy)

    return eg_tracking_policies


def main():
    fields = dict(
        account_id=dict(type='str'),
        availability_vs_cost=dict(type='str', required=True),
        availability_zones=dict(type='list', elements='dict', required=True),
        block_device_mappings=dict(type='list', elements='dict'),
        chef=dict(type='dict'),
        credentials_path=dict(type='path', default="~/.spotinst/credentials"),
        do_not_update=dict(default=[], type='list'),
        down_scaling_policies=dict(type='list', elements='dict'),
        draining_timeout=dict(type='int'),
        ebs_optimized=dict(type='bool'),
        ebs_volume_pool=dict(type='list', elements='dict'),
        ecs=dict(type='dict'),
        elastic_beanstalk=dict(type='dict'),
        elastic_ips=dict(type='list', elements='str'),
        fallback_to_od=dict(type='bool'),
        id=dict(type='str'),
        health_check_grace_period=dict(type='int'),
        health_check_type=dict(type='str'),
        health_check_unhealthy_duration_before_replacement=dict(type='int'),
        iam_role_arn=dict(type='str'),
        iam_role_name=dict(type='str'),
        image_id=dict(type='str', required=True),
        key_pair=dict(type='str', no_log=False),
        kubernetes=dict(type='dict'),
        lifetime_period=dict(type='int'),
        load_balancers=dict(type='list', elements='str'),
        max_size=dict(type='int', required=True),
        mesosphere=dict(type='dict'),
        min_size=dict(type='int', required=True),
        monitoring=dict(type='str'),
        multai_load_balancers=dict(type='list'),
        multai_token=dict(type='str', no_log=True),
        name=dict(type='str', required=True),
        network_interfaces=dict(type='list', elements='dict'),
        on_demand_count=dict(type='int'),
        on_demand_instance_type=dict(type='str'),
        opsworks=dict(type='dict'),
        persistence=dict(type='dict'),
        product=dict(type='str', required=True),
        rancher=dict(type='dict'),
        right_scale=dict(type='dict'),
        risk=dict(type='int'),
        roll_config=dict(type='dict'),
        scheduled_tasks=dict(type='list', elements='dict'),
        security_group_ids=dict(type='list', elements='str', required=True),
        shutdown_script=dict(type='str'),
        signals=dict(type='list', elements='dict'),
        spin_up_time=dict(type='int'),
        spot_instance_types=dict(type='list', elements='str', required=True),
        state=dict(default='present', choices=['present', 'absent']),
        tags=dict(type='list', elements='dict'),
        target=dict(type='int', required=True),
        target_group_arns=dict(type='list', elements='str'),
        tenancy=dict(type='str'),
        terminate_at_end_of_billing_hour=dict(type='bool'),
        token=dict(type='str', no_log=True),
        unit=dict(type='str'),
        user_data=dict(type='str'),
        utilize_reserved_instances=dict(type='bool'),
        uniqueness_by=dict(default='name', choices=['name', 'id']),
        up_scaling_policies=dict(type='list', elements='dict'),
        target_tracking_policies=dict(type='list', elements='dict'),
        wait_for_instances=dict(type='bool', default=False),
        wait_timeout=dict(type='int')
    )

    module = AnsibleModule(argument_spec=fields)

    if not HAS_SPOTINST_SDK:
        module.fail_json(msg="the Spotinst SDK library is required. (pip install spotinst_sdk)")

    # Retrieve creds file variables
    creds_file_loaded_vars = dict()

    credentials_path = module.params.get('credentials_path')

    try:
        with open(credentials_path, "r") as creds:
            for line in creds:
                eq_index = line.find('=')
                var_name = line[:eq_index].strip()
                string_value = line[eq_index + 1:].strip()
                creds_file_loaded_vars[var_name] = string_value
    except IOError:
        pass
    # End of creds file retrieval

    token = module.params.get('token')
    if not token:
        token = os.environ.get('SPOTINST_TOKEN')
    if not token:
        token = creds_file_loaded_vars.get("token")

    account = module.params.get('account_id')
    if not account:
        account = os.environ.get('SPOTINST_ACCOUNT_ID') or os.environ.get('ACCOUNT')
    if not account:
        account = creds_file_loaded_vars.get("account")

    client = spotinst.SpotinstClient(auth_token=token, print_output=False)

    if account is not None:
        client = spotinst.SpotinstClient(auth_token=token, print_output=False, account_id=account)

    group_id, message, has_changed = handle_elastigroup(client=client, module=module)

    instances = retrieve_group_instances(client=client, module=module, group_id=group_id)

    module.exit_json(changed=has_changed, group_id=group_id, message=message, instances=instances)


if __name__ == '__main__':
    main()
