# retoor <retoor@molodetz.nl>

from __future__ import annotations

import asyncio
import ipaddress
import socket
from typing import Any
from urllib.parse import urlparse

import httpx

from devplacepy import stealth

NAT64_PREFIXES = (
    ipaddress.ip_network("64:ff9b::/96"),
    ipaddress.ip_network("64:ff9b:1::/48"),
)


class BlockedAddressError(ValueError):
    pass


def effective_address(address: Any) -> Any:
    if isinstance(address, ipaddress.IPv6Address):
        if address.ipv4_mapped is not None:
            return address.ipv4_mapped
        for prefix in NAT64_PREFIXES:
            if address in prefix:
                return ipaddress.IPv4Address(int(address) & 0xFFFFFFFF)
    return address


def is_blocked_address(address: Any) -> bool:
    resolved = effective_address(address)
    return (
        resolved.is_private
        or resolved.is_loopback
        or resolved.is_link_local
        or resolved.is_reserved
        or resolved.is_multicast
        or resolved.is_unspecified
    )


async def guard_public_url(url: str, *, allow_private: bool = False) -> str:
    parsed = urlparse(url)
    if parsed.scheme not in ("http", "https"):
        raise BlockedAddressError("Only http and https URLs are allowed.")
    host = parsed.hostname
    if not host:
        raise BlockedAddressError("URL has no host.")
    if allow_private:
        return host
    try:
        infos = await asyncio.to_thread(socket.getaddrinfo, host, None)
    except socket.gaierror as exc:
        raise BlockedAddressError(f"Could not resolve host: {host}") from exc
    for info in infos:
        address = ipaddress.ip_address(info[4][0])
        if is_blocked_address(address):
            raise BlockedAddressError(
                f"Refusing to reach a private or local address ({effective_address(address)})."
            )
    return host


def guard_public_host_sync(host: str) -> str:
    if not host:
        raise BlockedAddressError("No host provided.")
    try:
        infos = socket.getaddrinfo(host, None)
    except socket.gaierror as exc:
        raise BlockedAddressError(f"Could not resolve host: {host}") from exc
    for info in infos:
        address = ipaddress.ip_address(info[4][0])
        if is_blocked_address(address):
            raise BlockedAddressError(
                f"Refusing to reach a private or local address ({effective_address(address)})."
            )
    return host


async def guard_public_host(host: str) -> str:
    return await asyncio.to_thread(guard_public_host_sync, host)


class _GuardedTransport(httpx.AsyncBaseTransport):
    def __init__(self, inner: httpx.AsyncBaseTransport) -> None:
        self._inner = inner

    async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
        await guard_public_url(str(request.url))
        return await self._inner.handle_async_request(request)

    async def aclose(self) -> None:
        await self._inner.aclose()


def guarded_async_client(**kwargs: Any) -> httpx.AsyncClient:
    transport = _GuardedTransport(stealth.stealth_transport())
    return stealth.stealth_async_client(transport=transport, **kwargs)
