import re
from typing import cast

from email_validator import EmailNotValidError, validate_email
from flask import current_app
from wtforms import StringField
from wtforms.fields.choices import SelectMultipleField
from wtforms.fields.core import Field
from wtforms.form import BaseForm
from wtforms.validators import Email, HostnameValidation, Regexp, ValidationError

from app.common.data import interfaces
from app.common.forms.fields import DecimalWithCommasField


class WordRange:
    """
    Validates that the number of words in a field's data is within a specified range.
    """

    def __init__(
        self,
        min_words: int | None = None,
        max_words: int | None = None,
        field_display_name: str | None = None,
    ) -> None:
        if min_words is None and max_words is None:
            raise ValueError("min_words and max_words cannot both be None")
        if min_words is not None and max_words is not None and max_words < min_words:
            raise ValueError("max_words cannot be less than min_words")
        self.min_words = min_words
        self.max_words = max_words
        self.field_display_name = field_display_name

    def __call__(self, form: BaseForm, field: Field) -> None:
        if not field.data:
            return  # Don't validate empty fields - use DataRequired for that

        words = field.data.split()
        word_count = len(words)
        field_display_name = self.field_display_name or field.name

        # Ensure first character is uppercase since we start all validation messages with it.
        field_display_name = field_display_name[0].upper() + field_display_name[1:]

        if self.min_words is not None and self.max_words is not None:
            if self.min_words == self.max_words and word_count != self.min_words:
                raise ValidationError(f"{field_display_name} must contain exactly {self.min_words} words")

            if word_count < self.min_words or word_count > self.max_words:
                raise ValidationError(
                    f"{field_display_name} must be between {self.min_words} words and {self.max_words} words"
                )

        if self.min_words is not None:
            if word_count < self.min_words:
                raise ValidationError(f"{field_display_name} must be {self.min_words} words or more")

        if self.max_words is not None:
            if word_count > self.max_words:
                raise ValidationError(f"{field_display_name} must be {self.max_words} words or fewer")


class CommunitiesEmail(Email):
    def __call__(self, form: BaseForm, field: Field) -> None:
        allowed_domains = current_app.config["INTERNAL_DOMAINS"]
        try:
            result = validate_email(
                field.data,
                check_deliverability=self.check_deliverability,
                allow_smtputf8=self.allow_smtputf8,
                allow_empty_local=self.allow_empty_local,
            )
            domain = f"@{result.domain}"
        except EmailNotValidError as e:
            raise ValidationError("Enter an email address in the correct format, like name@example.com") from e

        if allowed_domains and domain.lower() not in [d.lower() for d in allowed_domains]:
            raise ValidationError(f"Email address must end with {' or '.join(allowed_domains)}")


class AccessGrantFundingEmail(Email):
    def __call__(self, form: BaseForm, field: Field) -> None:
        email = field.data
        internal_domains = current_app.config["INTERNAL_DOMAINS"]

        if not email:
            return

        if email.endswith(internal_domains):
            return

        user = interfaces.user.get_user_by_email(email)

        if user is None or not user.get_grant_recipients():
            raise ValidationError(
                "The email address you entered does not have access to this service. "
                "Check the email address is correct or request access."
            )


class URLWithoutProtocol(Regexp):
    """
    Based off the `URL` validator from WTForms, except we specifically allow no protocol (eg https://) to be provided.
    """

    def __init__(self, require_tld: bool = True, allow_ip: bool = True, message: str | None = None) -> None:
        regex = (
            r"^(?P<protocol>https?://)?"
            r"(?P<host>[^\/\?:]+)"
            r"(?P<port>:[0-9]+)?"
            r"(?P<path>\/.*?)?"
            r"(?P<query>\?.*)?$"
        )
        super().__init__(regex, re.IGNORECASE, message)
        self.validate_hostname = HostnameValidation(require_tld=require_tld, allow_ip=allow_ip)

    def __call__(self, form: BaseForm, field: StringField, message: str | None = None) -> re.Match[str]:
        message = self.message
        if message is None:
            message = field.gettext("Invalid URL.")

        match = super().__call__(form, field, message)
        if not self.validate_hostname(match.group("host")):
            raise ValidationError(message)

        return match


class FinalOptionExclusive:
    """
    Validates that the user cannot select one or more checkbox options plus the final "Other" option, which should be
    exclusive.

    The GOV.UK Checkbox component does cater for this with an 'exclusive' property for the final option which will
    uncheck all options. This relies on Javascript however so we need some validation as a fallback should the user have
    Javascript disabled.

    This validator should only be used if the checkbox question type has been used with a separate final option.
    """

    def __init__(self, question_name: str, message: str | None = None) -> None:
        self.question_name = question_name
        self.message = message

    def __call__(self, form: BaseForm, field: SelectMultipleField) -> None:
        if not field.data:
            return  # Don't validate empty fields - use DataRequired for that

        checkbox_choices = field.data
        form_choices = cast(list[tuple[str, str]], field.choices)
        final_option_key, final_option_label, *_ = form_choices[-1]
        if final_option_key in checkbox_choices and len(checkbox_choices) > 1:
            message = self.message or f"Select {self.question_name}, or select {final_option_label}"
            raise ValidationError(message)


class MaxNumberOfDecimalPlacesValidator:
    """
    Validates that the user has not entered more than the maximum number of decimal places allowed. We don't do rounding
    as this could result in using a value other than what the user typed in so we error if they have entered too
    many decimal places
    """

    def __init__(self, max_decimal_places: int, message: str | None = None) -> None:
        self.max_decimal_places = max_decimal_places
        self.message = message

    def __call__(self, form: BaseForm, field: DecimalWithCommasField) -> None:
        if not field.data:
            return  # Don't validate empty fields - use DataRequired for that

        value_entered = str(field.data)

        if "." not in value_entered:
            # If they've just entered a whole number there's nothing to validate here
            return

        decimal_places = value_entered.split(".")[1]
        if len(decimal_places) > self.max_decimal_places:
            message = self.message or f"The answer cannot be more than {self.max_decimal_places} decimal places"
            raise ValidationError(message)
