"""Network pre-flight utilities for AiCippy CLI.

Provides lightweight connectivity checks that other modules call BEFORE
making network requests. All functions are non-throwing: they catch
exceptions internally and return True/False, logging results at DEBUG level.

These checks use only the ``socket`` module (DNS resolution and raw TCP
connections). No HTTP libraries are imported or used.

Example::

    from aicippy.utils.network import check_internet_connectivity

    if not check_internet_connectivity():
        console.print("No internet connection. Please check your network.")
        raise SystemExit(1)
"""

from __future__ import annotations

import socket
import time
from typing import Final

from aicippy.utils.logging import get_logger

__all__ = [
    "check_cognito_reachable",
    "check_endpoint_reachable",
    "check_internet_connectivity",
    "check_pypi_reachable",
    "wait_for_connectivity",
]

logger = get_logger(__name__)

# ---------------------------------------------------------------------------
# DNS hosts used for the quick connectivity probe
# ---------------------------------------------------------------------------
_DNS_PROBE_HOSTS: Final[tuple[str, ...]] = (
    "pypi.org",
    "auth.aivibe.cloud",
)

_PYPI_HOST: Final[str] = "pypi.org"
_AUTH_HOST: Final[str] = "auth.aivibe.cloud"
_DEFAULT_PORT: Final[int] = 443


# ============================================================================
# Public API
# ============================================================================


def check_internet_connectivity(timeout: float = 3.0) -> bool:
    """Check basic internet connectivity via DNS resolution.

    Attempts to resolve well-known hosts (``pypi.org`` and the Cognito
    endpoint) using :func:`socket.getaddrinfo`. If *any* host resolves
    successfully, the machine has a working DNS path to the internet.

    No HTTP or TCP connections are made -- this is a DNS-only probe.

    Args:
        timeout: Maximum seconds to spend on *each* DNS lookup.

    Returns:
        ``True`` if at least one host resolved, ``False`` otherwise.

    """
    original_timeout = socket.getdefaulttimeout()
    try:
        socket.setdefaulttimeout(timeout)
        for host in _DNS_PROBE_HOSTS:
            try:
                socket.getaddrinfo(host, _DEFAULT_PORT)
                logger.debug(
                    "internet_connectivity_check_passed",
                    resolved_host=host,
                )
                return True
            except (socket.gaierror, OSError):
                logger.debug(
                    "dns_resolution_failed",
                    host=host,
                )
        logger.debug("internet_connectivity_check_failed")
        return False
    finally:
        socket.setdefaulttimeout(original_timeout)


def check_endpoint_reachable(
    host: str,
    port: int = _DEFAULT_PORT,
    timeout: float = 3.0,
) -> bool:
    """Check whether a specific ``host:port`` is reachable via TCP.

    Opens a TCP socket to the target and immediately closes it. No
    application-layer data (HTTP, TLS handshake, etc.) is exchanged.

    Args:
        host: Hostname or IP address.
        port: TCP port number (default ``443``).
        timeout: Connection timeout in seconds.

    Returns:
        ``True`` if the TCP connection succeeded, ``False`` otherwise.

    """
    sock: socket.socket | None = None
    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.settimeout(timeout)
        sock.connect((host, port))
        logger.debug(
            "endpoint_reachable",
            host=host,
            port=port,
        )
        return True
    except OSError:
        logger.debug(
            "endpoint_unreachable",
            host=host,
            port=port,
        )
        return False
    finally:
        if sock is not None:
            sock.close()


def check_pypi_reachable(timeout: float = 3.0) -> bool:
    """Check whether ``pypi.org:443`` is reachable via TCP.

    Convenience wrapper around :func:`check_endpoint_reachable` targeting
    the Python Package Index.

    Args:
        timeout: Connection timeout in seconds.

    Returns:
        ``True`` if PyPI is reachable, ``False`` otherwise.

    """
    reachable = check_endpoint_reachable(
        host=_PYPI_HOST,
        port=_DEFAULT_PORT,
        timeout=timeout,
    )
    logger.debug("pypi_reachability_result", reachable=reachable)
    return reachable


def check_cognito_reachable(
    timeout: float = 3.0,
) -> bool:
    """Check whether the auth endpoint (auth.aivibe.cloud) is reachable via TCP.

    Args:
        timeout: Connection timeout in seconds.

    Returns:
        ``True`` if the auth endpoint is reachable, ``False`` otherwise.

    """
    reachable = check_endpoint_reachable(
        host=_AUTH_HOST,
        port=_DEFAULT_PORT,
        timeout=timeout,
    )
    logger.debug(
        "auth_reachability_result",
        host=_AUTH_HOST,
        reachable=reachable,
    )
    return reachable


def wait_for_connectivity(
    timeout: float = 10.0,
    check_interval: float = 1.0,
) -> bool:
    """Poll for internet connectivity until *timeout* elapses.

    Repeatedly calls :func:`check_internet_connectivity` at
    *check_interval* intervals. Returns as soon as connectivity is
    detected or when the deadline is exceeded.

    Args:
        timeout: Maximum total seconds to wait.
        check_interval: Seconds between successive checks.

    Returns:
        ``True`` if connectivity was established within *timeout*,
        ``False`` if the timeout expired without success.

    """
    deadline = time.monotonic() + timeout
    attempts = 0

    while True:
        attempts += 1
        per_check_timeout = min(check_interval, deadline - time.monotonic())
        if per_check_timeout <= 0:
            break

        if check_internet_connectivity(timeout=per_check_timeout):
            logger.debug(
                "wait_for_connectivity_succeeded",
                attempts=attempts,
                elapsed=timeout - (deadline - time.monotonic()),
            )
            return True

        remaining = deadline - time.monotonic()
        if remaining <= 0:
            break

        sleep_duration = min(check_interval, remaining)
        time.sleep(sleep_duration)

    logger.debug(
        "wait_for_connectivity_timed_out",
        attempts=attempts,
        timeout=timeout,
    )
    return False
