# Copyright (c) 2021 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

from collections import defaultdict
from dataclasses import dataclass, field
from typing import Any, Callable, List, Mapping

from aws_lambda_powertools.utilities.data_classes.api_gateway_proxy_event import \
    APIGatewayProxyEvent
from voluptuous import ALLOW_EXTRA, All, Coerce, Range, Schema

from czc.api.v2.constants import (CURSOR_PARAMETER, DEFAULT_LIMIT,
                                  DEFAULT_SORT_KEY, DEFAULT_SORT_ORDER,
                                  LIMIT_PARAMETER, OFFSET_PARAMETER,
                                  SORT_ORDER_ASC, SORT_ORDER_DESC)
from czc.api.v2.exceptions import APIErrorCode, BadRequestError
from czc.logging import get_logger
from czc.string import vacuum_pack, vacuum_unpack

logger = get_logger()


PAGINATION_PARAMETERS_SCHEMA = Schema({
    LIMIT_PARAMETER: All(Coerce(int), Range(1)),
    OFFSET_PARAMETER: All(Coerce(int), Range(0))
}, extra=ALLOW_EXTRA)


def pagination_parameters_schema():
    return PAGINATION_PARAMETERS_SCHEMA


def _format_cursor(unpacked_cursor):
    return vacuum_pack(unpacked_cursor).decode('utf-8')  # type: ignore


def limit_offset_cursors(unpacked_current_cursor, total_count):
    limit = unpacked_current_cursor[LIMIT_PARAMETER]
    offset = unpacked_current_cursor[OFFSET_PARAMETER]

    next_offset = offset + limit
    previous_offset = offset - limit

    next_cursor = _format_cursor({
        **unpacked_current_cursor,
        OFFSET_PARAMETER: next_offset
    }) if next_offset < total_count else None

    previous_cursor = _format_cursor({
        **unpacked_current_cursor,
        OFFSET_PARAMETER: previous_offset
    }) if previous_offset >= 0 else None

    logger.debug(f'previous cursor {previous_cursor} and next cursor {next_cursor}')
    return (previous_cursor, next_cursor)


@dataclass(frozen=True)
class PaginationSorting:
    sort_key_fn: Callable[[Mapping], int] = field(default_factory=lambda: lambda x: x.get('last_updated', 0))
    sort_order: str = SORT_ORDER_ASC

    @property
    def reverse(self) -> bool:
        return self.sort_order == SORT_ORDER_DESC


@dataclass(frozen=True)
class PaginationFiltering:
    filters: Mapping[str, list] = field(default_factory=dict)
    filter_fns: Mapping[str, Callable[[Mapping, List[Any]], bool]] = field(default_factory=lambda: defaultdict(lambda: lambda _, __: True))


@dataclass(frozen=True)
class PaginationArguments:
    filtering: PaginationFiltering
    sorting: PaginationSorting
    start_index: int = 0
    limit: int = 10
    to_public: Callable[[Mapping], Mapping] = lambda x: x


@dataclass(frozen=True)
class PaginationResponse:
    items: List[Mapping]
    count_current_items: int
    count_filtered_items: int
    count_all_items: int


def paginate_items(items: List[Mapping],
                   pagination: PaginationArguments) -> PaginationResponse:

    logger.debug(f'Raw Items Returned={len(items)}')
    count_all_items = len(items)

    filter_fns = pagination.filtering.filter_fns or defaultdict(lambda: lambda _, __: True)
    filter_item_fn = lambda item: all(filter_fns[k](item, v) for (k, v) in pagination.filtering.filters.items())
    filtered_items = list(filter(filter_item_fn, items))
    count_filtered_items = len(filtered_items)

    if pagination.start_index and pagination.start_index >= count_filtered_items:
        return PaginationResponse([], 0, count_filtered_items, count_all_items)

    # NOTE: need to type: ignore the next two lines b/c of this mypy bug:  https://github.com/python/mypy/issues/6910
    sorted_filtered_items = list(sorted(filtered_items, key=lambda x: pagination.sorting.sort_key_fn(x), reverse=pagination.sorting.reverse))  # type: ignore
    public_items = [pagination.to_public(item) for item in sorted_filtered_items[pagination.start_index:pagination.start_index + pagination.limit]]  # type: ignore
    return PaginationResponse(public_items, len(public_items), count_filtered_items, count_all_items)


def pagination_response(total_count, current_count, limit, previous_cursor, next_cursor):
    return {
        'pagination': {
            'page_count': int((total_count + (limit - 1)) / limit) if total_count > 0 else 0,
            'item_count': current_count,
            'total_count': total_count,
            CURSOR_PARAMETER: {
                'next_cursor': next_cursor,
                'previous_cursor': previous_cursor,
                'has_next': next_cursor is not None,
                'has_previous': previous_cursor is not None
            },
        }
    }


def unpack_cursor(event: APIGatewayProxyEvent) -> Mapping:
    cursor = event.get_query_string_value(CURSOR_PARAMETER)
    if not cursor:
        return {}
    try:
        return vacuum_unpack(cursor)
    except Exception as exc:
        logger.error(f'Invalid cursor: {exc}')
        raise BadRequestError(message='Invalid pagination cursor; did you urlencode it?', code=APIErrorCode.INVALID_PAGINATION)


def validate_cursor(cursor_schema: Schema, cursor: Mapping, options: Mapping):
    try:
        return cursor_schema(cursor or {
            'sort_key': options.get('sort_key', DEFAULT_SORT_KEY),
            'sort_order': options.get('sort_order', DEFAULT_SORT_ORDER),
            'limit': options.get('limit', DEFAULT_LIMIT),
            'offset': 0
        })
    except Exception:
        logger.error('Invalid cursor', exc_info=True)
        raise BadRequestError('Invalid cursor')
