from collections.abc import Awaitable, Callable
import logging
import secrets

import trio

from nanolib.abc import IHost
from nanolib.discovery.random_walk.config import (
    RANDOM_WALK_CONCURRENCY,
    RANDOM_WALK_RT_THRESHOLD,
    REFRESH_QUERY_TIMEOUT,
)
from nanolib.discovery.random_walk.exceptions import RandomWalkError
from nanolib.peer.id import ID
from nanolib.peer.peerinfo import PeerInfo

logger = logging.getLogger("nanolib.discovery.random_walk")


class RandomWalk:
    """
    Random Walk implementation for peer discovery in Kademlia DHT.

    Generates random peer IDs and performs FIND_NODE queries to discover
    new peers and populate the routing table.
    """

    def __init__(
        self,
        host: IHost,
        local_peer_id: ID,
        query_function: Callable[[bytes], Awaitable[list[ID]]],
    ):
        """
        Initialize Random Walk module.

        Args:
            host: The libp2p host instance
            local_peer_id: Local peer ID
            query_function: Function to query for closest peers given target key bytes

        """
        self.host = host
        self.local_peer_id = local_peer_id
        self.query_function = query_function

    def generate_random_peer_id(self) -> str:
        """
        Generate a completely random peer ID
         for random walk queries.

        Returns:
            Random peer ID as string

        """
        # Generate 32 random bytes (256 bits) - same as go-libp2p
        random_bytes = secrets.token_bytes(32)
        # Convert to hex string for query
        return random_bytes.hex()

    async def perform_random_walk(self) -> list[PeerInfo]:
        """
        Perform a single random walk operation.

        Returns:
            List of validated peers discovered during the walk

        """
        try:
            # Generate random peer ID
            random_peer_id = self.generate_random_peer_id()
            logger.info(f"Starting random walk for peer ID: {random_peer_id}")

            # Perform FIND_NODE query
            discovered_peer_ids: list[ID] = []

            with trio.move_on_after(REFRESH_QUERY_TIMEOUT):
                # Call the query function with target key bytes
                target_key = bytes.fromhex(random_peer_id)
                discovered_peer_ids = await self.query_function(target_key) or []

            if not discovered_peer_ids:
                logger.debug(f"No peers discovered in random walk for {random_peer_id}")
                return []

            logger.info(
                f"Discovered {len(discovered_peer_ids)} peers in random walk "
                f"for {random_peer_id[:8]}..."  # Show only first 8 chars for brevity
            )

            # Convert peer IDs to PeerInfo objects and validate
            validated_peers: list[PeerInfo] = []

            for peer_id in discovered_peer_ids:
                try:
                    # Get addresses from peerstore
                    addrs = self.host.get_peerstore().addrs(peer_id)
                    if addrs:
                        peer_info = PeerInfo(peer_id, addrs)
                        validated_peers.append(peer_info)
                except Exception as e:
                    logger.debug(f"Failed to create PeerInfo for {peer_id}: {e}")
                    continue

            return validated_peers

        except Exception as e:
            logger.error(f"Random walk failed: {e}")
            raise RandomWalkError(f"Random walk operation failed: {e}") from e

    async def run_concurrent_random_walks(
        self, count: int = RANDOM_WALK_CONCURRENCY, current_routing_table_size: int = 0
    ) -> list[PeerInfo]:
        """
        Run multiple random walks concurrently.

        Args:
            count: Number of concurrent random walks to perform
            current_routing_table_size: Current size of routing table (for optimization)

        Returns:
            Combined list of all validated peers discovered

        """
        all_validated_peers: list[PeerInfo] = []
        logger.info(f"Starting {count} concurrent random walks")

        # First, try to add peers from peerstore if routing table is small
        if current_routing_table_size < RANDOM_WALK_RT_THRESHOLD:
            try:
                peerstore_peers = self._get_peerstore_peers()
                if peerstore_peers:
                    logger.debug(
                        f"RT size ({current_routing_table_size}) below threshold, "
                        f"adding {len(peerstore_peers)} peerstore peers"
                    )
                all_validated_peers.extend(peerstore_peers)
            except Exception as e:
                logger.warning(f"Error processing peerstore peers: {e}")

        async def single_walk() -> None:
            try:
                peers = await self.perform_random_walk()
                all_validated_peers.extend(peers)
            except Exception as e:
                logger.warning(f"Concurrent random walk failed: {e}")
            return

        # Run concurrent random walks
        async with trio.open_nursery() as nursery:
            for _ in range(count):
                nursery.start_soon(single_walk)

        # Remove duplicates based on peer ID
        unique_peers = {}
        for peer in all_validated_peers:
            unique_peers[peer.peer_id] = peer

        result = list(unique_peers.values())
        logger.info(
            f"Concurrent random walks completed: {len(result)} unique peers discovered"
        )
        return result

    def _get_peerstore_peers(self) -> list[PeerInfo]:
        """
        Get peer info objects from the host's peerstore.

        Returns:
            List of PeerInfo objects from peerstore

        """
        try:
            peerstore = self.host.get_peerstore()
            peer_ids = peerstore.peers_with_addrs()

            peer_infos = []
            for peer_id in peer_ids:
                try:
                    # Skip local peer
                    if peer_id == self.local_peer_id:
                        continue

                    peer_info = peerstore.peer_info(peer_id)
                    if peer_info and peer_info.addrs:
                        # Filter for compatible addresses (TCP + IPv4)
                        if self._has_compatible_addresses(peer_info):
                            peer_infos.append(peer_info)
                except Exception as e:
                    logger.debug(f"Error getting peer info for {peer_id}: {e}")

            return peer_infos

        except Exception as e:
            logger.warning(f"Error accessing peerstore: {e}")
            return []

    def _has_compatible_addresses(self, peer_info: PeerInfo) -> bool:
        """
        Check if a peer has TCP+IPv4 compatible addresses.

        Args:
            peer_info: PeerInfo to check

        Returns:
            True if peer has compatible addresses

        """
        if not peer_info.addrs:
            return False

        for addr in peer_info.addrs:
            addr_str = str(addr)
            # Check for TCP and IPv4 compatibility, avoid QUIC
            if "/tcp/" in addr_str and "/ip4/" in addr_str and "/quic" not in addr_str:
                return True

        return False
