import asyncio
import json
import typing as t
from json import JSONDecodeError

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

from ..containers import Container

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


@inject
async def create_order(
    request: HttpRequest,
    paypal: "PaypalController" = Provide[Container.paypal],
    civi: "CivicrmRepositoryProtocol" = Provide[Container.civi],
) -> HttpResponse:
    if request.method != "POST":
        return HttpResponseNotAllowed(["POST"])

    data = json.loads(request.body)
    form = json.loads(data["form"])
    try:
        order = await paypal.create_order(data["payment_amount"])
        await civi.handle_donation_form_data(request, form, order["id"])
        return JsonResponse(order)
    except ValueError as e:
        return JsonResponse({"error": str(e)})


@inject
async def capture_order_payment(
    request: HttpRequest, paypal: "PaypalController" = Provide[Container.paypal]
) -> HttpResponse:
    if request.method != "POST":
        return HttpResponseNotAllowed(["POST"])

    data = json.loads(request.body)
    try:
        order = await paypal.capture_payment(data["order_id"])
        return JsonResponse(order)
    except ValueError as e:
        return JsonResponse({"error": str(e)})


@inject
async def create_subscription(
    request: HttpRequest, paypal: "PaypalController" = Provide[Container.paypal]
) -> HttpResponse:
    if request.method != "POST":
        return HttpResponseNotAllowed(["POST"])

    data = json.loads(request.body)
    try:
        product = await paypal.get_or_create_product()
        subscription = await paypal.create_subscription(
            data["payment_amount"],
            product["id"],
        )
        return JsonResponse(subscription)
    except ValueError as e:
        return JsonResponse({"error": str(e)})


@inject
async def capture_subscription(
    request: HttpRequest,
    civi: "CivicrmRepositoryProtocol" = Provide[Container.civi],
) -> HttpResponse:
    if request.method != "POST":
        return HttpResponseNotAllowed(["POST"])

    data = json.loads(request.body)
    subscription_id = data["sid"]
    try:
        form = json.loads(data["form"])
        await civi.handle_donation_form_data(request, form, subscription_id)
        return JsonResponse({"status": "OK"})
    except (JSONDecodeError, ValueError) as e:
        return JsonResponse({"error": str(e)})


# This route handles legacy IPN messages generated by subscriptions which were made outside of
# a REST API app context, and therefore cannot generate webhooks. (This is not clearly documented
# by Paypal, but IPN documentation lives here: https://developer.paypal.com/api/nvp-soap/ipn/)
# We reject all incoming messages which are not explicitly validated by Paypal, and process the
# rest as we would webhook messages pertaining to ongoing subscriptions.
@inject
@csrf_exempt
def ipn(
    request: HttpRequest,
    paypal: "PaypalController" = Provide[Container.paypal]
) -> HttpResponse:

    if request.method != "POST":
        return HttpResponseNotAllowed(["POST"])

    # IPNs deliver all transaction data to the relevant endpoint as POST data.
    # Because Paypal requires this data to remain as unchanged as possible, we refrain from
    # processing or modifying it, outside of the particular manipulation Paypal requests.
    ipn_body = request.body

    # IPN transaction data lives in the POST body, so if there isn't any, we're done here.
    if not ipn_body:
        return HttpResponse(status=400)

    try:
        ipn_valid = asyncio.run(paypal.validate_ipn(ipn_body))
    except ValueError:
        return HttpResponse(status=400)

    if ipn_valid:
        try:
            asyncio.run(paypal.process_ipn(ipn_body))
        except ValueError:
            return HttpResponse(status=400)

    return HttpResponse(status=200)


# 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
# paypal.validate_webhook().
@inject
@csrf_exempt
def webhook(
    request: HttpRequest, paypal: "PaypalController" = Provide[Container.paypal]
) -> HttpResponse:
    if request.method != "POST":
        return HttpResponseNotAllowed(["POST"])
    payload = request.body

    try:
        parsed_request_body = json.loads(payload)
    except json.decoder.JSONDecodeError:
        return HttpResponse(status=400)

    # When Paypal issues requests to our webhook endpoint, it interprets
    # a 200 OK HTTP response code 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:
        verify = asyncio.run(
            paypal.validate_webhook(request.headers, parsed_request_body)
        )
    except ValueError:
        return HttpResponse(status=400)

    if verify["verification_status"] == "SUCCESS":
        try:
            asyncio.run(paypal.process_webhook(parsed_request_body))
            return HttpResponse(status=200)
        except ValueError:
            return HttpResponse(status=400)

    return HttpResponse(status=400)
