#!/usr/bin/env python3
"""
Live interop test: py-ipfs-lite (Peer) ↔ PUBLIC IPFS NETWORK

Connects to an actual public IPFS bootstrap node, then:
  1. Measures how long identify takes
  2. Sends an initial ping and measures RTT
  3. Watches for 3 minutes to see if the connection drops or if they ping us

Run:  uv run python test_public_interop.py
"""

import logging
import sys
import time

import trio

# ── Logging ─────────────────────────────────────────────────────────────────
logging.basicConfig(
    level=logging.WARNING,
    format="%(asctime)s %(levelname)s [%(name)s] %(message)s",
    stream=sys.stderr,
)
LOG = logging.getLogger("interop")
LOG.setLevel(logging.DEBUG)
_h = logging.StreamHandler(sys.stdout)
_h.setFormatter(logging.Formatter("%(message)s"))
LOG.addHandler(_h)
LOG.propagate = False

from libp2p.crypto.ed25519 import create_new_key_pair
from libp2p.host.ping import PingService
from libp2p.host.ping import ID as PING_PROTO
from libp2p.peer.id import ID
from libp2p.peer.peerinfo import info_from_p2p_addr
from multiaddr import Multiaddr

from py_ipfs_lite.config import Config
from py_ipfs_lite.peer import Peer
from py_ipfs_lite.cli import DEFAULT_BOOTSTRAP_PEERS

IDENTIFY_PROTO = "/ipfs/id/1.0.0"
OBSERVE_MINUTES = 3
T0 = time.monotonic()


def ts() -> str:
    elapsed = time.monotonic() - T0
    m, s = divmod(elapsed, 60)
    h, m = divmod(m, 60)
    ms = int((elapsed % 1) * 1000)
    return f"+{int(h):02d}:{int(m):02d}:{int(s):02d}.{ms:03d}"


def event(icon: str, msg: str) -> None:
    LOG.info(f"  {ts()}  {icon}  {msg}")


class LoggingPingService(PingService):
    def __init__(self, host, ping_log: list):
        super().__init__(host)
        self._ping_log = ping_log

    async def handle_ping(self, stream):
        peer_id = stream.muxed_conn.peer_id
        now = time.monotonic()
        self._ping_log.append(now)
        count = len(self._ping_log)
        if count == 1:
            event("📥", f"Public Node → us  INBOUND PING #{count}  (first re-ping)")
        else:
            gap = now - self._ping_log[-2]
            event("📥", f"Public Node → us  INBOUND PING #{count}  (gap since last = {gap:.1f} s)")
        await super().handle_ping(stream)


async def do_identify(host, target_id: ID) -> tuple:
    t_start = time.monotonic()
    stream = await host.new_stream(target_id, [IDENTIFY_PROTO])
    data = b""
    try:
        with trio.move_on_after(8):
            while True:
                try:
                    chunk = await stream.read(4096)
                    if not chunk:
                        break
                    data += chunk
                except Exception:
                    break
    finally:
        try:
            await stream.close()
        except Exception:
            pass
    elapsed = time.monotonic() - t_start
    return elapsed, len(data)


async def main():
    print()
    print("=" * 62)
    print("  LIVE INTEROP: py-ipfs-lite (Peer) ↔ PUBLIC IPFS NODE")
    print("=" * 62)

    # Use sg1.bootstrap.libp2p.io
    target_addr = "/ip4/15.235.144.210/tcp/4001/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt"
    target_info = info_from_p2p_addr(Multiaddr(target_addr))
    target_id = target_info.peer_id

    event("🚀", f"Selected public node: {target_id}")
    event("   ", f"                      {target_addr}")

    try:
        event("🔧", "Building py-ipfs-lite Peer …")
        host_key  = create_new_key_pair()
        import tempfile
        import os
        tmp_dir = tempfile.mkdtemp(prefix="lite_test_")
        config = Config(blockstore_path=os.path.join(tmp_dir, "blocks"))
        config.offline = True
        
        lite_peer = Peer(
            config=config,
            host_key=host_key,
            listen_addrs=["/ip4/0.0.0.0/tcp/0"],
        )
        
        await lite_peer.start()
        # Extract the REAL py-libp2p host from inside the HostAdapter wrapper
        host = getattr(lite_peer.host, "_host", lite_peer.host)
        my_id = ID.from_pubkey(host_key.public_key)
        event("✅", f"Peer ready   id={my_id}")

        # Register inbound-ping handler
        ping_log: list[float] = []
        ping_svc = LoggingPingService(host, ping_log)
        # Just set it (HostAdapter supports set_stream_handler but doesn't expose stream_handlers dict)
        host.set_stream_handler(PING_PROTO, ping_svc.handle_ping)
        event("📋", f"Inbound ping handler registered on {PING_PROTO}")

        # Connect
        event("🔗", f"Connecting to public node …")
        t_conn = time.monotonic()
        with trio.fail_after(15):
            await host.connect(target_info)
        conn_ms = int((time.monotonic() - t_conn) * 1000)
        event("✅", f"Connected!   ({conn_ms} ms)")

        conns = host.get_network().connections.get(target_id, [])
        if conns:
            conn = conns[0]
            muxed = getattr(conn, "muxed_conn", None)
            mux_name = type(muxed).__name__ if muxed else "unknown"
            sec = (
                getattr(conn, "security_protocol", None)
                or getattr(getattr(conn, "secured_conn", None), "protocol_id", "unknown")
            )
            event("   ", f"             Security={sec}  Muxer={mux_name}")

        # Identify
        event("🔍", f"Opening {IDENTIFY_PROTO} …")
        t_id = time.monotonic()
        try:
            with trio.fail_after(10):
                elapsed_id, id_bytes = await do_identify(host, target_id)
            event("✅", f"Identify complete  ({elapsed_id*1000:.0f} ms, {id_bytes} bytes)")
        except Exception as e:
            event("❌", f"Identify failed: {type(e).__name__}: {e}")

        # Outbound ping
        event("📤", "Sending outbound pings …")
        try:
            with trio.fail_after(10):
                rtt_list = await ping_svc.ping(target_id, ping_amt=3)
            for i, rtt in enumerate(rtt_list, 1):
                event("✅", f"us → Public Node  ping {i}/3  RTT = {rtt} ms")
        except Exception as e:
            event("❌", f"Outbound ping failed: {type(e).__name__}: {e}")
            rtt_list = []

        # Watch
        print()
        print(f"  Watching connection to public node for {OBSERVE_MINUTES} min …")
        print()

        deadline = trio.current_time() + OBSERVE_MINUTES * 60
        ping_count_at_start = len(ping_log)

        while trio.current_time() < deadline:
            remaining = int(deadline - trio.current_time())
            new_pings = len(ping_log) - ping_count_at_start

            conns = host.get_network().connections.get(target_id, [])
            alive = any(not c.is_closed for c in conns)
            conn_status = "connected" if alive else "DISCONNECTED"

            print(
                f"\r  [{ts()}]  Status: {conn_status} | "
                f"Public Node re-pings: {new_pings} | "
                f"Time left: {remaining}s   ",
                end="", flush=True,
            )
            await trio.sleep(2)

        print()

        # Summary
        print()
        print("=" * 62)
        print("  SUMMARY")
        print("=" * 62)
        print(f"  Connection time      : {conn_ms} ms")
        if 'elapsed_id' in locals():
            print(f"  Identify time        : {elapsed_id*1000:.0f} ms")
        else:
            print(f"  Identify time        : FAILED")
        print(f"  Outbound pings       : {len(rtt_list)} pings, RTTs={rtt_list} ms")
        new_pings = len(ping_log) - ping_count_at_start
        if new_pings:
            gaps = [
                f"{ping_log[i]-ping_log[i-1]:.1f}s"
                for i in range(1, len(ping_log))
            ]
            print(f"  Inbound re-pings     : {new_pings} received")
            if gaps:
                print(f"  Re-ping intervals    : {', '.join(gaps)}")
        else:
            print(f"  Inbound re-pings     : 0 in {OBSERVE_MINUTES} min")

        conns = host.get_network().connections.get(target_id, [])
        alive = any(not c.is_closed for c in conns)
        print(f"  Connection at end    : {'✅ alive' if alive else '❌ dropped'}")
        print("=" * 62)

    finally:
        try:
            await lite_peer.close()
        except Exception:
            pass


if __name__ == "__main__":
    trio.run(main)
