# Copyright 2016 Capital One Services, LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Actions to take on resources
"""
import base64
import logging
import zlib

from botocore.exceptions import ClientError

from c7n.registry import PluginRegistry
from c7n.executor import ThreadPoolExecutor
from c7n import utils
from c7n.version import version as VERSION


class ActionRegistry(PluginRegistry):

    def __init__(self, *args, **kw):
        super(ActionRegistry, self).__init__(*args, **kw)
        self.register('notify', Notify)
        self.register('invoke-lambda', LambdaInvoke)

    def parse(self, data, manager):
        results = []
        for d in data:
            results.append(self.factory(d, manager))
        return results

    def factory(self, data, manager):
        if isinstance(data, dict):
            action_type = data.get('type')
            if action_type is None:
                raise ValueError(
                    "Invalid action type found in %s" % (data))
        else:
            action_type = data
            data = {}

        action_class = self.get(action_type)
        if action_class is None:
            raise ValueError(
                "Invalid action type %s, valid actions %s" % (
                    action_type, self.keys()))
        # Construct a ResourceManager
        return action_class(data, manager).validate()


class BaseAction(object):

    permissions = ()
    metrics = ()

    log = logging.getLogger("custodian.actions")

    executor_factory = ThreadPoolExecutor

    schema = {'type': 'object'}

    def __init__(self, data=None, manager=None, log_dir=None):
        self.data = data or {}
        self.manager = manager
        self.log_dir = log_dir

    def validate(self):
        return self

    @property
    def name(self):
        return self.__class__.__name__.lower()

    def process(self, resources):
        raise NotImplemented(
            "Base action class does not implement behavior")

    def get_permissions(self):
        return self.permissions

    def _run_api(self, cmd, *args, **kw):
        try:
            return cmd(*args, **kw)
        except ClientError, e:
            if (e.response['Error']['Code'] == 'DryRunOperation'
                    and e.response['ResponseMetadata']['HTTPStatusCode'] == 412
                    and 'would have succeeded' in e.message):
                return self.log.info(
                    "Dry run operation %s succeeded" % (
                        self.__class__.__name__.lower()))
            raise

Action = BaseAction


class ModifyVpcSecurityGroupsAction(BaseAction):
    """Common actions for modifying security groups on a resource
    
    Can target either physical groups as a list of group ids or
    symbolic groups like 'matched' or 'all'. 'matched' uses
    the annotations of the 'security-group' interface filter.
    
    Note an interface always gets at least one security group, so
    we mandate the specification of an isolation/quarantine group
    that can be specified if there would otherwise be no groups.

    type: modify-security-groups
        add: []
        remove: [] | matched
        isolation-group: sg-xyz
    """

    properties = {
        'add': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type':'string'}}]},
        'remove': {'oneOf': [
            {'type': 'array', 'items': {'type': 'string'}},
            {'enum': ['matched', 'all', {'type': 'string'}]}]},
        'isolation-group': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}
    }
    schema = utils.type_schema(
        'modify-security-groups',
        **properties
    )

    def validate(self):
        """ Validate the schema for modify-security-groups action

        Must specify 'add' or 'remove' parameters.

        If 'remove' is specified, one of 'add' or 'isolation-group' must also
        be specified in the event that the 'remove' operation marks all extant
        security groups on the interface for removal.

        Valid input types:
        'add': list, string
        'remove': list, string, keywords: 'matched' or 'all'
        'isolation-group': list, string

        """
        # must specify add and remove params
        if 'add' not in self.data and 'remove' not in self.data:
            raise ValueError('Must specify either \'add\' or \'remove\' parameters')
        if 'remove' in self.data:
            # need 'add' or 'isolation-group'
            if 'isolation-group' not in self.data and 'add' not in self.data:
                raise ValueError('Must specify \'isolation-group\' or \'add\' parameters when using the \'remove\' action')
            # type validation
            if isinstance(self.data['remove'], basestring):
                if 'sg-' not in self.data['remove'] and 'all' not in self.data['remove'] and 'matched' not in self.data['remove']:
                    raise ValueError('Must specify valid input for the \'remove\' parameter')
            if isinstance(self.data['remove'], list) and any('sg-' not in g for g in self.data['remove']):
                raise ValueError('Must specify valid security group ids for the \'remove\' parameter')
        # type validations for the rest: str with 'sg-' or list with all 'sg-' strs
        if 'add' in self.data:
            if isinstance(self.data['add'], basestring) and 'sg-' not in self.data['add']:
                raise ValueError('Must specify a valid security group id for the \'add\' parameter')
            if isinstance(self.data['add'], list) and any('sg-' not in g for g in self.data['add']):
                raise ValueError('Must specify valid security group ids for the \'add\' parameter')
        if 'isolation-group' in self.data:
            if isinstance(self.data['isolation-group'], basestring) and 'sg-' not in self.data['isolation-group']:
                raise ValueError('Must specify a valid security group id for the \'isolation-group\' parameter')
            if isinstance(self.data['isolation-group'], list) and any('sg-' not in g for g in self.data['isolation-group']):
                raise ValueError('Must specify valid security group ids for the \'isolation-group\' parameter')

        return self

    def get_groups(self, resources, metadata_key=None):
        """ Parse policies to get lists of security groups to attach to each input resource

        For each input resource, parse the various add/remove/isolation-
        group policies for 'modify-security-groups' to find the resulting
        set of VPC security groups to attach to that resource.

        The 'metadata_key' parameter can be used for two purposes at the moment;
        The first use is for resources' APIs that return a list of security group
        IDs but use a different metadata key than 'Groups' or 'SecurityGroups'.

        The second use is for when there are richer objects in the 'Groups' or
        'SecurityGroups' lists. The custodian actions need to act on lists of
        just security group IDs, so the metadata_key can be used to select IDs
        from the richer objects in the provided lists.

        Returns a list of lists containing the resulting VPC security groups
        that should end up on each resource passed in.

        :param resources: List of resources containing VPC Security Groups
        :param metadata_key: Metadata key for security groups list (see notes for detailed usage)
        :return: List of lists of security groups per resource
        """
        # parse the add, remove, and isolation group params to return the
        # list of security groups that will end up on the resource
        # target_group_ids = self.data.get('groups', 'matched')

        add_target_group_ids = self.data.get('add', None)
        remove_target_group_ids = self.data.get('remove', None)
        isolation_group = self.data.get('isolation-group')
        add_groups = []
        remove_groups = []
        return_groups = []

        for idx, r in enumerate(resources):
            if r.get('Groups'):
                if metadata_key and isinstance(r['Groups'][0], dict):
                    rgroups = [g[metadata_key] for g in r['SecurityGroups']]
                else:
                    rgroups = [g['GroupId'] for g in r['Groups']]
            elif r.get('SecurityGroups'):
                if metadata_key and isinstance(r['SecurityGroups'][0], dict):
                    rgroups = [g[metadata_key] for g in r['SecurityGroups']]
                else:
                    rgroups = [g for g in r['SecurityGroups']]
            elif r.get('VpcSecurityGroups'):
                if metadata_key and isinstance(r['VpcSecurityGroups'][0], dict):
                    rgroups = [g[metadata_key] for g in r['VpcSecurityGroups']]
                else:
                    rgroups = [g for g in r['VpcSecurityGroups']]
            # use as substitution for 'Groups' or '[Vpc]SecurityGroups'
            # unsure if necessary - defer to coverage report
            elif metadata_key and r.get(metadata_key):
                rgroups = [g for g in r[metadata_key]]

            # Parse remove_groups
            if remove_target_group_ids == 'matched':
                remove_groups = r.get('c7n.matched-security-groups', ())
            elif remove_target_group_ids == 'all':
                remove_groups = rgroups
            elif isinstance(remove_target_group_ids, list):
                remove_groups = remove_target_group_ids
            elif isinstance(remove_target_group_ids, basestring):
                remove_groups = [remove_target_group_ids]

            # Parse add_groups
            if isinstance(add_target_group_ids, list):
                add_groups = add_target_group_ids
            elif isinstance(add_target_group_ids, basestring):
                add_groups = [add_target_group_ids]

            # seems extraneous with list?
            # if not remove_groups and not add_groups:
            #     continue

            for g in remove_groups:
                if g in rgroups:
                    rgroups.remove(g)

            for g in add_groups:
                if g not in rgroups:
                    rgroups.append(g)

            if not rgroups:
                rgroups.append(isolation_group)

            return_groups.append(rgroups)

        return return_groups


class EventAction(BaseAction):
    """Actions which receive lambda event if present
    """


class LambdaInvoke(EventAction):
    """ Invoke an arbitrary lambda

    serialized invocation parameters

     - resources / collection of resources
     - policy / policy that is invoke the lambda
     - action / action that is invoking the lambda
     - event / cloud trail event if any
     - version / version of custodian invoking the lambda

    We automatically batch into sets of 250 for invocation,
    We try to utilize async invocation by default, this imposes
    some greater size limits of 128kb which means we batch
    invoke.

    Example::

     - type: invoke-lambda
       function: my-function
    """

    schema = utils.type_schema(
        'invoke-lambda',
        function={'type': 'string'},
        async={'type': 'boolean'},
        qualifier={'type': 'string'},
        batch_size={'type': 'integer'},