# 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 typing
import uuid
from collections.abc import Callable, Mapping
from datetime import datetime
from decimal import Decimal
from typing import Optional, TypeVar

import pyfaaster.aws.handlers_decorators_v2 as faaster
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.logging import get_logger, get_tracer
from czc.schema_validators import UUID

logger = get_logger()
tracer = get_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: typing.Any):
    return inspect.getmodule(type(event)) is not builtins


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


def publish(event: typing.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 None

    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
