#!/usr/bin/python
# Copyright: 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
__metaclass__ = type


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


DOCUMENTATION = '''
---
module: cloudformation_info
short_description: Obtain information about an AWS CloudFormation stack
description:
  - Gets information about an AWS CloudFormation stack.
  - This module was called C(cloudformation_facts) before Ansible 2.9, returning C(ansible_facts).
    Note that the M(cloudformation_info) module no longer returns C(ansible_facts)!
requirements:
  - boto3 >= 1.0.0
  - python >= 2.6
version_added: "2.2"
author:
    - Justin Menga (@jmenga)
    - Kevin Coming (@waffie1)
options:
    stack_name:
        description:
          - The name or id of the CloudFormation stack. Gathers information on all stacks by default.
        type: str
    all_facts:
        description:
            - Get all stack information for the stack.
        type: bool
        default: false
    stack_events:
        description:
            - Get stack events for the stack.
        type: bool
        default: false
    stack_template:
        description:
            - Get stack template body for the stack.
        type: bool
        default: false
    stack_resources:
        description:
            - Get stack resources for the stack.
        type: bool
        default: false
    stack_policy:
        description:
            - Get stack policy for the stack.
        type: bool
        default: false
    stack_change_sets:
        description:
            - Get stack change sets for the stack
        type: bool
        default: false
        version_added: '2.10'
extends_documentation_fragment:
    - aws
    - ec2
'''

EXAMPLES = '''
# Note: These examples do not set authentication details, see the AWS Guide for details.

# Get summary information about a stack
- cloudformation_info:
    stack_name: my-cloudformation-stack
  register: output

- debug:
    msg: "{{ output['cloudformation']['my-cloudformation-stack'] }}"

# When the module is called as cloudformation_facts, return values are published
# in ansible_facts['cloudformation'][<stack_name>] and can be used as follows.
# Note that this is deprecated and will stop working in Ansible 2.13.

- cloudformation_facts:
    stack_name: my-cloudformation-stack

- debug:
    msg: "{{ ansible_facts['cloudformation']['my-cloudformation-stack'] }}"

# Get stack outputs, when you have the stack name available as a fact
- set_fact:
    stack_name: my-awesome-stack

- cloudformation_info:
    stack_name: "{{ stack_name }}"
  register: my_stack

- debug:
    msg: "{{ my_stack.cloudformation[stack_name].stack_outputs }}"

# Get all stack information about a stack
- cloudformation_info:
    stack_name: my-cloudformation-stack
    all_facts: true

# Get stack resource and stack policy information about a stack
- cloudformation_info:
    stack_name: my-cloudformation-stack
    stack_resources: true
    stack_policy: true

# Fail if the stack doesn't exist
- name: try to get facts about a stack but fail if it doesn't exist
  cloudformation_info:
    stack_name: nonexistent-stack
    all_facts: yes
  failed_when: cloudformation['nonexistent-stack'] is undefined
'''

RETURN = '''
stack_description:
    description: Summary facts about the stack
    returned: if the stack exists
    type: dict
stack_outputs:
    description: Dictionary of stack outputs keyed by the value of each output 'OutputKey' parameter and corresponding value of each
                 output 'OutputValue' parameter
    returned: if the stack exists
    type: dict
    sample:
      ApplicationDatabaseName: dazvlpr01xj55a.ap-southeast-2.rds.amazonaws.com
stack_parameters:
    description: Dictionary of stack parameters keyed by the value of each parameter 'ParameterKey' parameter and corresponding value of
                 each parameter 'ParameterValue' parameter
    returned: if the stack exists
    type: dict
    sample:
      DatabaseEngine: mysql
      DatabasePassword: "***"
stack_events:
    description: All stack events for the stack
    returned: only if all_facts or stack_events is true and the stack exists
    type: list
stack_policy:
    description: Describes the stack policy for the stack
    returned: only if all_facts or stack_policy is true and the stack exists
    type: dict
stack_template:
    description: Describes the stack template for the stack
    returned: only if all_facts or stack_template is true and the stack exists
    type: dict
stack_resource_list:
    description: Describes stack resources for the stack
    returned: only if all_facts or stack_resourses is true and the stack exists
    type: list
stack_resources:
    description: Dictionary of stack resources keyed by the value of each resource 'LogicalResourceId' parameter and corresponding value of each
                 resource 'PhysicalResourceId' parameter
    returned: only if all_facts or stack_resourses is true and the stack exists
    type: dict
    sample:
      AutoScalingGroup: "dev-someapp-AutoscalingGroup-1SKEXXBCAN0S7"
      AutoScalingSecurityGroup: "sg-abcd1234"
      ApplicationDatabase: "dazvlpr01xj55a"
stack_change_sets:
    description: A list of stack change sets.  Each item in the list represents the details of a specific changeset

    returned: only if all_facts or stack_change_sets is true and the stack exists
    type: list
'''

import json
import traceback

from functools import partial
from ansible.module_utils._text import to_native
from ansible.module_utils.aws.core import AnsibleAWSModule
from ansible.module_utils.ec2 import (camel_dict_to_snake_dict, AWSRetry, boto3_tag_list_to_ansible_dict)

try:
    import botocore
except ImportError:
    pass  # handled by AnsibleAWSModule


class CloudFormationServiceManager:
    """Handles CloudFormation Services"""

    def __init__(self, module):
        self.module = module
        self.client = module.client('cloudformation')

    @AWSRetry.exponential_backoff(retries=5, delay=5)
    def describe_stacks_with_backoff(self, **kwargs):
        paginator = self.client.get_paginator('describe_stacks')
        return paginator.paginate(**kwargs).build_full_result()['Stacks']

    def describe_stacks(self, stack_name=None):
        try:
            kwargs = {'StackName': stack_name} if stack_name else {}
            response = self.describe_stacks_with_backoff(**kwargs)
            if response is not None:
                return response
            self.module.fail_json(msg="Error describing stack(s) - an empty response was returned")
        except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
            if 'does not exist' in e.response['Error']['Message']:
                # missing stack, don't bail.
                return {}
            self.module.fail_json_aws(e, msg="Error describing stack " + stack_name)

    @AWSRetry.exponential_backoff(retries=5, delay=5)
    def list_stack_resources_with_backoff(self, stack_name):
        paginator = self.client.get_paginator('list_stack_resources')
        return paginator.paginate(StackName=stack_name).build_full_result()['StackResourceSummaries']

    def list_stack_resources(self, stack_name):
        try:
            return self.list_stack_resources_with_backoff(stack_name)
        except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
            self.module.fail_json_aws(e, msg="Error listing stack resources for stack " + stack_name)

    @AWSRetry.exponential_backoff(retries=5, delay=5)
    def describe_stack_events_with_backoff(self, stack_name):
        paginator = self.client.get_paginator('describe_stack_events')
        return paginator.paginate(StackName=stack_name).build_full_result()['StackEvents']

    def describe_stack_events(self, stack_name):
        try:
            return self.describe_stack_events_with_backoff(stack_name)
        except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
            self.module.fail_json_aws(e, msg="Error listing stack events for stack " + stack_name)

    @AWSRetry.exponential_backoff(retries=5, delay=5)
    def list_stack_change_sets_with_backoff(self, stack_name):
        paginator = self.client.get_paginator('list_change_sets')
        return paginator.paginate(StackName=stack_name).build_full_result()['Summaries']

    @AWSRetry.exponential_backoff(retries=5, delay=5)
    def describe_stack_change_set_with_backoff(self, **kwargs):
        paginator = self.client.get_paginator('describe_change_set')
        return paginator.paginate(**kwargs).build_full_result()

    def describe_stack_change_sets(self, stack_name):
        changes = []
        try:
            change_sets = self.list_stack_change_sets_with_backoff(stack_name)
            for item in change_sets:
                changes.append(self.describe_stack_change_set_with_backoff(
                               StackName=stack_name,
                               ChangeSetName=item['ChangeSetName']))
            return changes
        except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
            self.module.fail_json_aws(e, msg="Error describing stack change sets for stack " + stack_name)

    @AWSRetry.exponential_backoff(retries=5, delay=5)
    def get_stack_policy_with_backoff(self, stack_name):
        return self.client.get_stack_policy(StackName=stack_name)

    def get_stack_policy(self, stack_name):
        try:
            response = self.get_stack_policy_with_backoff(stack_name)
            stack_policy = response.get('StackPolicyBody')
            if stack_policy:
                return json.loads(stack_policy)
            return dict()
        except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
            self.module.fail_json_aws(e, msg="Error getting stack policy for stack " + stack_name)

    @AWSRetry.exponential_backoff(retries=5, delay=5)
    def get_template_with_backoff(self, stack_name):
        return self.client.get_template(StackName=stack_name)

    def get_template(self, stack_name):
        try:
            response = self.get_template_with_backoff(stack_name)
            return response.get('TemplateBody')
        except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
            self.module.fail_json_aws(e, msg="Error getting stack template for stack " + stack_name)


def to_dict(items, key, value):
    ''' Transforms a list of items to a Key/Value dictionary '''
    if items:
        return dict(zip([i.get(key) for i in items], [i.get(value) for i in items]))
    else:
        return dict()


def main():
    argument_spec = dict(
        stack_name=dict(),
        all_facts=dict(required=False, default=False, type='bool'),
        stack_policy=dict(required=False, default=False, type='bool'),
        stack_events=dict(required=False, default=False, type='bool'),
        stack_resources=dict(required=False, default=False, type='bool'),
        stack_template=dict(required=False, default=False, type='bool'),
        stack_change_sets=dict(required=False, default=False, type='bool'),
    )
    module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True)

    is_old_facts = module._name == 'cloudformation_facts'
    if is_old_facts:
        module.deprecate("The 'cloudformation_facts' module has been renamed to 'cloudformation_info', "
                         "and the renamed one no longer returns ansible_facts",
                         version='2.13', collection_name='ansible.builtin')

    service_mgr = CloudFormationServiceManager(module)

    if is_old_facts:
        result = {'ansible_facts': {'cloudformation': {}}}
    else:
        result = {'cloudformation': {}}

    for stack_description in service_mgr.describe_stacks(module.params.get('stack_name')):
        facts = {'stack_description': stack_description}
        stack_name = stack_description.get('StackName')

        # Create stack output and stack parameter dictionaries
        if facts['stack_description']:
            facts['stack_outputs'] = to_dict(facts['stack_description'].get('Outputs'), 'OutputKey', 'OutputValue')
            facts['stack_parameters'] = to_dict(facts['stack_description'].get('Parameters'),
                                                'ParameterKey', 'ParameterValue')
            facts['stack_tags'] = boto3_tag_list_to_ansible_dict(facts['stack_description'].get('Tags'))

        # Create optional stack outputs
        all_facts = module.params.get('all_facts')
        if all_facts or module.params.get('stack_resources'):
            facts['stack_resource_list'] = service_mgr.list_stack_resources(stack_name)
            facts['stack_resources'] = to_dict(facts.get('stack_resource_list'),
                                               'LogicalResourceId', 'PhysicalResourceId')
        if all_facts or module.params.get('stack_template'):
            facts['stack_template'] = service_mgr.get_template(stack_name)
        if all_facts or module.params.get('stack_policy'):
            facts['stack_policy'] = service_mgr.get_stack_policy(stack_name)
        if all_facts or module.params.get('stack_events'):
            facts['stack_events'] = service_mgr.describe_stack_events(stack_name)
        if all_facts or module.params.get('stack_change_sets'):
            facts['stack_change_sets'] = service_mgr.describe_stack_change_sets(stack_name)

        if is_old_facts:
            result['ansible_facts']['cloudformation'][stack_name] = facts
        else:
            result['cloudformation'][stack_name] = camel_dict_to_snake_dict(facts, ignore_list=('stack_outputs',
                                                                                                'stack_parameters',
                                                                                                'stack_policy',
                                                                                                'stack_resources',
                                                                                                'stack_tags',
                                                                                                'stack_template'))

    module.exit_json(changed=False, **result)


if __name__ == '__main__':
    main()