import json
import typing as t

from dependency_injector.wiring import Provide, inject
from django import forms
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator
from django.urls import reverse_lazy
from django.utils.translation import gettext as _
from django_altcha import AltchaField
from email_validator import EmailNotValidError, validate_email

from . import container, prometheus
from .civicrm.models import Perk
from .civicrm.repository import CivicrmRepositoryProtocol
from .containers import Container


class PerkField(forms.ChoiceField):

    def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
        no_gift = ("no_gift", "")
        perk_data = {perk.id: perk for perk in container.civi().get_active_perks_synced()}
        kwargs['choices'] = [no_gift] + [(id, perk.name) for id, perk in perk_data.items()]
        kwargs['initial'] = "no_gift"
        kwargs['required'] = True
        super().__init__(*args, **kwargs)

    @inject
    def to_python(
        self,
        value: t.Any,
        civi: CivicrmRepositoryProtocol = Provide[Container.civi],
    ) -> t.Literal["no_gift"] | Perk:
        self.validate(value)

        if value == "no_gift":
            return "no_gift"
        else:
            perk = civi.get_perk_by_id_sync(value)  # type: ignore
            return t.cast(Perk, perk)

    def validate(self, value: t.Any) -> None:
        for perk_id, __ in self.choices:
            if (type(value) is Perk and value.id == perk_id) \
                    or value == perk_id:
                return

        raise ValidationError(
            self.error_messages["invalid_choice"],
            code="invalid_choice",
            params={"value": value},
        )


class DonorInfoForm(forms.Form):
    defaultDonation = {
        "single": settings.DEFAULT_DONATION["single"],
        "monthly": settings.DEFAULT_DONATION["monthly"],
    }
    minimum_donation = {
        "cents": container.civi().minimum_donation_sync(),  # type: ignore
        "dollars": container.civi().minimum_donation_sync() // 100,  # type: ignore
        "display": "$"
        + str(int(container.civi().minimum_donation_sync() // 100)),  # type: ignore
    }
    custom_donation = forms.FloatField(
        required=True,
        validators=[
            MinValueValidator(container.civi().minimum_donation_sync() // 100),  # type: ignore
        ],
    )
    custom_donation.widget = forms.NumberInput(
        attrs={
            "class": "form-control w-100",
            "id": "customDonation",
            "placeholder": "Custom amount",
            "value": "125",
            "min": minimum_donation["dollars"],
            "step": "0.01",
            "required": "required",
        }
    )
    single_or_monthly = forms.ChoiceField(
        choices=[("single", "Give once"), ("monthly", "Repeat monthly")],
        widget=forms.RadioSelect(),
        initial="single",
    )
    perk_data = {perk.id: perk for perk in container.civi().get_active_perks_synced()}
    perk = PerkField(
        widget=forms.RadioSelect(attrs={"class": "btn-check", "disabled": "disabled"}),
    )
    options_by_perk = []
    for index, optionByPerk in enumerate(container.civi().get_active_perks_synced()):
        optionList = []
        for optionGroup in optionByPerk.options:
            optionList.append({optionGroup: optionByPerk.options[optionGroup]})
        options_by_perk.append(
            {
                "index": index,
                "id": optionByPerk.id,
                "name": optionByPerk.name,
                "options": optionList
            }
        )

    # TODO: once API is modeled to return option names and option dependencies,
    #       create select fields here rather than in template for proper
    #       backend validation

    first_name = forms.CharField(max_length=1000)
    first_name.widget = forms.TextInput(attrs={"class": "mt-2 form-control"})

    last_name = forms.CharField(max_length=1000)
    last_name.widget = forms.TextInput(attrs={"class": "mt-2 form-control"})

    street_address = forms.CharField(max_length=1000)
    street_address.widget = forms.TextInput(attrs={"class": "mt-2 form-control"})

    apartment_number = forms.CharField(max_length=1000, required=False)
    apartment_number.widget = forms.TextInput(attrs={"class": "mt-2"})

    country = forms.CharField(max_length=1000)
    country.widget = forms.Select(
        attrs={"class": "mt-2 form-control form-select", "required": "required"}
    )

    city = forms.CharField(max_length=1000)
    city.widget = forms.TextInput(attrs={"class": "mt-2 form-control"})

    state = forms.CharField(max_length=1000, required=False)
    state.widget = forms.Select(
        attrs={"class": "mt-2 form-control form-select"}
    )

    postal_code = forms.CharField(max_length=1000)
    postal_code.widget = forms.TextInput(attrs={"class": "mt-2 form-control"})

    email_address = forms.CharField(max_length=1000)
    email_address.widget = forms.EmailInput(attrs={"class": "mt-2 mb-2 form-control"})

    email_updates = forms.BooleanField(
        required=False, label="Send me updates from the Tor Project"
    )
    email_updates.widget = forms.CheckboxInput(attrs={"class": "form-check-input"})

    altcha = AltchaField(
        hidelogo=True,
        hidefooter=True,
        challengeurl=reverse_lazy("altcha_challenge")
    )

    def clean(self) -> dict[str, t.Any]:
        errors = []
        cleaned_data = t.cast(dict[str, t.Any], super().clean())

        with open('tordonate/static/data/states.json') as states_json:
            states_data = json.load(states_json)
        have_states = set([s['country_name'] for s in states_data])

        # Check for missing fields
        if cleaned_data.get("first_name") is None:
            errors.append(
                ValidationError(
                    _("Please enter a first name."),
                    code="first_name_missing"
                )
            )
        if cleaned_data.get("last_name") is None:
            errors.append(
                ValidationError(
                    _("Please enter a last name."),
                    code="last_name_missing"
                )
            )
        if cleaned_data.get("street_address") is None:
            errors.append(
                ValidationError(
                    _("Please enter a street address."),
                    code="address_missing"
                )
            )
        if cleaned_data.get("city") is None:
            errors.append(
                ValidationError(
                    _("Please enter a city."),
                    code="city_missing"
                )
            )
        if cleaned_data.get("country") is None:
            errors.append(
                ValidationError(
                    _("Please enter a country."),
                    code="country_missing"
                )
            )
        if cleaned_data.get("country") in ["Ukraine", "Russia"]:
            errors.append(
                ValidationError(
                    """Due to shipping restrictions, we currently can not ship to Ukraine, or Russia.
 We apologize for the inconvenience.""",  # noqa: E501
                    code="state_missing"
                )
            )
        if cleaned_data.get("state") is None and cleaned_data.get("country") in have_states:
            errors.append(
                ValidationError(
                    _("Please enter a state."),
                    code="state_missing"
                )
            )
        if cleaned_data.get("postal_code") is None:
            errors.append(
                ValidationError(
                    _("Please enter a postal code."),
                    code="postal_code_missing"
                )
            )

        # validate email address
        if cleaned_data.get("email_address") is None:
            errors.append(
                ValidationError(
                    _("Please enter an email address."), code="email_missing"))
        else:
            try:
                validate_email(
                    cleaned_data.get("email_address"),  # type: ignore
                    check_deliverability=False
                )
            except EmailNotValidError:
                errors.append(
                    ValidationError(
                        _("Please enter a valid email address."), code="email_invalid"))

        donation_amount = cleaned_data.get("custom_donation")
        donation_type = cleaned_data.get("single_or_monthly")

        # donation_amount is always the literal value of the custom_donation field;
        # we compare it to the "dollars" value of the minimum_donation object
        # in order to avoid having to convert one value or another for sake of comparison
        if isinstance(donation_amount, float):
            if donation_amount < self.minimum_donation["dollars"]:
                errors.append(
                    ValidationError(
                        _(
                            "Please donate at least {}.".format(
                                self.minimum_donation["display"]
                            )
                        ),
                        code="minimum_donation_not_met"
                    )
                )
        else:
            errors.append(
                ValidationError(
                    _("Please enter a valid donation amount."), code="amount_not_an_integer"))

        perk = self.cleaned_data.get("perk")

        # Retrieve and convert perk minimum-donation values for use in comparing against dollars
        if type(perk) is Perk:
            if donation_type == "single":
                perk_minimum = perk.single_price / 100
            elif donation_type == "monthly":
                perk_minimum = perk.monthly_price / 100

            # Validate single/monthly perk prices
            if donation_amount and perk_minimum and donation_amount < perk_minimum:
                errors.append(
                    ValidationError(
                        _(
                            "Donation amount of %(donation_amount)s is less than the minimum "
                            "donation of %(perk_minimum)s for %(perk_name)s"
                        ),
                        params={
                            "donation_amount": donation_amount,
                            "perk_minimum": perk_minimum,
                            "perk_name": perk.name,
                        },
                        code="minimum_perk_donation_not_met"
                    ),
                )

        cohort = cleaned_data.get("cohort", "undefined")
        if len(errors):
            for error in errors:
                prometheus.DonationProcessingError.labels(error.code, cohort).inc()
            raise ValidationError(errors)
        return cleaned_data


class DonorInfoFormA(DonorInfoForm):
    cohort = forms.CharField(widget=forms.HiddenInput(), initial="A")

    def clean(self) -> dict[str, t.Any]:
        cleaned_data = super().clean()
        cohort = cleaned_data.get("cohort", "undefined")

        if cohort != "A":
            raise ValidationError(
                _('Unexpected cohort sent back'),
                code="wrong_cohort"
            )

        return cleaned_data


class DonorInfoFormB(DonorInfoForm):
    cohort = forms.CharField(widget=forms.HiddenInput(), initial="B")

    def clean(self) -> dict[str, t.Any]:
        cleaned_data = super().clean()

        if cleaned_data.get("cohort") != "B":
            raise ValidationError(
                _('Unexpected cohort sent back'),
                code="wrong_cohort"
            )

        return cleaned_data


class CryptocurrencyForm(forms.Form):
    defaultDonation = {
        "single": settings.DEFAULT_DONATION["single"],
        "monthly": settings.DEFAULT_DONATION["monthly"],
    }

    minimum_donation = {
        "dollars": container.civi().minimum_donation_sync() // 100,  # type: ignore
        "display": "$"
        + str(int(container.civi().minimum_donation_sync() // 100)),  # type: ignore
    }

    custom_donation = forms.FloatField(
        required=True,
        validators=[
            MinValueValidator(container.civi().minimum_donation_sync() // 100),  # type: ignore
        ],
    )
    custom_donation.widget = forms.NumberInput(
        attrs={
            "class": "form-control w-100",
            "id": "customDonation",
            "placeholder": "Custom amount",
            "value": "125",
            "min": minimum_donation["dollars"],
            "step": "0.01",
            "required": "required",
        }
    )

    def clean(self) -> dict[str, t.Any]:
        errors = []
        cleaned_data = t.cast(dict[str, t.Any], super().clean())

        donation_amount = cleaned_data.get("custom_donation")
        # donation_amount is always the literal value of the custom_donation field;
        # we compare it to the "dollars" value of the minimum_donation object
        # in order to avoid having to convert one value or another for sake of comparison
        if isinstance(donation_amount, float):
            if donation_amount < self.minimum_donation["dollars"]:
                errors.append(ValidationError(_("Please donate at least {}.".format(
                    self.minimum_donation["display"]
                ))))
        else:
            errors.append(ValidationError(_("Please enter a valid donation amount.")))

        if len(errors):
            raise ValidationError(errors)
        return cleaned_data
