# Copyright (c) 2020 CloudZero - ALL RIGHTS RESERVED - PROPRIETARY AND CONFIDENTIAL
# Unauthorized copying of this file and/or project, via any medium is strictly prohibited.
# Direct all questions to legal@cloudzero.com
import builtins
import functools
import inspect
import uuid
from datetime import datetime
from decimal import Decimal
from typing import Callable, Mapping, Optional, Type, TypeVar

import pyfaaster.aws.handlers_decorators_v2 as faaster
from aws_lambda_powertools.logging import Logger
from aws_lambda_powertools.tracing import Tracer
from aws_lambda_powertools.utilities.data_classes import EventBridgeEvent
from aws_lambda_powertools.utilities.data_classes.api_gateway_proxy_event import \
    APIGatewayProxyEvent
from dateutil.parser import isoparse
from toolz.dicttoolz import get_in
from voluptuous import REMOVE_EXTRA, Any, Required, Schema

from czc import event_bridge, json, type_util
from czc.api.v2.constants import CLOUDZERO_TEST_KEY
from czc.constants import PROJECT_NAME
from czc.date import get_current_timestamp
from czc.json import serializable
from czc.schema_validators import UUID

logger = Logger()
tracer = Tracer()
T = TypeVar('T')


def create_event(schema: Schema, data_fragment: Mapping) -> Mapping:
    return schema({'data': data_fragment})


def create_schema(namespace: str, feature: str, event_name: str, data_schema: Mapping,
                  correlation_id: str = None, version: int = None,
                  timestamp: Decimal = None, event_id: str = None):
    if not correlation_id:
        correlation_id = str(uuid.uuid4())
    if not version:
        version = 1
    if not timestamp:
        timestamp = get_current_timestamp()
    if not event_id:
        event_id = str(uuid.uuid4())
    return Schema({
        Required('correlationId', default=correlation_id): UUID,
        Required('feature', default=feature): str,
        Required('namespace', default=namespace): str,
        Required('eventName', default=event_name): str,
        Required('data'): data_schema,
        Required('id', default=event_id): str,
        Required('timestamp', default=str(timestamp)): str,
        Required('version', default=version): int,
    })


@faaster.namespace_aware
def _publish_event(detail_type, detail, NAMESPACE='alfa'):
    entries = event_bridge.publish_event(NAMESPACE, detail_type, PROJECT_NAME, detail)

    return {
        'event': detail,
        'event_ids': [x['EventId'] for x in entries]
    }


def _default_id_fn_v1(event: Mapping) -> Optional[str]:
    return get_in(['pathParameters', 'organizationId'], event)


def _default_id_fn_v2(event: APIGatewayProxyEvent) -> Optional[str]:
    return get_in(['requestContext', 'authorizer', 'cz_organization_id'], event)


def _default_id_fn(event, _) -> Optional[str]:
    return _default_id_fn_v1(event) or _default_id_fn_v2(event)


SAFE_EVENT = Schema({
    'body': lambda x: list(json.loads(x).keys()) if isinstance(x, str) else [],
    'headers': dict,
    'path': str,
    'pathParameters': Any(dict, None),
    'requestContext': {
        'authorizer': {
            'role': str,
            'user_id': str,
        }
    },
    'resource': str,
}, extra=REMOVE_EXTRA)


def _default_event_filter_fn(event: Mapping) -> Mapping:
    return SAFE_EVENT(event)


def _default_response_filter_fn(event: Mapping) -> Mapping:
    return {}


def publish_event_response_data(
        detail_type: str, event_name: str,
        event: Mapping, response: Mapping,
        id_fn: Callable[[Mapping, Mapping], Optional[str]] = _default_id_fn,
        event_filter_fn: Callable[[Mapping], Mapping] = _default_event_filter_fn,
        response_filter_fn: Callable[[Mapping], Mapping] = _default_response_filter_fn):
    event_data = event.raw_event if isinstance(event, APIGatewayProxyEvent) else event

    detail = {
        'cz_organization_id': id_fn(event_data, response),
        'data': {
            'event': event_filter_fn(event_data),
            'response': response_filter_fn(response),
        },
        'event_name': event_name
    }
    return _publish_event(detail_type, detail)


def publish_event_response(
        detail_type: str, event_name: str,
        id_fn: Callable[[Mapping, Mapping], Optional[str]] = _default_id_fn,
        event_filter_fn: Callable[[Mapping], Mapping] = _default_event_filter_fn,
        response_filter_fn: Callable[[Mapping], Mapping] = _default_response_filter_fn):

    def event_producer_handler(handler):
        def handler_wrapper(event, context, *args, **kwargs):
            response = handler(event, context, *args, **kwargs)
            publish_event_response_data(detail_type, event_name, event, response, id_fn=id_fn,
                                        event_filter_fn=event_filter_fn, response_filter_fn=response_filter_fn)
            return response
        return handler_wrapper
    return event_producer_handler


@faaster.environ_aware(optional=[CLOUDZERO_TEST_KEY])
def _is_test_environment(cloudzero_test_key: Optional[str]):
    return cloudzero_test_key is not None


def _is_valid_event_object(event: Any):
    return not inspect.getmodule(type(event)) is builtins


class EventMetadata(EventBridgeEvent):
    @property
    def timestamp(self) -> datetime:
        return isoparse(super().time)


def publish(event: Any):
    if not _is_valid_event_object(event):
        raise ValueError('Event should be serializable/deserializable data class '
                         '(NamedTuple, Pydantic model, @dataclass, etc.) not a dict or other builtin type.')

    detail_type = type(event).__name__
    if _is_test_environment():
        logger.info(f'Environment contains Test Key. Skipping event publish of {detail_type}')
        return

    return _publish_event(detail_type, serializable(event))


def event_consumer(event_type: Type[T], include_metadata=False):
    def _event_handler(handler):
        @functools.wraps(handler)
        def handler_wrapper(event, context, **kwargs):
            metadata = type_util.coerce(EventMetadata, event)
            detail = type_util.coerce(event_type, metadata.detail)
            return handler(event=detail,
                           **(dict(metadata=metadata) if include_metadata else {}),
                           **kwargs)

        return handler_wrapper

    return _event_handler
