import asyncio
import ipaddress
import socket
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from urllib.parse import urlsplit


class UnsafePushEndpoint(ValueError):
    """Raised when a Web Push endpoint could reach a non-public service."""


Resolver = Callable[[str, int], Awaitable[list[str]]]


@dataclass(frozen=True)
class ResolvedPushEndpoint:
    endpoint: str
    hostname: str
    port: int
    addresses: tuple[str, ...]


async def _resolve(host: str, port: int) -> list[str]:
    loop = asyncio.get_running_loop()
    records = await loop.getaddrinfo(
        host,
        port,
        family=socket.AF_UNSPEC,
        type=socket.SOCK_STREAM,
    )
    return sorted({record[4][0] for record in records})


async def resolve_public_push_endpoint(
    endpoint: str,
    *,
    resolver: Resolver = _resolve,
    timeout_seconds: float = 2.0,
) -> ResolvedPushEndpoint:
    """Resolve a canonical endpoint to public addresses for a pinned connection."""
    parsed = urlsplit(endpoint)
    try:
        port = parsed.port
    except ValueError as error:
        raise UnsafePushEndpoint(
            "Endpoint must be a canonical public Web Push service URL"
        ) from error
    if (
        parsed.scheme != "https"
        or not parsed.hostname
        or parsed.username is not None
        or parsed.password is not None
        or parsed.fragment
        or port not in {None, 443}
    ):
        raise UnsafePushEndpoint("Endpoint must be a canonical public Web Push service URL")
    try:
        addresses = await asyncio.wait_for(
            resolver(parsed.hostname, port or 443),
            timeout=max(0.01, timeout_seconds),
        )
    except (OSError, asyncio.TimeoutError) as error:
        raise UnsafePushEndpoint("Endpoint must resolve to a public Web Push service") from error
    try:
        public = addresses and all(ipaddress.ip_address(address).is_global for address in addresses)
    except ValueError as error:
        raise UnsafePushEndpoint("Endpoint must resolve to a public Web Push service") from error
    if not public:
        raise UnsafePushEndpoint("Endpoint must resolve to a public Web Push service")
    return ResolvedPushEndpoint(
        endpoint=endpoint,
        hostname=parsed.hostname,
        port=port or 443,
        addresses=tuple(sorted(set(addresses))),
    )


async def validate_public_push_endpoint(
    endpoint: str,
    *,
    resolver: Resolver = _resolve,
    timeout_seconds: float = 2.0,
) -> str:
    """Return a canonical public HTTPS push endpoint or fail closed."""
    resolved = await resolve_public_push_endpoint(
        endpoint,
        resolver=resolver,
        timeout_seconds=timeout_seconds,
    )
    return resolved.endpoint
