"""Idempotent decorator for endpoints.

As inspired by stripe's API documentation.

Stripe's idempotency works by saving the resulting status
code and body of the first request made for any given
idempotency key, regardless of whether it succeeded or failed.
Subsequent requests with the same key return the same result,
including 500 errors.

An idempotency key is a unique value generated by the client
which the server uses to recognize subsequent retries of the
same request. How you create unique keys is up to you, but we
suggest using V4 UUIDs, or another random string with enough
entropy to avoid collisions.

Keys are eligible to be removed from the system after they're
at least 24 hours old, and a new request is generated if a key
is reused after the original has been pruned. The idempotency
layer compares incoming parameters to those of the original
request and errors unless they're the same to prevent
accidental misuse.

Results are only saved if an API endpoint started executing.
If incoming parameters failed validation, or the request
conflicted with another that was executing concurrently, no
idempotent result is saved because no API endpoint began
execution. It is safe to retry these requests.

Cache should support the same interface as a python dict.
This should also support a configurable TTL.
"""
from flask import current_app, make_response, request
from werkzeug.exceptions import BadRequest

import wrapt

_WERKZEUG_IDEMPOTENT_KEY = "HTTP_IDEMPOTENT_KEY"
_IDEMPOTENT_HEADER = "Idempotent-Key"


def idempotent():
    """
    Validation of the data passed to the endpoint
    should occur after this decorator.
    If the request has been retried and a response
    exists in the cache, we can skip validating its
    contents.
    """

    @wrapt.decorator()
    def wrapper(wrapped, instance, args, kwargs):
        # Require specifying the Idempotent-Key in the
        # header.
        idempotent_key = request.headers.get(_IDEMPOTENT_HEADER)
        if not idempotent_key:
            raise BadRequest("Missing 'Idempotent-Key' field in request header")

        resp = current_app.cache.get(idempotent_key)

        # This will return the same request as when it
        # was processed by the 'after_request' hook.
        # It will also refresh the request's ttl.
        if resp:
            if resp["request"] != request.data:
                raise BadRequest(
                    "'Idempotent-Key' exists but the request data does not match"
                )

            response = make_response(resp["response"], resp["status_code"])
            response.headers["Content-Type"] = resp["content_type"]
            response.headers[_IDEMPOTENT_HEADER] = idempotent_key
            return response

        return wrapped(*args, **kwargs)

    return wrapper


def attach(app, cache):
    """ Attach idempotent middleware for predictable requests."""

    @app.after_request
    def hook(response):
        """ After the request we write the response to cache."""
        # Do not write responses that are in the 4xx range.
        # 4xx status codes are client side errors, we can
        # avoid denial of service by our cache filling
        # with unauthorized requests and client faults.
        if 400 <= response.status_code < 500:
            return response

        # Add the idempotent header to the request
        idempotent_key = request.headers.get(_IDEMPOTENT_HEADER)
        if not idempotent_key:
            return response

        # Write the status_code, content_type and data of the
        # response to cache.set.
        cache[idempotent_key] = {
            "status_code": response.status_code,
            "content_type": response.content_type,
            "response": response.data,
            "request": request.data,
        }

        response.headers[_IDEMPOTENT_HEADER] = idempotent_key
        return response
