# mypy: disable-error-code=no-untyped-def
import json
import logging
from random import random
from typing import Type

from dependency_injector.wiring import Provide, inject
from django import forms
from django.conf import settings
from django.http import HttpRequest, HttpResponse, HttpResponseNotAllowed, JsonResponse, QueryDict
from django.shortcuts import redirect, render
from django.urls import reverse_lazy
from django.views.generic import FormView, TemplateView
from django_altcha import AltchaChallengeView, get_altcha_challenge
from django_ratelimit.core import get_usage

from . import prometheus
from .containers import Container
from .forms import CryptocurrencyForm, DonorInfoFormA, DonorInfoFormB
from .utils import get_client_ip

logger = logging.getLogger(__package__)


class DonateFormView(FormView):
    template_name = "donate.html.jinja"
    success_url = reverse_lazy("donate_thank_you")

    def get_form_class(self) -> Type[forms.BaseForm]:
        if self.request.session["cohort"] == "A":
            return DonorInfoFormA
        else:
            return DonorInfoFormB

    def get(self, request: HttpRequest, *args, **kwargs):
        cohorts = tuple("AB")
        if "cohort" not in self.request.session:
            if settings.APP_ENV in ["dev", "staging"] and "cohort" in request.GET and \
                    request.GET["cohort"] in cohorts:
                cohort = request.GET["cohort"]
            else:
                cohort = cohorts[int(random() <= settings.COHORTS_TEST_RATIO)]
            self.request.session.create()
            self.request.session["cohort"] = cohort
            prometheus.Cohort.labels(cohort).inc()
        return super().get(request, *args, **kwargs)

    def get_context_data(self, civi=Provide[Container.civi], **kwargs):
        context = super().get_context_data(**kwargs)
        context.update(
            {
                "donate_single_prices": settings.DONATE_PRICES["single"],
                "donate_monthly_prices": settings.DONATE_PRICES["monthly"],
                "paypal_client_id": settings.PAYPAL_CLIENT_ID,
                "stripe_api_key": settings.STRIPE_API_KEY,
                "crypto_wallets": settings.CRYPTO_WALLET_ADDRESSES,
                "is_yec": civi.is_yec_sync(),
                "cohort": self.request.session["cohort"],
            }
        )

        return context

    @inject
    def form_valid(self, form: DonorInfoFormA | DonorInfoFormB, civi=Provide[Container.civi]):
        civi.report_donation_sync(form)
        return super().form_valid(form)

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

        jsonData = json.loads(request.body)
        data = QueryDict(jsonData["form"].encode("ASCII"))

        if request.session["cohort"] == "A":
            form = DonorInfoFormA(data)
        else:
            form = DonorInfoFormB(data)

        if form.is_valid():
            if form.cleaned_data["email_updates"]:
                civi.newsletter_signup_sync({
                    "email_address": form.cleaned_data["email_address"],
                    "first_name": form.cleaned_data["first_name"],
                    "last_name": form.cleaned_data["last_name"],
                })
            return JsonResponse({})
        else:
            # Log IP of users who fail form validation, for cases of one IP causing bulk failures
            client_ip = get_client_ip(request.META)
            logger.error(
                "User (IP %s, cohort %s) submitted form, but its content caused a backend validation error: %s",  # noqa: E501
                client_ip,
                request.session["cohort"],
                form.errors.as_data()
            )
            return JsonResponse({"errors": form.errors})


class CryptocurrencyView(FormView):
    form_class = CryptocurrencyForm
    template_name = "cryptocurrency.html.jinja"
    success_url = reverse_lazy("donate_thank_you")

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context.update(
            {
                "donate_single_prices": settings.DONATE_PRICES["single"],
                "crypto_wallets": settings.CRYPTO_WALLET_ADDRESSES,
                "btcpayserver_url": settings.BTCPAYSERVER_URL,
            }
        )
        return context


class DonateThankYouView(TemplateView):
    template_name = "donate_thank_you.html.jinja"

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context.update(
            {
                "stripe_api_key": settings.STRIPE_API_KEY,
            }
        )
        return context


class PrivacyPolicyView(TemplateView):
    template_name = "privacy_policy.html.jinja"


class StateDisclosuresView(TemplateView):
    template_name = "state_disclosures.html.jinja"


class RefundPolicyView(TemplateView):
    template_name = "refund_policy.html.jinja"


def handler404(request, exception):
    return render(request, "errors/404.html.jinja", status=404)


def handler500(request):
    return render(request, "errors/500.html.jinja", status=500)


def handler503(request):
    return render(request, "errors/503.html.jinja", status=503)


def RateLimiter(request, exception):
    client_ip = get_client_ip(request.META)
    logger.info("Rate limiter triggered on path {} via method {} by IP {}".format(
        request.path,
        request.method,
        client_ip
    ))
    cohort = request.session.get("cohort", "undefined")
    prometheus.RateLimiter.labels("ip", request.method, cohort).inc()
    return HttpResponse("429: Rate Limit Exceeded", status=429)


def redirect_view(request, destpath: str = "/"):
    response = redirect(destpath, permanent=True)
    return response


# Provide a challenge of increasing difficulty for the altcha widget up to the default maximum
# of 1000000, based on the number of requests in the last minute. When the limit is exceeded,
# an error is returned instead, which causes the widget to display an error message.
class AltchaChallengeViewRateLimited(AltchaChallengeView):

    def get(self, request: HttpRequest, *args, **kwargs) -> JsonResponse:
        expires = kwargs.get("expires", self.expires)

        usage_per_min = get_usage(
            request=request,
            group="altcha_challenge_per_min",
            key="ip",
            rate="6/m",
            increment=True
        )
        if usage_per_min and usage_per_min["should_limit"]:
            return JsonResponse({}, status=429)

        usage_per_hour = get_usage(
            request=request,
            group="altcha_challenge_per_hour",
            key="ip",
            rate="48/h",
            increment=True
        )
        if usage_per_hour and usage_per_hour["should_limit"]:
            return JsonResponse({}, status=429)

        # generate a "max_number" challenge difficulty value
        # the minimum should be about 25K (lower than that is too easy and isn't
        # much faster) while the maximum is 1M because higher causes issues with
        # the frontend ; this generates a value for both per minute and per hour
        # usages, and uses whichever value is highest
        max_number_per_min = min(usage_per_min["count"] ** 2 * 27500, 1000000)
        max_number_per_hour = min(usage_per_hour["count"] ** 2 * 750, 1000000)
        max_number = max(max_number_per_min, max_number_per_hour)

        challenge = get_altcha_challenge(max_number=max_number, expires=expires)
        return JsonResponse(challenge.__dict__)
