import asyncio
import json
import typing as t

import altcha
from dependency_injector.wiring import Provide, inject
from django.http import HttpRequest, HttpResponse, HttpResponseNotAllowed, JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django_altcha import get_hmac_key

from .. import prometheus
from ..containers import Container
from .controller import (StripeAddressData, StripeCustomerData, StripeMetadata,
                         StripePaymentIntentData, StripeShippingData)
from .forms import StripeDonationForm

if t.TYPE_CHECKING:  # pragma: no cover
    from ..civicrm.repository import CivicrmRepositoryProtocol
    from .controller import StripeController


@inject
async def process(
    request: HttpRequest,
    stripe: "StripeController" = Provide[Container.stripe],
    civi: "CivicrmRepositoryProtocol" = Provide[Container.civi],
) -> HttpResponse:
    if request.method != "POST":
        return HttpResponseNotAllowed(['POST'])
    data = json.loads(request.body)
    form = StripeDonationForm(data=data)

    if not form.is_valid():
        return JsonResponse({"error": form.errors.as_json()})

    cohort = data.get("cohort", "undefined")
    try:
        verified, error = altcha.verify_solution(
            payload=data["payload"],
            hmac_key=get_hmac_key(),
            check_expires=True,
        )
    except Exception as e:
        return JsonResponse({"error": str(e)})
    if not verified:
        prometheus.DonationProcessingError.labels("altcha", cohort).inc()
        return JsonResponse({"error": "ALTCHA payload incorrect"})

    # In order to ensure that the client secret necessary for Stripe transactions is not
    # generated until after form and ALTCHA payload validation occurs, we now generate it
    # here, and only here. One-time payments and recurring payments are instantiated
    # differently, and return different payment objects, but both contain a client secret.
    metadata = StripeMetadata(
        cohort=cohort,
        perk=data.get("perk"),
        perk_option=next((v for k, v in data.items() if k.startswith("perk_option--")), ""),
    )
    if form.cleaned_data["recurring"]:
        try:
            plan = await stripe.get_or_create_monthly_plan(form.cleaned_data["amount"])
            customer = await stripe.create_customer(
                StripeCustomerData(
                    name=form.cleaned_data["first_name"] + " " + form.cleaned_data["last_name"],
                    email=form.cleaned_data["email"],
                    address=StripeAddressData(
                        city=form.cleaned_data["city"],
                        country=form.cleaned_data["country"],
                        line1=form.cleaned_data["street_address"],
                        line2=form.cleaned_data["apartment_number"],
                        postal_code=form.cleaned_data["postal_code"],
                        state=form.cleaned_data["state"],
                    )
                )
            )
            sub = await stripe.create_subscription(customer.id, plan.id, metadata)
            await civi.handle_donation_form_data(request, data, sub.latest_invoice.id)
            return JsonResponse(
                {
                    "client_secret": sub.latest_invoice.payment_intent.client_secret,
                }
            )
        except ValueError as e:
            return JsonResponse({"error": str(e)})
    else:
        try:
            name = form.cleaned_data["first_name"] + " " + form.cleaned_data["last_name"]
            intent = await stripe.create_payment_intent(
                StripePaymentIntentData(
                    amount=form.cleaned_data["amount"],
                    receipt_email=form.cleaned_data["email"],
                    metadata=metadata,
                    shipping=StripeShippingData(
                        name=name,
                        address=StripeAddressData(
                            city=form.cleaned_data["city"],
                            country=form.cleaned_data["country"],
                            line1=form.cleaned_data["street_address"],
                            line2=form.cleaned_data["apartment_number"],
                            postal_code=form.cleaned_data["postal_code"],
                            state=form.cleaned_data["state"],
                        )
                    )
                )
            )
            prometheus.StripePaymentIntentCount.labels("success", cohort).inc()
            await civi.handle_donation_form_data(request, data, intent.id)
            return JsonResponse(
                {
                    "client_secret": intent["client_secret"]
                }
            )
        except ValueError as e:
            prometheus.StripePaymentIntentCount.labels("failure", cohort).inc()
            return JsonResponse({"error": str(e)})


# This route handles incoming webhook requests originating from Paypal servers.
# Because these requests don't originate from our server, no CSRF token is
# generated, and therefore this method receives the rare CSRF exemption.
# In exchange, we explicitly verify this webhook's legitimacy via the code in
# stripe.validate_webhook().
@inject
@csrf_exempt
def webhook(
    request: HttpRequest,
    stripe: "StripeController" = Provide[Container.stripe],
) -> HttpResponse:
    try:
        asyncio.run(stripe.validate_webhook(request))
    except (KeyError, ValueError):
        return HttpResponse(status=400)

    # When Stripe issues requests to our webhook endpoint, it interprets
    # any HTTP response code in the 2xx range as a success, and anything
    # else as a failure. (It will reissue the same request several times
    # if it believes a request it has issued has failed.) There is no
    # nuance in how it interprets failures, so we simply issue - as their
    # webhook code samples suggest - a 400 Bad Request if exceptions
    # are encountered, and 200 OK if all goes well.

    try:
        asyncio.run(stripe.process_webhook(request))
    except ValueError:
        return HttpResponse(status=400)

    return HttpResponse(status=200)
