from __future__ import annotations

import asyncio
import logging
import os
import time
from dataclasses import dataclass
from typing import Dict, Iterable, List, Optional, Sequence

import hashlib

from .addrman import AddressManager
from .errors import ProtocolError
from .netaddress import NetAddress
from .peer import PeerState
from p2p.constants import DEFAULT_TCP_PORT

from .protocol import (
    AddrMessage,
    GetHeadersMessage,
    HeadersMessage,
    InvMessage,
    InventoryVector,
    VersionMessage,
    build_version_message,
)
from .sync_manager import ChainAdapter, SyncManager

INV_TYPE_TX = 1
INV_TYPE_BLOCK = 2

log = logging.getLogger("animica.p2p.core_p2p.net_processing")


@dataclass
class NetProcessing:
    chain: ChainAdapter
    addrman: AddressManager
    max_addr_relay: int = 1000
    max_inv: int = int(os.getenv("ANIMICA_P2P_MAX_INV", "50000"))

    def __post_init__(self) -> None:
        self.sync = SyncManager(chain=self.chain)

    async def on_handshake_complete(self, peer: PeerState, send) -> None:
        msg = self.sync.build_getheaders()
        await send("getheaders", msg.serialize())
        self.sync.mark_headers_request(peer.peer_id)

    def build_version(self, peer: PeerState, local_addr: NetAddress, height: int) -> VersionMessage:
        return build_version_message(addr_recv=peer.address, addr_from=local_addr, start_height=height)

    async def handle_message(
        self,
        peer: PeerState,
        command: str,
        payload: bytes,
        *,
        send,
        disconnect,
        local_addr: NetAddress,
        peers: Iterable[PeerState],
        send_peer,
    ) -> None:
        peer.mark_recv()
        if command == "version":
            msg = VersionMessage.parse(payload)
            peer.version_received = True
            peer.version = msg.version
            peer.services = msg.services
            peer.user_agent = msg.user_agent
            peer.start_height = msg.start_height
            peer.relay = msg.relay
            try:
                reported_ip = msg.addr_from.ip
                if not reported_ip or reported_ip in {"0.0.0.0", "::"}:
                    reported_ip = peer.address.ip
                reported_port = int(msg.addr_from.port or 0)
                if not (1 <= reported_port <= 65535):
                    reported_port = DEFAULT_TCP_PORT
                if reported_port >= 49152 and DEFAULT_TCP_PORT:
                    reported_port = DEFAULT_TCP_PORT
                reported = NetAddress(
                    services=msg.services,
                    ip=reported_ip,
                    port=reported_port,
                    timestamp=int(time.time()),
                )
                self.addrman.add([reported])
                peer.address = reported
            except Exception:
                pass
            if not peer.version_sent:
                version = self.build_version(peer, local_addr, self.chain_height())
                await send("version", version.serialize())
                peer.version_sent = True
            await send("verack", b"")
            peer.verack_sent = True
            if peer.verack_received and not peer.handshake_complete:
                peer.handshake_complete = True
                await self.on_handshake_complete(peer, send)
            return

        if command == "verack":
            peer.verack_received = True
            if peer.version_received and not peer.handshake_complete:
                peer.handshake_complete = True
                await self.on_handshake_complete(peer, send)
            return

        if not peer.handshake_complete:
            raise ProtocolError("message before handshake")

        if command == "ping":
            await send("pong", payload)
            return

        if command == "addr":
            msg = AddrMessage.parse(payload)
            self.addrman.add(msg.addresses)
            return

        if command == "getaddr":
            addresses = self.addrman.get_addresses(limit=self.max_addr_relay)
            await send("addr", AddrMessage(addresses).serialize())
            return

        if command == "inv":
            msg = InvMessage.parse(payload)
            if len(msg.inventory) > self.max_inv:
                raise ProtocolError("inv too large")
            request = []
            for inv in msg.inventory:
                if inv.inv_hash in peer.known_inventory:
                    continue
                peer.known_inventory.add(inv.inv_hash)
                request.append(inv)
            if request:
                await send("getdata", InvMessage(request).serialize())
            return

        if command == "getdata":
            msg = InvMessage.parse(payload)
            response: List[InventoryVector] = []
            for inv in msg.inventory:
                if inv.inv_type == INV_TYPE_BLOCK:
                    block = self.chain.get_block(inv.inv_hash)
                    if block is None:
                        response.append(inv)
                        continue
                    await send("block", block)
                elif inv.inv_type == INV_TYPE_TX:
                    tx = self.chain.get_tx(inv.inv_hash)
                    if tx is None:
                        response.append(inv)
                        continue
                    await send("tx", tx)
            if response:
                await send("notfound", InvMessage(response).serialize())
            return

        if command == "getheaders":
            msg = GetHeadersMessage.parse(payload)
            headers = self.chain.headers_since(msg.locator_hashes, msg.stop_hash)
            await send("headers", HeadersMessage(headers).serialize())
            return

        if command == "headers":
            msg = HeadersMessage.parse(payload)
            self.sync.receive_headers(msg.headers)
            await self._queue_header_blocks(msg.headers, send)
            return

        if command == "block":
            self.chain.process_block(payload)
            self.sync.complete_inflight(self._block_hash(payload))
            await self._request_pending_blocks(send)
            await self.announce_block(peers, self._block_hash(payload), send_peer)
            return

        if command == "tx":
            self.chain.process_tx(payload)
            await self.announce_tx(peers, self._tx_hash(payload), send_peer)
            return

    def chain_height(self) -> int:
        header = self.chain.best_header()
        if not header:
            return 0
        return int.from_bytes(header[:4], "little")

    async def _queue_header_blocks(
        self, headers: Sequence[bytes], send
    ) -> None:
        block_hashes: list[bytes] = []
        for header in headers:
            block_hash = self._header_block_hash(header)
            if self.chain.get_block(block_hash) is not None:
                continue
            block_hashes.append(block_hash)
        if block_hashes:
            self.sync.queue_blocks(block_hashes)
            await self._request_pending_blocks(send)

    async def _request_pending_blocks(self, send) -> None:
        batch = self.sync.next_block_batch(self.max_inv)
        if not batch:
            return
        payload = InvMessage(
            [InventoryVector(inv_type=INV_TYPE_BLOCK, inv_hash=h) for h in batch]
        ).serialize()
        await send("getdata", payload)

    async def sync_tick(self, peers: Iterable[PeerState], send) -> None:
        now = time.time()
        self.sync.expire_inflight(now)
        local_height = self.chain_height()
        peers_list = [peer for peer in peers if peer.handshake_complete]
        if not peers_list:
            return
        best_peer = self._select_best_peer(peers_list, local_height)
        if best_peer is None:
            return
        self.sync.record_tip(best_peer.start_height)
        needs_headers = (
            best_peer.start_height > local_height
            or self.sync.pending_blocks
            or self.sync.inflight_blocks
        )
        if needs_headers and now - self.sync.last_headers_request >= self.sync.header_request_interval:
            await send(best_peer, "getheaders", self.sync.build_getheaders().serialize())
            self.sync.mark_headers_request(best_peer.peer_id)
        if needs_headers and now - self.sync.last_progress >= self.sync.stall_timeout:
            alt_peer = self._select_best_peer(
                peers_list, local_height, avoid_peer_id=best_peer.peer_id
            )
            if alt_peer is not None and alt_peer.peer_id != best_peer.peer_id:
                await send(alt_peer, "getheaders", self.sync.build_getheaders().serialize())
                self.sync.mark_headers_request(alt_peer.peer_id)
            if self.sync.should_log_stall(now):
                self.sync.mark_stall_logged(now)
                log.warning(
                    "sync stall detected",
                    extra={
                        "local_height": local_height,
                        "best_height": best_peer.start_height,
                        "pending": len(self.sync.pending_blocks),
                        "inflight": len(self.sync.inflight_blocks),
                    },
                )

    async def announce_block(self, peers: Iterable[PeerState], block_hash: bytes, send) -> None:
        inv = InventoryVector(inv_type=INV_TYPE_BLOCK, inv_hash=block_hash)
        payload = InvMessage([inv]).serialize()
        for peer in peers:
            if block_hash not in peer.known_inventory:
                peer.known_inventory.add(block_hash)
                await send(peer, "inv", payload)

    async def announce_tx(self, peers: Iterable[PeerState], tx_hash: bytes, send) -> None:
        inv = InventoryVector(inv_type=INV_TYPE_TX, inv_hash=tx_hash)
        payload = InvMessage([inv]).serialize()
        for peer in peers:
            if tx_hash not in peer.known_inventory:
                peer.known_inventory.add(tx_hash)
                await send(peer, "inv", payload)

    def _select_best_peer(
        self,
        peers: Sequence[PeerState],
        local_height: int,
        avoid_peer_id: Optional[str] = None,
    ) -> Optional[PeerState]:
        candidates = [peer for peer in peers if peer.start_height >= local_height]
        if avoid_peer_id:
            filtered = [peer for peer in candidates if peer.peer_id != avoid_peer_id]
            if filtered:
                candidates = filtered
        if not candidates:
            return None
        candidates.sort(key=lambda peer: (peer.start_height, peer.last_recv), reverse=True)
        return candidates[0]

    def _block_hash(self, payload: bytes) -> bytes:
        getter = getattr(self.chain, "block_hash", None)
        if callable(getter):
            return getter(payload)
        return hashlib.sha256(payload).digest()

    def _header_block_hash(self, header: bytes) -> bytes:
        if len(header) >= 72:
            candidate = header[40:72]
            if candidate != b"\x00" * 32:
                return candidate
        return hashlib.sha256(header).digest()

    def _tx_hash(self, payload: bytes) -> bytes:
        getter = getattr(self.chain, "tx_hash", None)
        if callable(getter):
            return getter(payload)
        return hashlib.sha256(payload).digest()
