"""
Lambda event construction and generation
"""

import base64
import logging
from datetime import datetime
from time import time
from typing import Any, Dict

from samcli.local.apigw.path_converter import PathConverter
from samcli.local.events.api_event import (
    ApiGatewayLambdaEvent,
    ApiGatewayV2LambdaEvent,
    ContextHTTP,
    ContextIdentity,
    RequestContext,
    RequestContextV2,
)

LOG = logging.getLogger(__name__)


def construct_v1_event(
    flask_request, port, binary_types, stage_name=None, stage_variables=None, operation_name=None
) -> Dict[str, Any]:
    """
    Helper method that constructs the Event to be passed to Lambda

    :param request flask_request: Flask Request
    :param port: the port number
    :param binary_types: list of binary types
    :param stage_name: Optional, the stage name string
    :param stage_variables: Optional, API Gateway Stage Variables
    :return: JSON object
    """

    identity = ContextIdentity(source_ip=flask_request.remote_addr)

    endpoint = PathConverter.convert_path_to_api_gateway(flask_request.endpoint)
    method = flask_request.method
    protocol = flask_request.environ.get("SERVER_PROTOCOL", "HTTP/1.1")
    host = flask_request.host

    request_data = flask_request.get_data()

    request_mimetype = flask_request.mimetype

    is_base_64 = _should_base64_encode(binary_types, request_mimetype)

    if is_base_64:
        LOG.debug("Incoming Request seems to be binary. Base64 encoding the request data before sending to Lambda.")
        request_data = base64.b64encode(request_data)

    if request_data:
        # Flask does not parse/decode the request data. We should do it ourselves
        # Note(xinhol): here we change request_data's type from bytes to str and confused mypy
        # We might want to consider to use a new variable here.
        request_data = request_data.decode("utf-8")

    query_string_dict, multi_value_query_string_dict = _query_string_params(flask_request)

    context = RequestContext(
        resource_path=endpoint,
        http_method=method,
        stage=stage_name,
        identity=identity,
        path=endpoint,
        protocol=protocol,
        domain_name=host,
        operation_name=operation_name,
    )

    headers_dict, multi_value_headers_dict = _event_headers(flask_request, port)

    event = ApiGatewayLambdaEvent(
        http_method=method,
        body=request_data,
        resource=endpoint,
        request_context=context,
        query_string_params=query_string_dict,
        multi_value_query_string_params=multi_value_query_string_dict,
        headers=headers_dict,
        multi_value_headers=multi_value_headers_dict,
        path_parameters=flask_request.view_args,
        path=flask_request.path,
        is_base_64_encoded=is_base_64,
        stage_variables=stage_variables,
    )

    event_dict = event.to_dict()
    LOG.debug("Constructed Event 1.0 to invoke Lambda. Event: %s", event_dict)
    return event_dict


def construct_v2_event_http(
    flask_request,
    port,
    binary_types,
    stage_name=None,
    stage_variables=None,
    route_key=None,
    request_time_epoch=int(time()),
    request_time=datetime.utcnow().strftime("%d/%b/%Y:%H:%M:%S +0000"),
) -> Dict[str, Any]:
    """
    Helper method that constructs the Event 2.0 to be passed to Lambda

    https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html

    :param request flask_request: Flask Request
    :param port: the port number
    :param binary_types: list of binary types
    :param stage_name: Optional, the stage name string
    :param stage_variables: Optional, API Gateway Stage Variables
    :param route_key: Optional, the route key for the route
    :return: JSON object
    """
    method = flask_request.method

    request_data = flask_request.get_data()

    request_mimetype = flask_request.mimetype

    is_base_64 = _should_base64_encode(binary_types, request_mimetype)

    if is_base_64:
        LOG.debug("Incoming Request seems to be binary. Base64 encoding the request data before sending to Lambda.")
        request_data = base64.b64encode(request_data)

    if request_data is not None:
        # Flask does not parse/decode the request data. We should do it ourselves
        request_data = request_data.decode("utf-8")

    query_string_dict = _query_string_params_v_2_0(flask_request)

    cookies = _event_http_cookies(flask_request)
    headers = _event_http_headers(flask_request, port)
    context_http = ContextHTTP(method=method, path=flask_request.path, source_ip=flask_request.remote_addr)
    context = RequestContextV2(
        http=context_http,
        route_key=route_key,
        stage=stage_name,
        request_time_epoch=request_time_epoch,
        request_time=request_time,
    )

    event = ApiGatewayV2LambdaEvent(
        route_key=route_key,
        raw_path=flask_request.path,
        raw_query_string=flask_request.query_string.decode("utf-8"),
        cookies=cookies,
        headers=headers,
        query_string_params=query_string_dict,
        request_context=context,
        body=request_data,
        path_parameters=flask_request.view_args,
        is_base_64_encoded=is_base_64,
        stage_variables=stage_variables,
    )

    event_dict = event.to_dict()
    LOG.debug("Constructed Event Version 2.0 to invoke Lambda. Event: %s", event_dict)
    return event_dict


def _query_string_params(flask_request):
    """
    Constructs an APIGW equivalent query string dictionary

    Parameters
    ----------
    flask_request request
        Request from Flask

    Returns dict (str: str), dict (str: list of str)
    -------
        Empty dict if no query params where in the request otherwise returns a dictionary of key to value

    """
    query_string_dict = {}
    multi_value_query_string_dict = {}

    # Flask returns an ImmutableMultiDict so convert to a dictionary that becomes
    # a dict(str: list) then iterate over
    for query_string_key, query_string_list in flask_request.args.lists():
        query_string_value_length = len(query_string_list)

        # if the list is empty, default to empty string
        if not query_string_value_length:
            query_string_dict[query_string_key] = ""
            multi_value_query_string_dict[query_string_key] = [""]
        else:
            query_string_dict[query_string_key] = query_string_list[-1]
            multi_value_query_string_dict[query_string_key] = query_string_list

    return query_string_dict, multi_value_query_string_dict


def _query_string_params_v_2_0(flask_request):
    """
    Constructs an APIGW equivalent query string dictionary using the 2.0 format
    https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html#2.0

    Parameters
    ----------
    flask_request request
        Request from Flask

    Returns dict (str: str)
    -------
        Empty dict if no query params where in the request otherwise returns a dictionary of key to value

    """
    query_string_dict = {}

    # Flask returns an ImmutableMultiDict so convert to a dictionary that becomes
    # a dict(str: list) then iterate over
    query_string_dict = {
        query_string_key: ",".join(query_string_list)
        for query_string_key, query_string_list in flask_request.args.lists()
    }

    return query_string_dict


def _event_headers(flask_request, port):
    """
    Constructs an APIGW equivalent headers dictionary

    Parameters
    ----------
    flask_request request
        Request from Flask
    int port
        Forwarded Port
    cors_headers dict
        Dict of the Cors properties

    Returns dict (str: str), dict (str: list of str)
    -------
        Returns a dictionary of key to list of strings

    """
    headers_dict = {}
    multi_value_headers_dict = {}

    # Multi-value request headers is not really supported by Flask.
    # See https://github.com/pallets/flask/issues/850
    for header_key in flask_request.headers.keys():
        headers_dict[header_key] = flask_request.headers.get(header_key)
        multi_value_headers_dict[header_key] = flask_request.headers.getlist(header_key)

    headers_dict["X-Forwarded-Proto"] = flask_request.scheme
    multi_value_headers_dict["X-Forwarded-Proto"] = [flask_request.scheme]

    headers_dict["X-Forwarded-Port"] = str(port)
    multi_value_headers_dict["X-Forwarded-Port"] = [str(port)]
    return headers_dict, multi_value_headers_dict


def _event_http_cookies(flask_request):
    """
    All cookie headers in the request are combined with commas.

    https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html

    Parameters
    ----------
    flask_request request
        Request from Flask

    Returns list
    -------
        Returns a list of cookies

    """
    cookies = []
    for cookie_key in flask_request.cookies.keys():
        cookies.append(f"{cookie_key}={flask_request.cookies.get(cookie_key)}")
    return cookies


def _event_http_headers(flask_request, port):
    """
    Duplicate headers are combined with commas.

    https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html

    Parameters
    ----------
    flask_request request
        Request from Flask

    Returns list
    -------
        Returns a list of cookies

    """
    headers = {}
    # Multi-value request headers is not really supported by Flask.
    # See https://github.com/pallets/flask/issues/850
    for header_key in flask_request.headers.keys():
        headers[header_key] = flask_request.headers.get(header_key)

    headers["X-Forwarded-Proto"] = flask_request.scheme
    headers["X-Forwarded-Port"] = str(port)
    return headers


def _should_base64_encode(binary_types, request_mimetype):
    """
    Whether or not to encode the data from the request to Base64

    Parameters
    ----------
    binary_types list(basestring)
        Corresponds to self.binary_types (aka. what is parsed from SAM Template
    request_mimetype str
        Mimetype for the request

    Returns
    -------
        True if the data should be encoded to Base64 otherwise False

    """
    return request_mimetype in binary_types or "*/*" in binary_types