"""
NIP-66 network metadata container with ASN lookup capabilities.

Resolves a relay's hostname to IPv4/IPv6 addresses and queries the
GeoIP ASN database for autonomous system number, organization name,
and CIDR network ranges as part of
[NIP-66](https://github.com/nostr-protocol/nips/blob/master/66.md)
monitoring. Clearnet relays only.

Note:
    Hostname resolution uses [resolve_host][bigbrotr.utils.dns.resolve_host]
    to obtain both IPv4 and IPv6 addresses. The GeoIP ASN database
    (GeoLite2-ASN) must be provided as an open ``geoip2.database.Reader``
    -- the caller is responsible for database lifecycle management.

    IPv4 ASN data takes priority; IPv6 ASN data is used only as a fallback
    when no IPv4 address is available. IPv6-specific network ranges are
    always recorded separately.

See Also:
    [bigbrotr.nips.nip66.data.Nip66NetData][bigbrotr.nips.nip66.data.Nip66NetData]:
        Data model for network/ASN fields.
    [bigbrotr.nips.nip66.logs.Nip66NetLogs][bigbrotr.nips.nip66.logs.Nip66NetLogs]:
        Log model for network lookup results.
    [bigbrotr.nips.nip66.geo.Nip66GeoMetadata][bigbrotr.nips.nip66.geo.Nip66GeoMetadata]:
        Geolocation test that also uses
        [resolve_host][bigbrotr.utils.dns.resolve_host] for IP resolution.
    [bigbrotr.utils.dns.resolve_host][bigbrotr.utils.dns.resolve_host]:
        DNS resolution utility used to obtain IP addresses.
"""

from __future__ import annotations

import asyncio
import logging
from typing import Any, Self

import geoip2.database
import geoip2.errors

from bigbrotr.models.constants import NetworkType
from bigbrotr.models.relay import Relay  # noqa: TC001
from bigbrotr.nips.base import BaseNipMetadata
from bigbrotr.utils.dns import resolve_host

from .data import Nip66NetData
from .logs import Nip66NetLogs


logger = logging.getLogger("bigbrotr.nips.nip66")


class Nip66NetMetadata(BaseNipMetadata):
    """Container for network/ASN data and lookup logs.

    Provides the ``execute()`` class method that resolves the relay hostname
    and performs GeoIP ASN lookups for both IPv4 and IPv6 addresses.

    See Also:
        [bigbrotr.nips.nip66.nip66.Nip66][bigbrotr.nips.nip66.nip66.Nip66]:
            Top-level model that orchestrates this alongside other tests.
        [bigbrotr.models.metadata.MetadataType][bigbrotr.models.metadata.MetadataType]:
            The ``NIP66_NET`` variant used when storing these results.
        [bigbrotr.nips.nip66.geo.Nip66GeoMetadata][bigbrotr.nips.nip66.geo.Nip66GeoMetadata]:
            Geolocation test that shares the IP resolution step.
    """

    data: Nip66NetData
    logs: Nip66NetLogs

    @staticmethod
    def _net(
        ipv4: str | None,
        ipv6: str | None,
        asn_reader: geoip2.database.Reader,
    ) -> dict[str, Any]:
        """Perform synchronous ASN lookups for IPv4 and/or IPv6 addresses.

        IPv4 ASN data takes priority; IPv6 ASN data is used as a fallback
        when IPv4 is not available. IPv6-specific network range is always
        recorded separately.

        Args:
            ipv4: Resolved IPv4 address, or None.
            ipv6: Resolved IPv6 address, or None.
            asn_reader: Open GeoLite2-ASN database reader.

        Returns:
            Dictionary of network fields (IP, ASN, org, CIDR ranges).
        """
        result: dict[str, Any] = {}

        if ipv4:
            result["net_ip"] = ipv4
            try:
                asn_response = asn_reader.asn(ipv4)
                if asn_response.autonomous_system_number:
                    result["net_asn"] = asn_response.autonomous_system_number
                if asn_response.autonomous_system_organization:
                    result["net_asn_org"] = asn_response.autonomous_system_organization
                if asn_response.network:
                    result["net_network"] = str(asn_response.network)
            except (geoip2.errors.GeoIP2Error, ValueError) as e:
                logger.debug("net_asn_ipv4_lookup_error ip=%s error=%s", ipv4, str(e))

        if ipv6:
            result["net_ipv6"] = ipv6
            try:
                asn_response = asn_reader.asn(ipv6)
                if asn_response.network:
                    result["net_network_v6"] = str(asn_response.network)
                # Use IPv6 ASN data only if IPv4 lookup did not provide it
                if "net_asn" not in result:
                    if asn_response.autonomous_system_number:
                        result["net_asn"] = asn_response.autonomous_system_number
                    if asn_response.autonomous_system_organization:
                        result["net_asn_org"] = asn_response.autonomous_system_organization
            except (geoip2.errors.GeoIP2Error, ValueError) as e:
                logger.debug("net_asn_ipv6_lookup_error ip=%s error=%s", ipv6, str(e))

        return result

    @classmethod
    async def execute(
        cls,
        relay: Relay,
        asn_reader: geoip2.database.Reader,
    ) -> Self:
        """Perform network/ASN lookups for a clearnet relay.

        Resolves the relay hostname to IPv4 and IPv6 addresses, then
        queries the GeoIP ASN database in a thread pool.

        Args:
            relay: Clearnet relay to look up.
            asn_reader: Open GeoLite2-ASN database reader.

        Returns:
            An ``Nip66NetMetadata`` instance with network data and logs.
        """
        logger.debug("net_testing relay=%s", relay.url)

        if relay.network != NetworkType.CLEARNET:
            return cls(
                data=Nip66NetData(),
                logs=Nip66NetLogs(
                    success=False, reason=f"requires clearnet, got {relay.network.value}"
                ),
            )

        logs: dict[str, Any] = {"success": False, "reason": None}

        resolved = await resolve_host(relay.host)

        data: dict[str, Any] = {}
        if resolved.has_ip:
            data = await asyncio.to_thread(cls._net, resolved.ipv4, resolved.ipv6, asn_reader)
            if data:
                logs["success"] = True
                logger.debug("net_completed relay=%s asn=%s", relay.url, data.get("net_asn"))
            else:
                logs["reason"] = "no ASN data found for IP"
                logger.debug("net_no_data relay=%s", relay.url)
        else:
            logs["reason"] = "could not resolve hostname to IP"
            logger.debug("net_no_ip relay=%s", relay.url)

        return cls(
            data=Nip66NetData.model_validate(Nip66NetData.parse(data)),
            logs=Nip66NetLogs.model_validate(logs),
        )
