import re
from datetime import datetime, timedelta, timezone
from enum import Enum
from ipaddress import IPv4Address, IPv6Address, ip_address
from typing import Any, Optional

from moto.core.common_models import BaseModel
from moto.moto_api._internal import mock_random
from moto.route53domains.exceptions import UnsupportedTLDException

DOMAIN_OPERATION_STATUSES = (
    "SUBMITTED",
    "IN_PROGRESS",
    "ERROR",
    "SUCCESSFUL",
    "FAILED",
)

DOMAIN_OPERATION_TYPES = (
    "REGISTER_DOMAIN",
    "DELETE_DOMAIN",
    "TRANSFER_IN_DOMAIN",
    "UPDATE_DOMAIN_CONTACT",
    "UPDATE_NAMESERVER",
    "CHANGE_PRIVACY_PROTECTION",
    "DOMAIN_LOCK",
    "ENABLE_AUTORENEW",
    "DISABLE_AUTORENEW",
    "ADD_DNSSEC",
    "REMOVE_DNSSEC",
    "EXPIRE_DOMAIN",
    "TRANSFER_OUT_DOMAIN",
    "CHANGE_DOMAIN_OWNER",
    "RENEW_DOMAIN",
    "PUSH_DOMAIN",
    "INTERNAL_TRANSFER_OUT_DOMAIN",
    "INTERNAL_TRANSFER_IN_DOMAIN",
)

DOMAIN_OPERATION_STATUS_FLAGS = (
    "PENDING_ACCEPTANCE",
    "PENDING_CUSTOMER_ACTION",
    "PENDING_AUTHORIZATION",
    "PENDING_PAYMENT_VERIFICATION",
    "PENDING_SUPPORT_CASE",
)

DOMAIN_CONTACT_DETAIL_CONTACT_TYPES = (
    "PERSON",
    "COMPANY",
    "ASSOCIATION",
    "PUBLIC_BODY",
    "RESELLER",
    "ORGANIZATION",
)

DOMAIN_CONTACT_DETAIL_COUNTRY_CODES = (
    "AC",
    "AD",
    "AE",
    "AF",
    "AG",
    "AI",
    "AL",
    "AM",
    "AN",
    "AO",
    "AQ",
    "AR",
    "AS",
    "AT",
    "AU",
    "AW",
    "AX",
    "AZ",
    "BA",
    "BB",
    "BD",
    "BE",
    "BF",
    "BG",
    "BH",
    "BI",
    "BJ",
    "BL",
    "BM",
    "BN",
    "BO",
    "BQ",
    "BR",
    "BS",
    "BT",
    "BV",
    "BW",
    "BY",
    "BZ",
    "CA",
    "CC",
    "CD",
    "CF",
    "CG",
    "CH",
    "CI",
    "CK",
    "CL",
    "CM",
    "CN",
    "CO",
    "CR",
    "CU",
    "CV",
    "CW",
    "CX",
    "CY",
    "CZ",
    "DE",
    "DJ",
    "DK",
    "DM",
    "DO",
    "DZ",
    "EC",
    "EE",
    "EG",
    "EH",
    "ER",
    "ES",
    "ET",
    "FI",
    "FJ",
    "FK",
    "FM",
    "FO",
    "FR",
    "GA",
    "GB",
    "GD",
    "GE",
    "GF",
    "GG",
    "GH",
    "GI",
    "GL",
    "GM",
    "GN",
    "GP",
    "GQ",
    "GR",
    "GS",
    "GT",
    "GU",
    "GW",
    "GY",
    "HK",
    "HM",
    "HN",
    "HR",
    "HT",
    "HU",
    "ID",
    "IE",
    "IL",
    "IM",
    "IN",
    "IO",
    "IQ",
    "IR",
    "IS",
    "IT",
    "JE",
    "JM",
    "JO",
    "JP",
    "KE",
    "KG",
    "KH",
    "KI",
    "KM",
    "KN",
    "KP",
    "KR",
    "KW",
    "KY",
    "KZ",
    "LA",
    "LB",
    "LC",
    "LI",
    "LK",
    "LR",
    "LS",
    "LT",
    "LU",
    "LV",
    "LY",
    "MA",
    "MC",
    "MD",
    "ME",
    "MF",
    "MG",
    "MH",
    "MK",
    "ML",
    "MM",
    "MN",
    "MO",
    "MP",
    "MQ",
    "MR",
    "MS",
    "MT",
    "MU",
    "MV",
    "MW",
    "MX",
    "MY",
    "MZ",
    "NA",
    "NC",
    "NE",
    "NF",
    "NG",
    "NI",
    "NL",
    "NO",
    "NP",
    "NR",
    "NU",
    "NZ",
    "OM",
    "PA",
    "PE",
    "PF",
    "PG",
    "PH",
    "PK",
    "PL",
    "PM",
    "PN",
    "PR",
    "PS",
    "PT",
    "PW",
    "PY",
    "QA",
    "RE",
    "RO",
    "RS",
    "RU",
    "RW",
    "SA",
    "SB",
    "SC",
    "SD",
    "SE",
    "SG",
    "SH",
    "SI",
    "SJ",
    "SK",
    "SL",
    "SM",
    "SN",
    "SO",
    "SR",
    "SS",
    "ST",
    "SV",
    "SX",
    "SY",
    "SZ",
    "TC",
    "TD",
    "TF",
    "TG",
    "TH",
    "TJ",
    "TK",
    "TL",
    "TM",
    "TN",
    "TO",
    "TP",
    "TR",
    "TT",
    "TV",
    "TW",
    "TZ",
    "UA",
    "UG",
    "US",
    "UY",
    "UZ",
    "VA",
    "VC",
    "VE",
    "VG",
    "VI",
    "VN",
    "VU",
    "WF",
    "WS",
    "YE",
    "YT",
    "ZA",
    "ZM",
    "ZW",
)

# List of supported top-level domains that you can register with Amazon Route53
AWS_SUPPORTED_TLDS = (
    "ac",
    "academy",
    "accountants",
    "actor",
    "adult",
    "agency",
    "airforce",
    "apartments",
    "associates",
    "auction",
    "audio",
    "band",
    "bargains",
    "bet",
    "bike",
    "bingo",
    "biz",
    "black",
    "blue",
    "boutique",
    "builders",
    "business",
    "buzz",
    "cab",
    "cafe",
    "camera",
    "camp",
    "capital",
    "cards",
    "care",
    "careers",
    "cash",
    "casino",
    "catering",
    "cc",
    "center",
    "ceo",
    "chat",
    "cheap",
    "church",
    "city",
    "claims",
    "cleaning",
    "click",
    "clinic",
    "clothing",
    "cloud",
    "club",
    "coach",
    "codes",
    "coffee",
    "college",
    "com",
    "community",
    "company",
    "computer",
    "condos",
    "construction",
    "consulting",
    "contractors",
    "cool",
    "coupons",
    "credit",
    "creditcard",
    "cruises",
    "dance",
    "dating",
    "deals",
    "degree",
    "delivery",
    "democrat",
    "dental",
    "diamonds",
    "diet",
    "digital",
    "direct",
    "directory",
    "discount",
    "dog",
    "domains",
    "education",
    "email",
    "energy",
    "engineering",
    "enterprises",
    "equipment",
    "estate",
    "events",
    "exchange",
    "expert",
    "exposed",
    "express",
    "fail",
    "farm",
    "finance",
    "financial",
    "fish",
    "fitness",
    "flights",
    "florist",
    "flowers",
    "fm",
    "football",
    "forsale",
    "foundation",
    "fund",
    "furniture",
    "futbol",
    "fyi",
    "gallery",
    "games",
    "gift",
    "gifts",
    "gives",
    "glass",
    "global",
    "gmbh",
    "gold",
    "golf",
    "graphics",
    "gratis",
    "green",
    "gripe",
    "group",
    "guide",
    "guitars",
    "guru",
    "haus",
    "healthcare",
    "help",
    "hiv",
    "hockey",
    "holdings",
    "holiday",
    "host",
    "hosting",
    "house",
    "im",
    "immo",
    "immobilien",
    "industries",
    "info",
    "ink",
    "institute",
    "insure",
    "international",
    "investments",
    "io",
    "irish",
    "jewelry",
    "juegos",
    "kaufen",
    "kim",
    "kitchen",
    "kiwi",
    "land",
    "lease",
    "legal",
    "lgbt",
    "life",
    "lighting",
    "limited",
    "limo",
    "link",
    "live",
    "loan",
    "loans",
    "lol",
    "maison",
    "management",
    "marketing",
    "mba",
    "media",
    "memorial",
    "mobi",
    "moda",
    "money",
    "mortgage",
    "movie",
    "name",
    "net",
    "network",
    "news",
    "ninja",
    "onl",
    "online",
    "org",
    "partners",
    "parts",
    "photo",
    "photography",
    "photos",
    "pics",
    "pictures",
    "pink",
    "pizza",
    "place",
    "plumbing",
    "plus",
    "poker",
    "porn",
    "press",
    "pro",
    "productions",
    "properties",
    "property",
    "pub",
    "qpon",
    "recipes",
    "red",
    "reise",
    "reisen",
    "rentals",
    "repair",
    "report",
    "republican",
    "restaurant",
    "reviews",
    "rip",
    "rocks",
    "run",
    "sale",
    "sarl",
    "school",
    "schule",
    "services",
    "sex",
    "sexy",
    "shiksha",
    "shoes",
    "show",
    "singles",
    "site",
    "soccer",
    "social",
    "solar",
    "solutions",
    "space",
    "store",
    "studio",
    "style",
    "sucks",
    "supplies",
    "supply",
    "support",
    "surgery",
    "systems",
    "tattoo",
    "tax",
    "taxi",
    "team",
    "tech",
    "technology",
    "tennis",
    "theater",
    "tienda",
    "tips",
    "tires",
    "today",
    "tools",
    "tours",
    "town",
    "toys",
    "trade",
    "training",
    "tv",
    "university",
    "uno",
    "vacations",
    "vegas",
    "ventures",
    "vg",
    "viajes",
    "video",
    "villas",
    "vision",
    "voyage",
    "watch",
    "website",
    "wedding",
    "wiki",
    "wine",
    "works",
    "world",
    "wtf",
    "xyz",
    "zone",
)

VALID_DOMAIN_REGEX = re.compile(
    r"(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]"
)
PHONE_NUMBER_REGEX = re.compile(r"\+\d*\.\d+$")


class DomainFilterField(str, Enum):
    DOMAIN_NAME = "DomainName"
    EXPIRY = "Expiry"


class DomainSortOrder(str, Enum):
    ASCENDING = "ASC"
    DESCENDING = "DES"


class DomainFilterOperator(str, Enum):
    LE = "LE"
    GE = "GE"
    BEGINS_WITH = "BEGINS_WITH"


def is_valid_enum(value: Any, enum_cls: type[Enum]) -> bool:
    try:
        enum_cls(value)
        return True
    except ValueError:
        return False


class ValidationException(Exception):
    def __init__(self, errors: list[str]):
        super().__init__("\n\t".join(errors))
        self.errors = errors


class Route53DomainsOperation(BaseModel):
    def __init__(
        self,
        id_: str,
        domain_name: str,
        status: str,
        type_: str,
        submitted_date: datetime,
        last_updated_date: datetime,
        message: Optional[str] = None,
        status_flag: Optional[str] = None,
    ):
        self.id = id_
        self.domain_name = domain_name
        self.status = status
        self.type = type_
        self.submitted_date = submitted_date
        self.last_updated_date = last_updated_date
        self.message = message
        self.status_flag = status_flag

    @classmethod
    def validate(  # type: ignore[misc,no-untyped-def]
        cls,
        domain_name: str,
        status: str,
        type_: str,
        message: Optional[str] = None,
        status_flag: Optional[str] = None,
    ):
        id_ = str(mock_random.uuid4())
        submitted_date = datetime.now(timezone.utc)
        last_updated_date = datetime.now(timezone.utc)

        return cls(
            id_,
            domain_name,
            status,
            type_,
            submitted_date,
            last_updated_date,
            message,
            status_flag,
        )

    def to_json(self) -> dict[str, Any]:
        d = {
            "OperationId": self.id,
            "Status": self.status,
            "StatusFlag": self.status_flag,
            "DomainName": self.domain_name,
            "LastUpdatedDate": self.last_updated_date.timestamp(),
            "SubmittedDate": self.submitted_date.timestamp(),
            "Type": self.type,
        }

        if self.message:
            d["Message"] = self.message

        return d


class Route53DomainsContactDetail(BaseModel):
    def __init__(
        self,
        address_line_1: Optional[str] = None,
        address_line_2: Optional[str] = None,
        city: Optional[str] = None,
        contact_type: Optional[str] = None,
        country_code: Optional[str] = None,
        email: Optional[str] = None,
        extra_params: Optional[list[dict[str, Any]]] = None,
        fax: Optional[str] = None,
        first_name: Optional[str] = None,
        last_name: Optional[str] = None,
        organization_name: Optional[str] = None,
        phone_number: Optional[str] = None,
        state: Optional[str] = None,
        zip_code: Optional[str] = None,
    ):
        super().__init__()
        self.address_line_1 = address_line_1
        self.address_line_2 = address_line_2
        self.city = city
        self.contact_type = contact_type
        self.country_code = country_code
        self.email = email
        self.extra_params = extra_params
        self.fax = fax
        self.first_name = first_name
        self.last_name = last_name
        self.organization_name = organization_name
        self.phone_number = phone_number
        self.state = state
        self.zip_code = zip_code

    @classmethod
    def validate(  # type: ignore[misc, no-untyped-def]
        cls,
        address_line_1: Optional[str] = None,
        address_line_2: Optional[str] = None,
        city: Optional[str] = None,
        contact_type: Optional[str] = None,
        country_code: Optional[str] = None,
        email: Optional[str] = None,
        extra_params: Optional[list[dict[str, Any]]] = None,
        fax: Optional[str] = None,
        first_name: Optional[str] = None,
        last_name: Optional[str] = None,
        organization_name: Optional[str] = None,
        phone_number: Optional[str] = None,
        state: Optional[str] = None,
        zip_code: Optional[str] = None,
    ):
        input_errors: list[str] = []

        cls.__validate_str_len(address_line_1, "AddressLine1", 255, input_errors)
        cls.__validate_str_len(address_line_2, "AddressLine2", 255, input_errors)
        cls.__validate_str_len(city, "City", 255, input_errors)
        cls.__validate_str_len(email, "Email", 255, input_errors)
        cls.__validate_str_len(fax, "Fax", 255, input_errors)
        cls.__validate_str_len(first_name, "FirstName", 255, input_errors)
        cls.__validate_str_len(last_name, "LastName", 255, input_errors)
        cls.__validate_str_len(state, "State", 255, input_errors)
        cls.__validate_str_len(zip_code, "ZipCode", 255, input_errors)

        if contact_type:
            if contact_type not in DOMAIN_CONTACT_DETAIL_CONTACT_TYPES:
                input_errors.append(f"Invalid contact type {contact_type}")
            else:
                if contact_type != "PERSON" and not organization_name:
                    input_errors.append(
                        "Must supply OrganizationName when ContactType is not PERSON"
                    )

        if country_code and country_code not in DOMAIN_CONTACT_DETAIL_COUNTRY_CODES:
            input_errors.append(f"CountryCode {country_code} is invalid")

        if phone_number and not PHONE_NUMBER_REGEX.match(phone_number):
            input_errors.append("PhoneNumber is in an invalid format")

        if input_errors:
            raise ValidationException(input_errors)

        return cls(
            address_line_1,
            address_line_2,
            city,
            contact_type,
            country_code,
            email,
            extra_params,
            fax,
            first_name,
            last_name,
            organization_name,
            phone_number,
            state,
            zip_code,
        )

    @classmethod
    def validate_dict(cls, d: dict[str, Any]):  # type: ignore[misc, no-untyped-def]
        address_line_1 = d.get("AddressLine1")
        address_line_2 = d.get("AddressLine2")
        city = d.get("City")
        contact_type = d.get("ContactType")
        country_code = d.get("CountryCode")
        email = d.get("Email")
        extra_params = d.get("ExtraParams")
        fax = d.get("Fax")
        first_name = d.get("FirstName")
        last_name = d.get("LastName")
        organization_name = d.get("OrganizationName")
        phone_number = d.get("PhoneNumber")
        state = d.get("State")
        zip_code = d.get("ZipCode")
        return cls.validate(
            address_line_1=address_line_1,
            address_line_2=address_line_2,
            city=city,
            contact_type=contact_type,
            country_code=country_code,
            email=email,
            extra_params=extra_params,
            fax=fax,
            first_name=first_name,
            last_name=last_name,
            organization_name=organization_name,
            phone_number=phone_number,
            state=state,
            zip_code=zip_code,
        )

    @staticmethod
    def __validate_str_len(
        value: Optional[str], field_name: str, max_len: int, input_errors: list[str]
    ) -> None:
        if value and len(value) > max_len:
            input_errors.append(f"Length of {field_name} is more than {max_len}")

    def to_json(self) -> dict[str, Any]:
        d = {
            "FirstName": self.first_name,
            "LastName": self.last_name,
            "ContactType": self.contact_type,
            "OrganizationName": self.organization_name,
            "AddressLine1": self.address_line_1,
            "AddressLine2": self.address_line_2,
            "City": self.city,
            "State": self.state,
            "CountryCode": self.country_code,
            "ZipCode": self.zip_code,
            "PhoneNumber": self.phone_number,
            "Email": self.email,
            "Fax": self.fax,
            "ExtraParams": self.extra_params,
        }

        return {key: value for key, value in d.items() if value is not None}


class NameServer:
    def __init__(self, name: str, glue_ips: list[str]):
        self.name = name
        self.glue_ips = glue_ips

    @classmethod
    def validate(cls, name: str, glue_ips: Optional[list[str]] = None):  # type: ignore[misc,no-untyped-def]
        glue_ips = glue_ips or []
        input_errors: list[str] = []

        if not VALID_DOMAIN_REGEX.match(name):
            input_errors.append(f"{name} is not a valid host name")

        num_ipv4_addresses = 0
        num_ipv6_addresses = 0
        for ip in glue_ips:
            try:
                address = ip_address(ip)
                if isinstance(address, IPv4Address):
                    num_ipv4_addresses += 1
                elif isinstance(address, IPv6Address):
                    num_ipv6_addresses += 1
            except ValueError:
                input_errors.append(f"{ip} is not a valid IP address")

        if num_ipv4_addresses > 1:
            input_errors.append("GlueIps list must include only 1 IPv4 address")
        if num_ipv6_addresses > 1:
            input_errors.append("GlueIps list must include only 1 IPv6 address")

        if input_errors:
            raise ValidationException(input_errors)

        return cls(name, glue_ips)

    @classmethod
    def validate_dict(cls, data: dict[str, Any]):  # type: ignore[misc,no-untyped-def]
        name = data.get("Name")
        glue_ips = data.get("GlueIps")
        return cls.validate(name, glue_ips)  # type: ignore[arg-type]

    def to_json(self) -> dict[str, Any]:
        d: dict[str, Any] = {"Name": self.name}
        if self.glue_ips:
            d["GlueIps"] = self.glue_ips
        return d


class Route53Domain(BaseModel):
    def __init__(
        self,
        domain_name: str,
        nameservers: list[NameServer],
        auto_renew: bool,
        admin_contact: Route53DomainsContactDetail,
        registrant_contact: Route53DomainsContactDetail,
        tech_contact: Route53DomainsContactDetail,
        admin_privacy: bool,
        registrant_privacy: bool,
        tech_privacy: bool,
        registrar_name: str,
        whois_server: str,
        registrar_url: str,
        abuse_contact_email: str,
        abuse_contact_phone: str,
        registry_domain_id: str,
        creation_date: datetime,
        updated_date: datetime,
        expiration_date: datetime,
        reseller: str,
        status_list: list[str],
        dns_sec_keys: list[dict[str, Any]],
        extra_params: list[dict[str, Any]],
    ):
        self.domain_name = domain_name
        self.nameservers = nameservers
        self.auto_renew = auto_renew
        self.admin_contact = admin_contact
        self.registrant_contact = registrant_contact
        self.tech_contact = tech_contact
        self.admin_privacy = admin_privacy
        self.registrant_privacy = registrant_privacy
        self.tech_privacy = tech_privacy
        self.registrar_name = registrar_name
        self.whois_server = whois_server
        self.registrar_url = registrar_url
        self.abuse_contact_email = abuse_contact_email
        self.abuse_contact_phone = abuse_contact_phone
        self.registry_domain_id = registry_domain_id
        self.creation_date = creation_date
        self.updated_date = updated_date
        self.expiration_date = expiration_date
        self.reseller = reseller
        self.status_list = status_list
        self.dns_sec_keys = dns_sec_keys
        self.extra_params = extra_params

    @classmethod
    def validate(  # type: ignore[misc,no-untyped-def]
        cls,
        domain_name: str,
        admin_contact: Route53DomainsContactDetail,
        registrant_contact: Route53DomainsContactDetail,
        tech_contact: Route53DomainsContactDetail,
        nameservers: Optional[list[dict[str, Any]]] = None,
        auto_renew: bool = True,
        admin_privacy: bool = True,
        registrant_privacy: bool = True,
        tech_privacy: bool = True,
        registrar_name: Optional[str] = None,
        whois_server: Optional[str] = None,
        registrar_url: Optional[str] = None,
        abuse_contact_email: Optional[str] = None,
        abuse_contact_phone: Optional[str] = None,
        registry_domain_id: Optional[str] = None,
        expiration_date: Optional[datetime] = None,
        reseller: Optional[str] = None,
        dns_sec_keys: Optional[list[dict[str, Any]]] = None,
        extra_params: Optional[list[dict[str, Any]]] = None,
    ):
        input_errors: list[str] = []

        cls.validate_domain_name(domain_name, input_errors)

        nameservers = nameservers or []
        try:
            nameservers = [
                NameServer.validate_dict(nameserver) for nameserver in nameservers
            ] or [
                NameServer.validate(name="ns-2048.awscdn-64.net"),
                NameServer.validate(name="ns-2051.awscdn-67.net"),
                NameServer.validate(name="ns-2050.awscdn-66.net"),
                NameServer.validate(name="ns-2049.awscdn-65.net"),
            ]
        except ValidationException as e:
            input_errors += e.errors

        creation_date = datetime.now(timezone.utc)
        updated_date = datetime.now(timezone.utc)
        expiration_date = expiration_date or datetime.now(timezone.utc) + timedelta(
            days=365 * 10
        )
        registrar_name = registrar_name or "GANDI SAS"
        whois_server = whois_server or "whois.gandi.net"
        registrar_url = registrar_url or "http://www.gandi.net"
        abuse_contact_email = abuse_contact_email or "abuse@support.gandi.net"
        status_list = ["SUCCEEDED"]

        time_until_expiration = expiration_date - datetime.now(timezone.utc)
        if time_until_expiration < timedelta(
            days=365
        ) or time_until_expiration > timedelta(days=365 * 10):
            input_errors.append(
                "ExpirationDate must by between 1 and 10 years from now"
            )

        if input_errors:
            raise ValidationException(input_errors)

        return cls(
            domain_name=domain_name,
            nameservers=nameservers,  # type: ignore[arg-type]
            auto_renew=auto_renew,
            admin_contact=admin_contact,
            registrant_contact=registrant_contact,
            tech_contact=tech_contact,
            admin_privacy=admin_privacy,
            registrant_privacy=registrant_privacy,
            tech_privacy=tech_privacy,
            registrar_name=registrar_name,
            whois_server=whois_server,
            registrar_url=registrar_url,
            abuse_contact_email=abuse_contact_email,
            abuse_contact_phone=abuse_contact_phone,  # type: ignore[arg-type]
            registry_domain_id=registry_domain_id,  # type: ignore[arg-type]
            creation_date=creation_date,
            updated_date=updated_date,
            expiration_date=expiration_date,
            reseller=reseller,  # type: ignore[arg-type]
            status_list=status_list,
            dns_sec_keys=dns_sec_keys,  # type: ignore[arg-type]
            extra_params=extra_params,  # type: ignore[arg-type]
        )

    @staticmethod
    def validate_domain_name(domain_name: str, input_errors: list[str]) -> None:
        if not VALID_DOMAIN_REGEX.match(domain_name):
            input_errors.append("Invalid domain name")
            return

        tld = domain_name.split(".")[-1]
        if tld not in AWS_SUPPORTED_TLDS:
            raise UnsupportedTLDException(tld)

    def to_json(self) -> dict[str, Any]:
        return {
            "DomainName": self.domain_name,
            "Nameservers": [nameserver.to_json() for nameserver in self.nameservers],
            "AutoRenew": self.auto_renew,
            "AdminContact": self.admin_contact.to_json(),
            "RegistrantContact": self.registrant_contact.to_json(),
            "TechContact": self.tech_contact.to_json(),
            "AdminPrivacy": self.admin_privacy,
            "RegistrantPrivacy": self.registrant_privacy,
            "TechPrivacy": self.tech_privacy,
            "RegistrarName": self.registrar_name,
            "WhoIsServer": self.whois_server,
            "RegistrarUrl": self.registrar_url,
            "AbuseContactEmail": self.abuse_contact_email,
            "AbuseContactPhone": self.abuse_contact_phone,
            "RegistryDomainId": "",
            "CreationDate": self.creation_date.timestamp(),
            "UpdateDate": self.updated_date.timestamp(),
            "ExpirationDate": self.expiration_date.timestamp(),
            "Reseller": self.reseller,
            "DnsSec": "",
            "StatusList": self.status_list,
            "DnsSecKeys": self.dns_sec_keys,
            "BillingContact": self.admin_contact.to_json(),
        }


class DomainsFilter:
    def __init__(
        self, name: DomainFilterField, operator: DomainFilterOperator, values: list[str]
    ):
        self.name: DomainFilterField = name
        self.operator: DomainFilterOperator = operator
        self.values = values

    def filter(self, domain: Route53Domain) -> bool:
        if self.name == DomainFilterField.DOMAIN_NAME:
            return self.__filter_by_domain_name(domain)
        return self.__filter_by_expiry_date(domain)

    def __filter_by_domain_name(self, domain: Route53Domain) -> bool:
        return any(domain.domain_name.startswith(value) for value in self.values)

    def __filter_by_expiry_date(self, domain: Route53Domain) -> bool:
        return (
            any(
                value
                for value in self.values
                if domain.expiration_date
                >= datetime.fromtimestamp(float(value), tz=timezone.utc)
            )
            if self.operator == DomainFilterOperator.GE
            else any(
                value
                for value in self.values
                if domain.expiration_date
                <= datetime.fromtimestamp(float(value), tz=timezone.utc)
            )
        )

    @classmethod
    def validate(cls, name: str, operator: str, values: list[str]):  # type: ignore[misc, no-untyped-def]
        input_errors: list[str] = []

        if not is_valid_enum(name, DomainFilterField):
            input_errors.append(f"Cannot filter by field {name}")

        if not is_valid_enum(operator, DomainFilterOperator):
            input_errors.append(f"Invalid filter operator {operator}")

        if len(values) != 1:
            input_errors.append("Multiple filter values are not currently supported")

        if (
            name == DomainFilterField.DOMAIN_NAME
            and operator != DomainFilterOperator.BEGINS_WITH
        ):
            input_errors.append(
                f"Operator {operator} cannot be used with the DomainName filter"
            )

        if name == DomainFilterField.EXPIRY and operator not in (
            DomainFilterOperator.GE,
            DomainFilterOperator.LE,
        ):
            input_errors.append(
                f"Operator {operator} cannot be used with the Expiry filter"
            )

        if input_errors:
            raise ValidationException(input_errors)

        return cls(
            name=DomainFilterField(name),
            operator=DomainFilterOperator(operator),
            values=values,
        )

    @classmethod
    def validate_dict(cls, data: dict[str, Any]):  # type: ignore[misc,no-untyped-def]
        name = data.get("Name")
        operator = data.get("Operator")
        values = data.get("Values")
        return cls.validate(name=name, operator=operator, values=values)  # type: ignore[arg-type]


class DomainsSortCondition:
    def __init__(self, name: DomainFilterField, sort_order: DomainSortOrder):
        self.name: DomainFilterField = name
        self.sort_order: DomainSortOrder = sort_order

    @classmethod
    def validate(cls, name: str, sort_order: str):  # type: ignore[misc,no-untyped-def]
        input_errors: list[str] = []
        if not is_valid_enum(name, DomainFilterField):
            input_errors.append(f"Cannot sort by field {name}")

        if not is_valid_enum(sort_order, DomainSortOrder):
            input_errors.append(f"Invalid sort order {sort_order}")

        if input_errors:
            raise ValidationException(input_errors)

        return cls(name=DomainFilterField(name), sort_order=DomainSortOrder(sort_order))

    @classmethod
    def validate_dict(cls, data: dict[str, Any]):  # type: ignore[misc,no-untyped-def]
        name = data.get("Name")
        sort_order = data.get("SortOrder")
        return cls.validate(name=name, sort_order=sort_order)  # type: ignore[arg-type]
