# 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 logging
import os
from typing import Any as TypeAny
from typing import Callable, Mapping, Optional, Tuple

import simplejson as json
from toolz.dicttoolz import dissoc, get_in
from voluptuous import ALLOW_EXTRA, All, FqdnUrl, Invalid, Maybe, Schema

from czc import event_bridge, sentry
from czc.constants import FEATURE_NAME
from czc.date import get_current_timestamp, timestamp_to_utc_iso
from czc.schema_validators import ORGANIZATION_ID
from czc.string import kebab_to_title, snake_to_kebab

logger = logging.getLogger(FEATURE_NAME)
AUDIT_DATA_KEY = 'audit_data'
PUBLISH_AUDIT_DATA = 'publish_audit_data'
DEFAULT_API_KEY_NAME = 'organization-api-key'


EXTERNAL_CHANGE_DATA = Schema({
    'url': FqdnUrl(),
    'id': str
}, required=True, extra=ALLOW_EXTRA)


AUDIT_IAPI_REQUEST = Schema({
    AUDIT_DATA_KEY: {
        'cz_organization_id': str,
        'user_id': str,
        'external_change_data': EXTERNAL_CHANGE_DATA
    }
}, required=True, extra=ALLOW_EXTRA)


AUDIT_RESPONSE = Schema({
    AUDIT_DATA_KEY: {
        PUBLISH_AUDIT_DATA: bool
    }
}, required=True, extra=ALLOW_EXTRA)


def _get_current_iso_string() -> str:
    return timestamp_to_utc_iso(get_current_timestamp())


def _get_namespace() -> Optional[str]:
    return os.environ.get('NAMESPACE')


def _validate_change_data(changes: dict) -> dict:
    if any([changes.get('external_change_data'), changes.get('internal_change_data')]):
        return changes
    else:
        raise Invalid('Either external_change_data or internal_change_data must be provided')


AUDIT_EVENT_DETAIL = Schema(All({
    'timestamp': str,
    'feature_name': str,
    'namespace': str,
    'event_name': str,
    'user_id': str,
    'cz_organization_id': ORGANIZATION_ID,
    'publisher': str,
    'target': str,
    'external_change_data': Maybe(EXTERNAL_CHANGE_DATA),
    'internal_change_data': Maybe(dict)
}, _validate_change_data), required=True, extra=ALLOW_EXTRA)


def publish_audit_event(event_type: str, target: str,
                        event_detail: dict) -> Mapping:
    source = f'cz.audit.{event_type}'

    normalized_target = snake_to_kebab(target)
    detail_type = kebab_to_title(f'{FEATURE_NAME} {normalized_target}')
    event_name = f'audit-event-{event_type}-{FEATURE_NAME}-{normalized_target}'
    namespace = _get_namespace()

    all_detail = {'timestamp': _get_current_iso_string(),
                  'feature_name': FEATURE_NAME,
                  'namespace': namespace,
                  'event_name': event_name,
                  **event_detail}

    return event_bridge.publish_event(namespace, detail_type,
                                      source, all_detail,
                                      event_detail_schema=AUDIT_EVENT_DETAIL)


def get_ids_from_authorizer(event: Mapping) -> Tuple[str, str]:
    user_id = event['requestContext']['authorizer']['user_id']
    cz_organization_id = event['requestContext']['authorizer']['organization_id']
    return (user_id, cz_organization_id)


def get_ids_from_event(event: Mapping) -> Tuple[str, str]:
    return (event[AUDIT_DATA_KEY]['user_id'], event[AUDIT_DATA_KEY]['cz_organization_id'])


def get_programmatic_event_ids(event: Mapping) -> Tuple[str, str]:
    cz_organization_id = event['requestContext']['authorizer']['cz_organization_id']
    api_key_name: str = get_in(['requestContext', 'authorizer', 'api_key_name'], event) or DEFAULT_API_KEY_NAME  # type: ignore
    return (api_key_name, cz_organization_id)


def _get_public_event_body(event):
    if 'body' in event:
        return {'event_body': json.loads(event['body'])}
    return None


def _get_audit_detail(event: Mapping, response: TypeAny,
                      lambda_context,
                      target: str,
                      get_ids_fn: Callable) -> dict:
    user_id, cz_organization_id = get_ids_fn(event)
    audit_event_detail = {
        'user_id': user_id,
        'cz_organization_id': cz_organization_id,
        'feature_name': FEATURE_NAME,
        'publisher': lambda_context.invoked_function_arn,
        'target': snake_to_kebab(target),
        'namespace': _get_namespace(),
        'external_change_data': get_in([AUDIT_DATA_KEY, 'external_change_data'], event),
        'internal_change_data': dissoc(
            response.get(AUDIT_DATA_KEY),
            PUBLISH_AUDIT_DATA) if response else _get_public_event_body(event)
    }

    return audit_event_detail


def _should_publish_audit_event(response):
    return (not response or response[AUDIT_DATA_KEY][PUBLISH_AUDIT_DATA])


def _audit_event_publisher(target: str, event_type: str,
                           get_ids_fn: Callable,
                           request_schema: Optional[Schema] = None) -> Callable:
    def audit_event_handler(handler):
        def handler_wrapper(event, context, **kwargs):
            validated_event = request_schema(event) if request_schema else event
            response = handler(validated_event, context, **kwargs)
            audit_event_detail = None

            try:
                validated_response = AUDIT_RESPONSE(response) if response else response
                should_publish_audit_event = _should_publish_audit_event(validated_response)

                if should_publish_audit_event:
                    audit_event_detail = _get_audit_detail(
                        validated_event, validated_response,
                        context,
                        target, get_ids_fn)

                    publish_audit_event(event_type, target,
                                        audit_event_detail)
            except Invalid as exc:
                message = {
                    'decorated_lambda': context.invoked_function_arn,
                    'exception': str(exc)
                }

                tags = {
                    'alarm_name': 'invalid-audited-lambda-response'
                }
                sentry.send(message, tags=tags)
            except Exception as exc:
                logger.exception(
                    f'Failed to publish audit event {audit_event_detail} at {_get_current_iso_string()}, {exc}')

            return dissoc(response, AUDIT_DATA_KEY) if isinstance(response, dict) else response
        return handler_wrapper
    return audit_event_handler


def iapi_audit_event_publisher(target: str,
                               event_type: str = 'configuration') -> TypeAny:
    return _audit_event_publisher(target, event_type,
                                  get_ids_from_event,
                                  request_schema=AUDIT_IAPI_REQUEST)


def public_api_audit_event_publisher(target: str,
                                     event_type: str = 'configuration') -> TypeAny:
    return _audit_event_publisher(target, event_type, get_ids_from_authorizer)


def programmatic_api_audit_event_publisher(target: str,
                                           event_type: str = 'configuration') -> TypeAny:
    return _audit_event_publisher(target, event_type, get_programmatic_event_ids)
