# -*- coding: utf-8 -*-
# Copyright (c) 2016-present, CloudZero, Inc. All rights reserved.
# Licensed under the BSD-style license. See LICENSE file in the project root for full license information.

"""
Main module used for enriching a resource, that is, discovering its properties and
explicit relationships given its CZRN identity, service type, and resource type.
"""

import functools
import types

import lambda_tools
import reactor.common.czrn as czrn
import reactor.features.accounts.exceptions as exceptions
import reactor.features.resources.enrich_plugins.plugin_loader as plugin_loader
import reactor.features.resources.resource as resource
from reactor.aws import lambda_utils
from reactor.aws.kinesis import batch_and_write_records_to_stream
from reactor.database.models import dynamodb
from reactor.features.accounts.account_context import build_cloud_account_context
from reactor.features.resources import observations
from reactor.aws.dynamodb import deserialize_dynamodb_stream_event_record

logger = lambda_tools.setup_logging('reactor')

account_contexts = {}


def update_discovery_map_cooldown(service_type, resource_type, cooldown):
    discovery_map_dbh = dynamodb.CZReactorDiscoveryMap()
    return discovery_map_dbh.update_cooldown(service_type, resource_type, cooldown)


def enrich_resource_observations(resource_observations, namespace, reactor_account_id, configuration):
    """
    Enrich a list of resources, calling specific resource enrichment plugins if they exist

    Args:
        resource_observations (list): A list of dicts containing DynamoDB Stream events. Each dict is assumed to
            be something like
            {
                "ApproximateCreationDateTime": 1527797580.0,
                "Keys": {
                  "resource_id": {
                    "S": "czrn:aws:cz-cloud-provider:us-east-1::aws-service:lambda.amazonaws.com"
                  }
                },
                "NewImage": {
                  "ingest_time": {
                    "S": "2018-05-31 20:13:00.000"
                  },
                  "last_seen": {
                    "N": "1527797579"
                  },
                  "cz_account_id": {
                    "S": "cdc0c1d6-c3c2-48d8-9022-0a6eee6bedef"
                  },
                  "resource_id": {
                    "S": "czrn:aws:cz-cloud-provider:us-east-1::aws-service:lambda.amazonaws.com"
                  },
                  "res_id_count": {
                    "N": "1"
                  },
                  "ttl": {
                    "N": "1527801212"
                  }
                },
                "SequenceNumber": "100000000029245419929",
                "SizeBytes": 283,
                "StreamViewType": "NEW_AND_OLD_IMAGES"
            }
        namespace (str): A human-readable identifier for the current Reactor installation in AWS.
            Distinct from Reactor ID, which is a UUID shared with Core.
        reactor_account_id (str): The 10 digit AWS account
        configuration (dict): Reactor configuration

    Returns:
        None
    """
    enriched_resources = []
    for resource_observation in resource_observations:
        resource_record = deserialize_dynamodb_stream_event_record(resource_observation)
        cz_account_id = get_cz_account_id_from_resource_record(resource_record)

        # This is a pattern we should probably use more globally in reactor - many lambdas need this
        account_context = account_contexts.get(cz_account_id)
        if not account_context:
            account_context = build_cloud_account_context(namespace, configuration, reactor_account_id,
                                                          cz_account_id=cz_account_id)
            account_contexts[cz_account_id] = account_context

        unenriched_resource = {}
        resource_id = get_resource_id_from_resource_record(resource_record)
        service_type = czrn.extract_service_type(resource_id)
        resource_type = czrn.extract_resource_type(resource_id)

        unenriched_resource['res_id'] = resource_id
        unenriched_resource['service_type'] = czrn.extract_service_type(resource_id)
        unenriched_resource['resource_type'] = czrn.extract_resource_type(resource_id)
        unenriched_resource['cloud_local_id'] = czrn.extract_cloud_local_id(resource_id)
        unenriched_resource['resource_name'] = czrn.extract_cloud_local_id(resource_id)
        unenriched_resource['cloud_account_id'] = czrn.extract_account_id(resource_id)
        unenriched_resource['environment_id'] = cz_account_id
        unenriched_resource['region'] = czrn.extract_region(resource_id)
        unenriched_resource['observation_count'] = get_res_id_count_from_resource_record(resource_record)
        unenriched_resource['last_observed'] = get_last_seen_from_resource_record(resource_record)
        unenriched_resource['reactor_account_id'] = reactor_account_id

        enrich_plugin = plugin_loader.find_enrichment_plugin(service_type, resource_type)

        if isinstance(enrich_plugin, types.ModuleType):
            logger.info(f"Executing enrichment plugin for {resource_id}:{unenriched_resource['last_observed']}")
            enriched_resources = _run_enrichment_plugin(account_context, enrich_plugin, unenriched_resource)
            try:
                _record_relationships(unenriched_resource, enriched_resources, namespace)
            except Exception as e:
                logger.exception(e)
        else:
            logger.debug(f"No enrich functions for {resource_id}")
            enriched_resource = resource.create_new_enriched_resource(
                resource_id=unenriched_resource['res_id'],
                service_type=service_type,
                resource_type=resource_type,
                cloud_local_id=unenriched_resource['cloud_local_id'],
                cloud_region=unenriched_resource['region'],
                cloud_account_id=unenriched_resource['cloud_account_id'],
                last_observed=unenriched_resource['last_observed'],
                observation_count=unenriched_resource['observation_count'],
                cz_account_id=unenriched_resource['environment_id'],
                raw_data={"message": "Not Available"},
                disposition=resource.DISPOSITION_NOT_SUPPORTED
            )
            enriched_resources.append(enriched_resource)

    put_enriched_resources_into_kinesis_stream(enriched_resources, namespace)


def get_resource_id_from_resource_record(resource_record):
    return resource_record['resource_id']


def get_last_seen_from_resource_record(resource_record):
    return resource_record['last_seen']


def get_cz_account_id_from_resource_record(resource_record):
    return resource_record['cz_account_id']


def get_res_id_count_from_resource_record(resource_record):
    observation_count = 0
    try:
        observation_count = resource_record['res_id_count']
    except KeyError:
        pass
    return observation_count


def put_enriched_resources_into_kinesis_stream(enriched_resources, namespace):
    stream_name = lambda_utils.get_fully_resolved_kinesis_stream_name(namespace, 'enriched-resources-stream')
    batch_and_write_records_to_stream(enriched_resources, stream_name, partition_key_key='res_id')
    logger.info(f"Enriched {len(enriched_resources)} and placed them on the {stream_name} Kinesis stream")


def _record_relationships(resource_stream_event, enriched_resources, namespace):
    message_source_environment_id = resource_stream_event['environment_id']
    relationship_resource_ids = set([
        relationship_res_id for resource in enriched_resources
        for relationship_res_id in resource.get('raw_data', {}).get('relationships', [])
    ])
    resource_count = len(relationship_resource_ids)
    if resource_count > 0:
        logger.info(f'A total of {resource_count} resource(s) found during enrichment, recording them.')
        observations.write_observations_to_kinesis(relationship_resource_ids, message_source_environment_id, namespace)


def _run_enrichment_plugin(account_context, enrich_plugin, resource_stream_event):
    data = {"message": "Not Available"}
    enriched_resources = []
    resource_id = resource_stream_event['res_id']

    # noinspection PyBroadException
    try:
        # Detect if observed resource exists outside of the currently connected cloud account
        # We are unable at this time to enrich such things
        cloud_account_id = account_context['connected_account_id']
        if cloud_account_id != resource_stream_event['cloud_account_id']:
            data = {"message": "Observed resource exists outside account",
                    "true_cloud_account_id": resource_stream_event['cloud_account_id']}
            enriched_resources.append(resource.create_from_stream_event(resource_stream_event,
                                                                        resource.DISPOSITION_EXTERNAL_ACCOUNT,
                                                                        data))
            logger.info(f"Outside account resource observed: {resource_id}")
            return enriched_resources

        resource_to_reconcile = enrich_plugin.enrich_resource(account_context, resource_stream_event)
        enriched_resources.append(resource_to_reconcile)

    except exceptions.CloudAccountMetadataLookupException:
        logger.warning(f'No cloud account data available for {resource_id}, Cannot enrich at this time.')
        enriched_resources.append(resource.create_from_stream_event(resource_stream_event,
                                                                    resource.DISPOSITION_NO_DATA,
                                                                    data))
    except Exception as err:
        logger.exception(f'Could not enrich {resource_id} ({err})')
        logger.error(f'Parsed CZRN: {czrn.extract_all_components(resource_id)}')
        enriched_resources.append(resource.create_from_stream_event(resource_stream_event,
                                                                    resource.DISPOSITION_ERROR,
                                                                    data))
    return enriched_resources


@functools.lru_cache(maxsize=512)
def _get_discovery_map_cached(service_type, resource_type):
    discovery_map_dbh = dynamodb.CZReactorDiscoveryMap()
    return discovery_map_dbh.get_record(service_type, resource_type)
