"""
smartaws.ec2.vpc
~~~~~~~~~~~~~~~~

This module manages Amazon VPC objects and related resources.

:copyright: (c) 2016 by Yann Lambret.
:license: MIT, see LICENSE for more details.
"""
from boto3 import Session
from botocore.exceptions import ClientError
from botocore.exceptions import ParamValidationError

from smartaws import log
from smartaws.ec2 import base


logger = log.get_logger(__name__)


class VpcWrapper(base.BaseWrapper):
    """Wrapper class for :class:`boto3.resources.factory.ec2.Vpc` objects."""
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)


def _add_wrapper(base_classes, **kwargs):
    """
    Register the :class:`VpcWrapper` class.

    See `Boto 3 Extensibility Guide
    <http://boto3.readthedocs.org/en/latest/guide/events.html>`_
    """
    base_classes.insert(0, VpcWrapper)


class VpcHandler(base.BaseHandler):
    """Manage a :class:`VpcWrapper` object's lifecycle."""

    def __init__(self, ctx, credentials):
        """Create boto3 session and clients."""
        super().__init__()

        # Create a boto session and add extra
        # features to the base class EC2.Vpc
        event = 'creating-resource-class.ec2.Vpc'
        self._session = Session(**credentials)
        self._session.events.register(event, _add_wrapper)

        # boto3 clients
        self._ec2 = self._session.resource('ec2')
        self._client = self._session.client('ec2')

        # Context for the resource we want
        # to create, update or delete
        self._ctx = ctx
        self._name = ctx.get('name')

    def create_resource(self):
        """Create a VPC, or update it if it already exists."""
        try:
            self._resource = self._load(self._name, manager_name='vpcs')
        except ValueError as err:
            logger.critical(err)
            return

        if self._resource:
            logger.info('VPC %(name)s|%(cidr_block)s already exists.', self._ctx)
        else:
            self._create_vpc()

        # Store the object in the cache for future use
        with self._lock:
            self._cache[self._name] = self._resource

    def update_resource(self):
        """Update a VPC."""
        try:
            self._resource = self._load(self._name, manager_name='vpcs')
        except ValueError as err:
            logger.critical(err)
            return

        if self._resource:
            self._update_vpc()

            # Store the object in the cache for future use
            with self._lock:
                self._cache[self._name] = self._resource
        else:
            logger.error('VPC %s does not exist.', self._name)

    def delete_resource(self):
        """Delete a :class:`VpcWrapper` object."""
        try:
            self._resource = self._load(self._name, manager_name='vpcs')
        except ValueError as err:
            logger.critical(err)
            return

        if self._resource:
            # If an internet gateway is attached to the VPC,
            # we try to detach it first
            filters = [{
                'Name': 'attachment.vpc-id',
                'Values': [self._resource.id]
            }]
            items = list(self._resource.internet_gateways.filter(Filters=filters))
            if items:
                igw = items[0]
                try:
                    self._resource.detach_internet_gateway(InternetGatewayId=igw.id)
                except ClientError as exc:
                    logger.error(exc)

            # Delete the VPC
            logger.info('Removing VPC %(name)s|%(cidr_block)s.', self._ctx)
            try:
                self._client.delete_vpc(VpcId=self._resource.id)
            except ClientError as exc:
                logger.error(exc)
        else:
            logger.error('VPC %s does not exist.', self._name)

    def _create_vpc(self):
        """Create a :class:`VpcWrapper` object."""
        logger.info('Creating VPC %(name)s|%(cidr_block)s.', self._ctx)
        try:
            self._resource = self._ec2.create_vpc(CidrBlock=self._ctx['cidr_block'])
        except ClientError as exc:
            logger.error(exc)
            return

        # Wait for the new VPC to become
        # available before going any further
        waiter = self._client.get_waiter('vpc_available')
        waiter.wait(VpcIds=[self._resource.id])

        # Set the value of the 'Name' tag
        self._resource.name = self._name

        # Update other object attributes
        self._update_vpc()

    def _update_vpc(self):
        """Update a :class:`VpcWrapper` object."""
        # Set VPC attributes:
        #
        #  - EnableDnsSupport
        #  - EnableDnsHostnames
        #
        enable_dns_support = self._ctx.get('enable_dns_support', True)
        enable_dns_hostnames = self._ctx.get('enable_dns_hostnames', False)

        try:
            response = self._resource.describe_attribute(Attribute='enableDnsSupport')
            attr = response.get('EnableDnsSupport')

            if enable_dns_support != attr.get('Value'):
                logger.info(
                    "Setting 'enable_dns_support' attribute to '%s' for VPC %s.",
                    enable_dns_support, self._name
                )
                self._resource.modify_attribute(EnableDnsSupport={'Value': enable_dns_support})
        except (ClientError, ParamValidationError) as err:
            logger.error(err)

        try:
            response = self._resource.describe_attribute(Attribute='enableDnsHostnames')
            attr = response.get('EnableDnsHostnames')

            if enable_dns_hostnames != attr.get('Value'):
                logger.info(
                    "Setting 'enable_dns_hostnames' attribute to '%s' for VPC %s.",
                    enable_dns_hostnames, self._name
                )
                self._resource.modify_attribute(EnableDnsHostnames={'Value': enable_dns_hostnames})
        except (ClientError, ParamValidationError) as exc:
            logger.error(exc)

        self._update_tags()
        self._manage_dhcp_options_set()
        self._manage_internet_gateway()

    def _manage_dhcp_options_set(self):
        """Manage the DHCP options set association."""
        dhcp_name = self._ctx.get('dhcp_options')