# Copyright 2015-2017 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 perform on Azure resources
"""
import datetime
from datetime import timedelta

from azure.mgmt.resource.resources.models import GenericResource, ResourceGroupPatchable
from c7n_azure.storage_utils import StorageUtilities
from c7n_azure.utils import utcnow
from dateutil import zoneinfo
from msrestazure.azure_exceptions import CloudError

from c7n import utils
from c7n.actions import BaseAction, BaseNotify
from c7n.filters import FilterValidationError
from c7n.filters.core import PolicyValidationError
from c7n.filters.offhours import Time
from c7n.resolver import ValuesFrom
from c7n.utils import type_schema


class TagHelper:

    @staticmethod
    def update_resource_tags(tag_action, resource, tags):
        client = tag_action.session.client('azure.mgmt.resource.ResourceManagementClient')

        # resource group type
        if tag_action.manager.type == 'resourcegroup':
            params_patch = ResourceGroupPatchable(
                tags=tags
            )
            client.resource_groups.update(
                resource['name'],
                params_patch,
            )
        # other Azure resources
        else:
            if tag_action.manager.type == 'armresource':
                raise NotImplementedError('Cannot tag generic ARM resources.')

            az_resource = GenericResource.deserialize(resource)
            api_version = tag_action.session.resource_api_version(az_resource.id)
            az_resource.tags = tags

            client.resources.create_or_update_by_id(resource['id'], api_version, az_resource)

    @staticmethod
    def remove_tags(tag_action, resource, tags_to_delete):
        # get existing tags
        tags = resource.get('tags', {})
        # delete tags
        resource_tags = {key: tags[key] for key in tags if key not in tags_to_delete}
        TagHelper.update_resource_tags(tag_action, resource, resource_tags)

    @staticmethod
    def add_tags(tag_action, resource, new_tags):
        # get existing tags
        tags = resource.get('tags', {})
        # add or update tags
        for key in new_tags:
            tags[key] = new_tags[key]
        TagHelper.update_resource_tags(tag_action, resource, tags)


class Tag(BaseAction):
    """Adds tags to Azure resources

        .. code-block:: yaml

          policies:
            - name: azure-tag-resourcegroups
              resource: azure.resourcegroup
              description: |
                Tag all existing resource groups with a value such as Environment
              actions:
               - type: tag
                 tag: Environment
                 value: Test
    """

    schema = utils.type_schema(
        'tag',
        **{
            'tag': {'type': 'string'},
            'value': {'type': 'string'},
            'tags': {'type': 'object'}
        }
    )

    def __init__(self, data=None, manager=None, log_dir=None):
        super(Tag, self).__init__(data, manager, log_dir)

    def validate(self):
        if not self.data.get('tags') and not (self.data.get('tag') and self.data.get('value')):
            raise FilterValidationError(
                "Must specify either tags or a tag and value")

        if self.data.get('tags') and self.data.get('tag'):
            raise FilterValidationError(
                "Can't specify both tags and tag, choose one")

        return self

    def process(self, resources):
        self.session = self.manager.get_session()
        with self.executor_factory(max_workers=3) as w:
            list(w.map(self.process_resource, resources))

    def process_resource(self, resource):
        new_tags = self.data.get('tags') or {self.data.get('tag'): self.data.get('value')}
        TagHelper.add_tags(self, resource, new_tags)


class RemoveTag(BaseAction):
    """Removes tags from Azure resources

        .. code-block:: yaml

          policies:
            - name: azure-remove-tag-resourcegroups
              resource: azure.resourcegroup
              description: |
                Remove tag for all existing resource groups with a key such as Environment
              actions:
               - type: untag
                 tags: ['Environment']
    """
    schema = utils.type_schema(
        'untag',
        tags={'type': 'array', 'items': {'type': 'string'}})

    def __init__(self, data=None, manager=None, log_dir=None):
        super(RemoveTag, self).__init__(data, manager, log_dir)

    def validate(self):
        if not self.data.get('tags'):
            raise FilterValidationError("Must specify tags")
        return self

    def process(self, resources):
        self.session = self.manager.get_session()
        with self.executor_factory(max_workers=3) as w:
            list(w.map(self.process_resource, resources))

    def process_resource(self, resource):
        tags_to_delete = self.data.get('tags')
        TagHelper.remove_tags(self, resource, tags_to_delete)


class AutoTagUser(BaseAction):
    """Attempts to tag a resource with the first user who created/modified it.

    .. code-block:: yaml

      policies:
        - name: azure-auto-tag-creator
          resource: azure.resourcegroup
          description: |
            Tag all existing resource groups with the 'CreatorEmail' tag
          actions:
           - type: auto-tag-user
             tag: CreatorEmail

    This action searches from the earliest 'write' operation's caller
    in the activity logs for a particular resource.

    Note: activity logs are only held for the last 90 days.

    """
    default_user = "Unknown"
    query_select = "eventTimestamp, operationName, caller"
    max_query_days = 90

    schema = utils.type_schema(
        'auto-tag-user',
        required=['tag'],
        **{'update': {'type': 'boolean'},
           'tag': {'type': 'string'},
           'days': {'type': 'integer'}})

    def __init__(self, data=None, manager=None, log_dir=None):
        super(AutoTagUser, self).__init__(data, manager, log_dir)

    def validate(self):
        if self.manager.action_registry.get('tag') is None:
            raise FilterValidationError("Resource does not support tagging")

        if (self.data.get('days') is not None and
                (self.data.get('days') < 1 or self.data.get('days') > 90)):
            raise FilterValidationError("Days must be between 1 and 90")

        return self

    def process(self, resources):
        self.session = self.manager.get_session()
        self.client = self.manager.get_client('azure.mgmt.monitor.MonitorManagementClient')
        self.tag_key = self.data['tag']
        self.should_update = self.data.get('update', False)
        with self.executor_factory(max_workers=3) as w:
            list(w.map(self.process_resource, resources))

    def process_resource(self, resource):
        # if the auto-tag-user policy set update to False (or it's unset) then we
        # will skip writing their UserName tag and not overwrite pre-existing values
        if not self.should_update and resource.get('tags', {}).get(self.tag_key, None):
            return

        user = self.default_user

        # Calculate start time
        delta_days = self.data.get('days', self.max_query_days)
        start_time = utcnow() - datetime.timedelta(days=delta_days)

        # resource group type
        if self.manager.type == 'resourcegroup':
            resource_type = "Microsoft.Resources/subscriptions/resourcegroups"
            query_filter = " and ".join([
                "eventTimestamp ge '%s'" % start_time,
                "resourceGroupName eq '%s'" % resource['name'],
                "eventChannels eq 'Operation'"
            ])
        # other Azure resources
        else:
            resource_type = resource['type']
            query_filter = " and ".join([
                "eventTimestamp ge '%s'" % start_time,
                "resourceUri eq '%s'" % resource['id'],
                "eventChannels eq 'Operation'"
            ])

        # fetch activity logs
        logs = self.client.activity_logs.list(
            filter=query_filter,
            select=self.query_select
        )

        # get the user who issued the first operation
        operation_name = "%s/write" % resource_type
        first_op = self.get_first_operation(logs, operation_name)
        if first_op is not None:
            user = first_op.caller

        # issue tag action to label user
        try:
            TagHelper.add_tags(self, resource, {self.tag_key: user})
        except CloudError as e:
            # resources can be locked
            if e.inner_exception.error == 'ScopeLocked':
                pass

    @staticmethod
    def get_first_operation(logs, operation_name):
        first_operation = None
        for l in logs:
            if l.operation_name.value and l.operation_name.value.lower() == operation_name.lower():
                first_operation = l

        return first_operation


class TagTrim(BaseAction):
    """Automatically remove tags from an azure resource.

    Azure Resources and Resource Groups have a limit of 15 tags.
    In order to make additional tag space on a set of resources,
    this action can be used to remove enough tags to make the
    desired amount of space while preserving a given set of tags.
    Setting the space value to 0 removes all tags but those
    listed to preserve.

    .. code-block :: yaml

      - policies:
         - name: azure-tag-trim
           comment: |
             Any instances with 14 or more tags get tags removed until
             they match the target tag count, in this case 13, so
             that we free up tag slots for another usage.
           resource: azure.resourcegroup
           filters:
               # Filter down to resources that do not have the space
               # to add additional required tags. For example, if an
               # additional 2 tags need to be added to a resource, with
               # 15 tags as the limit, then filter down to resources that
               # have 14 or more tags since they will need to have tags
               # removed for the 2 extra. This also ensures that metrics
               # reporting is correct for the policy.
               type: value
               key: "[length(Tags)][0]"
               op: ge
               value: 14
           actions:
             - type: tag-trim
               space: 2
               preserve:
                - OwnerContact
                - Environment
                - downtime
                - custodian_status
    """
    max_tag_count = 15

    schema = utils.type_schema(
        'tag-trim',
        space={'type': 'integer'},
        preserve={'type': 'array', 'items': {'type': 'string'}})

    def __init__(self, data=None, manager=None, log_dir=None):
        super(TagTrim, self).__init__(data, manager, log_dir)

    def validate(self):
        if self.data.get('space') < 0 or self.data.get('space') > 15:
            raise FilterValidationError("Space must be between 0 and 15")

        return self

    def process(self, resources):
        self.session = self.manager.get_session()
        self.preserve = set(self.data.get('preserve', {}))
        self.space = self.data.get('space', 1)

        with self.executor_factory(max_workers=3) as w:
            list(w.map(self.process_resource, resources))

    def process_resource(self, resource):
        # get existing tags
        tags = resource.get('tags', {})

        if self.space and len(tags) + self.space <= self.max_tag_count:
            return

        # delete tags
        keys = set(tags)
        tags_to_preserve = self.preserve.intersection(keys)
        candidates = keys - tags_to_preserve

        if self.space:
            # Free up slots to fit
            remove = len(candidates) - (self.max_tag_count - (self.space + len(tags_to_preserve)))
            candidates = list(sorted(candidates))[:remove]

        if not candidates:
            self.log.warning(
                "Could not find any candidates to trim %s" % resource['id'])
            return

        TagHelper.remove_tags(self, resource, candidates)


class Notify(BaseNotify):
    batch_size = 50

    schema = {
        'type': 'object',
        'anyOf': [
            {'required': ['type', 'transport', 'to']},
            {'required': ['type', 'transport', 'to_from']}],
        'properties': {
            'type': {'enum': ['notify']},
            'to': {'type': 'array', 'items': {'type': 'string'}},
            'owner_absent_contact': {'type': 'array', 'items': {'type': 'string'}},
            'to_from': ValuesFrom.schema,
            'cc': {'type': 'array', 'items': {'type': 'string'}},
            'cc_from': ValuesFrom.schema,
            'cc_manager': {'type': 'boolean'},
            'from': {'type': 'string'},
            'subject': {'type': 'string'},
            'template': {'type': 'string'},
            'transport': {
                'oneOf': [
                    {'type': 'object',
                     'required': ['type', 'queue'],
                     'properties': {
                         'queue': {'type': 'string'},
                         'type': {'enum': ['asq']}
                     }}],
            },
        }
    }

    def __init__(self, data=None, manager=None, log_dir=None):
        super(Notify, self).__init__(data, manager, log_dir)

    def process(self, resources, event=None):
        session = utils.local_session(self.manager.session_factory)
        message = {
            'event': event,
            'account_id': session.subscription_id,
            'account': session.subscription_id,
            'region': 'all',
            'policy': self.manager.data}

        message['action'] = self.expand_variables(message)

        for batch in utils.chunks(resources, self.batch_size):
            message['resources'] = batch
            receipt = self.send_data_message(message, session)
            self.log.info("sent message:%s policy:%s template:%s count:%s" % (
                receipt, self.manager.data['name'],
                self.data.get('template', 'default'), len(batch)))

    def send_data_message(self, message, session):
        if self.data['transport']['type'] == 'asq':
            queue_uri = self.data['transport']['queue']
            return self.send_to_azure_queue(queue_uri, message, session)

    def send_to_azure_queue(self, queue_uri, message, session):
        queue_service, queue_name = StorageUtilities.get_queue_client_by_uri(queue_uri, session)
        return StorageUtilities.put_queue_message(queue_service, queue_name, self.pack(message)).id


DEFAULT_TAG = "custodian_status"


class TagDelayedAction(BaseAction):
    """Tag resources for future action.

    The optional 'tz' parameter can be used to adjust the clock to align
    with a given timezone. The default value is 'utc'.

    If neither 'days' nor 'hours' is specified, Cloud Custodian will default
    to marking the resource for action 4 days in the future.

    .. code-block :: yaml

      - policies:
        - name: vm-mark-for-stop
          resource: azure.vm
          filters:
            - type: value
              key: Name
              value: instance-to-stop-in-four-days
          actions:
            - type: mark-for-op
              op: stop
    """

    schema = utils.type_schema(
        'mark-for-op',
        tag={'type': 'string'},
        msg={'type': 'string'},
        days={'type': 'integer', 'minimum': 0, 'exclusiveMinimum': False},
        hours={'type': 'integer', 'minimum': 0, 'exclusiveMinimum': False},
        tz={'type': 'string'},
        op={'type': 'string'})

    default_template = 'Resource does not meet policy: {op}@{action_date}'

    def __init__(self, data=None, manager=None, log_dir=None):
        super(TagDelayedAction, self).__init__(data, manager, log_dir)

    def validate(self):
        op = self.data.get('op')
        if self.manager and op not in self.manager.action_registry.keys():
            raise PolicyValidationError(
                "mark-for-op specifies invalid op:%s in %s" % (
                    op, self.manager.data))

        self.tz = zoneinfo.gettz(
            Time.TZ_ALIASES.get(self.data.get('tz', 'utc')))
        if not self.tz:
            raise PolicyValidationError(
                "Invalid timezone specified %s in %s" % (
                    self.tz, self.manager.data))
        return self

    def generate_timestamp(self, days, hours):
        from c7n_azure.utils import now
        n = now(tz=self.tz)
        if days is None or hours is None:
            # maintains default value of days being 4 if nothing is provided
            days = 4
        action_date = (n + timedelta(days=days, hours=hours))
        if hours > 0:
            action_date_string = action_date.strftime('%Y/%m/%d %H%M %Z')
        else:
            action_date_string = action_date.strftime('%Y/%m/%d')

        return action_date_string

    def process(self, resources):
        self.session = self.manager.get_session()
        self.tz = zoneinfo.gettz(
            Time.TZ_ALIASES.get(self.data.get('tz', 'utc')))

        msg_tmpl = self.data.get('msg', self.default_template)

        op = self.data.get('op', 'stop')
        days = self.data.get('days', 0)
        hours = self.data.get('hours', 0)
        action_date = self.generate_timestamp(days, hours)

        self.tag = self.data.get('tag', DEFAULT_TAG)

        self.msg = msg_tmpl.format(
            op=op, action_date=action_date)

        self.log.info("Tagging %d resources for %s on %s" % (
            len(resources), op, action_date))

        with self.executor_factory(max_workers=1) as w:
            list(w.map(self.process_resource, resources))

    def process_resource(self, resource):
        # get existing tags
        tags = resource.get('tags', {})

        # add new tag
        tags[self.tag] = self.msg

        TagHelper.update_resource_tags(self, resource, tags)


class DeleteAction(BaseAction):
    schema = type_schema('delete')

    def process(self, resources):
        session = self.manager.get_session()
        #: :type: azure.mgmt.resource.ResourceManagementClient
        client = self.manager.get_client('azure.mgmt.resource.ResourceManagementClient')
        for resource in resources:
            client.resources.delete_by_id(resource['id'],
                                          session.resource_api_version(resource['id']))
