from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List, Optional

from moto.core.base_backend import BackendDict, BaseBackend
from moto.route53 import route53_backends
from moto.route53.models import Route53Backend
from moto.utilities.paginator import paginate

from .exceptions import (
    DomainLimitExceededException,
    DuplicateRequestException,
    InvalidInputException,
)
from .validators import (
    DOMAIN_OPERATION_STATUSES,
    DOMAIN_OPERATION_TYPES,
    DomainFilterField,
    DomainsFilter,
    DomainSortOrder,
    DomainsSortCondition,
    NameServer,
    Route53Domain,
    Route53DomainsContactDetail,
    Route53DomainsOperation,
    ValidationException,
)


class Route53DomainsBackend(BaseBackend):
    """Implementation of Route53Domains APIs."""

    DEFAULT_MAX_DOMAINS_COUNT = 20
    PAGINATION_MODEL = {
        "list_domains": {
            "input_token": "marker",
            "limit_key": "max_items",
            "limit_default": 20,
            "unique_attribute": "domain_name",
        },
        "list_operations": {
            "input_token": "marker",
            "limit_key": "max_items",
            "limit_default": 20,
            "unique_attribute": "id",
        },
    }

    def __init__(self, region_name: str, account_id: str):
        super().__init__(region_name, account_id)
        self.__route53_backend: Route53Backend = route53_backends[account_id]["global"]
        self.__domains: Dict[str, Route53Domain] = {}
        self.__operations: Dict[str, Route53DomainsOperation] = {}

    def register_domain(
        self,
        domain_name: str,
        duration_in_years: int,
        auto_renew: bool,
        admin_contact: Dict[str, Any],
        registrant_contact: Dict[str, Any],
        tech_contact: Dict[str, Any],
        private_protect_admin_contact: bool,
        private_protect_registrant_contact: bool,
        private_protect_tech_contact: bool,
        extra_params: List[Dict[str, Any]],
    ) -> Route53DomainsOperation:
        """Register a domain"""

        if len(self.__domains) == self.DEFAULT_MAX_DOMAINS_COUNT:
            raise DomainLimitExceededException()

        requested_operation = Route53DomainsOperation.validate(
            domain_name=domain_name, status="SUCCESSFUL", type_="REGISTER_DOMAIN"
        )

        self.__validate_duplicate_operations(requested_operation)

        expiration_date = datetime.now(timezone.utc) + timedelta(
            days=365 * duration_in_years
        )

        try:
            domain = Route53Domain.validate(
                domain_name=domain_name,
                auto_renew=auto_renew,
                admin_contact=Route53DomainsContactDetail.validate_dict(admin_contact),
                registrant_contact=Route53DomainsContactDetail.validate_dict(
                    registrant_contact
                ),
                tech_contact=Route53DomainsContactDetail.validate_dict(tech_contact),
                admin_privacy=private_protect_admin_contact,
                registrant_privacy=private_protect_registrant_contact,
                tech_privacy=private_protect_tech_contact,
                expiration_date=expiration_date,
                extra_params=extra_params,
            )

        except ValidationException as e:
            raise InvalidInputException(e.errors)
        self.__operations[requested_operation.id] = requested_operation

        self.__route53_backend.create_hosted_zone(
            name=domain.domain_name, private_zone=False
        )

        self.__domains[domain_name] = domain
        return requested_operation

    def delete_domain(self, domain_name: str) -> Route53DomainsOperation:
        requested_operation = Route53DomainsOperation.validate(
            domain_name=domain_name, status="SUCCESSFUL", type_="DELETE_DOMAIN"
        )
        self.__validate_duplicate_operations(requested_operation)

        input_errors: List[str] = []
        Route53Domain.validate_domain_name(domain_name, input_errors)

        if input_errors:
            raise InvalidInputException(input_errors)

        if domain_name not in self.__domains:
            raise InvalidInputException(
                [f"Domain {domain_name} isn't registered in the current account"]
            )

        self.__operations[requested_operation.id] = requested_operation
        del self.__domains[domain_name]
        return requested_operation

    def __validate_duplicate_operations(
        self, requested_operation: Route53DomainsOperation
    ) -> None:
        for operation in self.__operations.values():
            if (
                operation.domain_name == requested_operation.domain_name
                and operation.type == requested_operation.type
            ):
                raise DuplicateRequestException()

    def get_domain(self, domain_name: str) -> Route53Domain:
        input_errors: List[str] = []
        Route53Domain.validate_domain_name(domain_name, input_errors)
        if input_errors:
            raise InvalidInputException(input_errors)

        if domain_name not in self.__domains:
            raise InvalidInputException(["Domain is not associated with this account"])

        return self.__domains[domain_name]

    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
    def list_domains(
        self,
        filter_conditions: Optional[List[Dict[str, Any]]] = None,
        sort_condition: Optional[Dict[str, Any]] = None,
    ) -> List[Route53Domain]:
        try:
            filters: List[DomainsFilter] = (
                [DomainsFilter.validate_dict(f) for f in filter_conditions]
                if filter_conditions
                else []
            )
            sort: Optional[DomainsSortCondition] = (
                DomainsSortCondition.validate_dict(sort_condition)
                if sort_condition
                else None
            )
        except ValidationException as e:
            raise InvalidInputException(e.errors)

        filter_fields = [f.name for f in filters]
        if sort and filter_fields and sort.name not in filter_fields:
            raise InvalidInputException(
                ["Sort condition must be the same as the filter condition"]
            )

        domains_to_return: List[Route53Domain] = []

        for domain in self.__domains.values():
            if all([f.filter(domain) for f in filters]):
                domains_to_return.append(domain)

        if sort:
            if sort.name == DomainFilterField.DOMAIN_NAME:
                domains_to_return.sort(
                    key=lambda d: d.domain_name,
                    reverse=(sort.sort_order == DomainSortOrder.DESCENDING),
                )
            else:
                domains_to_return.sort(
                    key=lambda d: d.expiration_date,
                    reverse=(sort.sort_order == DomainSortOrder.DESCENDING),
                )

        return domains_to_return

    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
    def list_operations(
        self,
        submitted_since_timestamp: Optional[int] = None,
        statuses: Optional[List[str]] = None,
        types: Optional[List[str]] = None,
        sort_by: Optional[str] = None,
        sort_order: Optional[str] = None,
    ) -> List[Route53DomainsOperation]:
        input_errors: List[str] = []
        statuses = statuses or []
        types = types or []

        if any(status not in DOMAIN_OPERATION_STATUSES for status in statuses):
            input_errors.append("Status is invalid")
        if any(type_ not in DOMAIN_OPERATION_TYPES for type_ in types):
            input_errors.append("Type is invalid")

        if input_errors:
            raise InvalidInputException(input_errors)

        submitted_since = (
            datetime.fromtimestamp(submitted_since_timestamp, timezone.utc)
            if submitted_since_timestamp
            else None
        )

        operations_to_return: List[Route53DomainsOperation] = []

        for operation in self.__operations.values():
            if statuses and operation.status not in statuses:
                continue

            if types and operation.type not in types:
                continue

            if submitted_since and operation.submitted_date < submitted_since:
                continue

            operations_to_return.append(operation)

        if sort_by == "SubmittedDate":
            operations_to_return.sort(
                key=lambda op: op.submitted_date,
                reverse=sort_order == DomainSortOrder.ASCENDING,
            )

        return operations_to_return

    def get_operation(self, operation_id: str) -> Route53DomainsOperation:
        if operation_id not in self.__operations:
            raise InvalidInputException(
                [f"Operation with id {operation_id} doesn't exist"]
            )

        return self.__operations[operation_id]

    def update_domain_nameservers(
        self, domain_name: str, nameservers: List[Dict[str, Any]]
    ) -> Route53DomainsOperation:
        input_errors: List[str] = []
        Route53Domain.validate_domain_name(domain_name, input_errors)
        if len(nameservers) < 1:
            input_errors.append("Must supply nameservers")

        servers: List[NameServer] = []
        try:
            servers = [NameServer.validate_dict(obj) for obj in nameservers]
        except ValidationException as e:
            input_errors += e.errors

        for server in servers:
            if domain_name in server.name and not server.glue_ips:
                input_errors.append(
                    f"Must supply glue IPs for name server {server.name} because it is a subdomain of "
                    f"the domain"
                )

        if input_errors:
            raise InvalidInputException(input_errors)

        if domain_name not in self.__domains:
            raise InvalidInputException(
                [f"Domain {domain_name} is not registered to the current AWS account"]
            )

        requested_operation = Route53DomainsOperation.validate(
            domain_name=domain_name, status="SUCCESSFUL", type_="UPDATE_NAMESERVER"
        )
        self.__validate_duplicate_operations(requested_operation)

        domain = self.__domains[domain_name]
        domain.nameservers = servers
        self.__operations[requested_operation.id] = requested_operation

        return requested_operation


route53domains_backends = BackendDict(
    Route53DomainsBackend,
    "route53domains",
    use_boto3_regions=False,
    additional_regions=["global"],
)
