#!/usr/bin/env python3
import logging
import sys
import time
import os
import tempfile

import trio

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

# ── Logging ─────────────────────────────────────────────────────────────────
logging.basicConfig(level=logging.WARNING, 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

IDENTIFY_PROTO = "/ipfs/id/1.0.0"
OBSERVE_MINUTES = 3
PING_INTERVAL = 10  # Ping every 10 seconds to keep connection alive
T0 = time.monotonic()

BOOTSTRAP_NODES = {
    "ny5": "/ip4/51.81.93.51/tcp/4001/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa",
    "sv15": "/ip4/147.135.44.132/tcp/4001/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN",
    "sg1": "/ip4/15.235.144.210/tcp/4001/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt",
    "am6": "/ip4/54.38.47.166/tcp/4001/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb",
}

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):
    async def handle_ping(self, stream):
        # We don't strictly need to log inbound pings for this test, but just in case
        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
    return time.monotonic() - t_start, len(data)

async def keep_pinging(ping_svc, target_id: ID, name: str):
    """Background task to continually ping a specific node."""
    while True:
        await trio.sleep(PING_INTERVAL)
        try:
            with trio.fail_after(5):
                rtt_list = await ping_svc.ping(target_id, ping_amt=1)
            # event("📡", f"Keepalive Ping {name} RTT={rtt_list[0]}ms")
        except Exception:
            # event("❌", f"Keepalive Ping {name} failed")
            pass

async def main():
    print()
    print("=" * 70)
    print("  LIVE INTEROP: Connecting to 4 Bootstrap Nodes (Offline)")
    print("=" * 70)

    tmp_dir = tempfile.mkdtemp(prefix="lite_multi_")
    config = Config(blockstore_path=os.path.join(tmp_dir, "blocks"))
    config.offline = True
    
    host_key = create_new_key_pair()
    lite_peer = Peer(
        config=config,
        host_key=host_key,
        listen_addrs=["/ip4/0.0.0.0/tcp/0"],
    )
    
    event("🔧", "Starting offline Peer…")
    await lite_peer.start()
    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}")

    ping_svc = LoggingPingService(host)
    host.set_stream_handler(PING_PROTO, ping_svc.handle_ping)

    nodes = {}
    for name, addr in BOOTSTRAP_NODES.items():
        info = info_from_p2p_addr(Multiaddr(addr))
        nodes[name] = info.peer_id

    async def watch_loop(cancel_scope):
        print()
        print(f"  Watching connections for {OBSERVE_MINUTES} min …")
        print(f"  (Sending an outbound PING every {PING_INTERVAL} seconds to keep them alive)")
        print()

        deadline = trio.current_time() + OBSERVE_MINUTES * 60
        while trio.current_time() < deadline:
            remaining = int(deadline - trio.current_time())
            status_strs = []
            for name, pid in nodes.items():
                conns = host.get_network().connections.get(pid, [])
                alive = any(not c.is_closed for c in conns)
                indicator = "✅" if alive else "❌"
                status_strs.append(f"{name}:{indicator}")
                
            status_line = " ".join(status_strs)
            print(f"\r  [{ts()}]  {status_line}  |  Time left: {remaining}s   ", end="", flush=True)
            await trio.sleep(2)
            
        print("\n\n" + "=" * 70)
        print("  FINAL STATUS")
        print("=" * 70)
        for name, pid in nodes.items():
            conns = host.get_network().connections.get(pid, [])
            alive = any(not c.is_closed for c in conns)
            print(f"  {name:5}: {'Connected' if alive else 'Disconnected'}")
        print("=" * 70)
        cancel_scope.cancel()

    # 1. Connect and Identify all
    async with trio.open_nursery() as nursery:
        nursery.start_soon(watch_loop, nursery.cancel_scope)
        for name, addr in BOOTSTRAP_NODES.items():
            info = info_from_p2p_addr(Multiaddr(addr))
            
            async def connect_and_identify(n=name, i=info):
                event("🔗", f"Connecting to {n}…")
                try:
                    with trio.fail_after(10):
                        await host.connect(i)
                    event("✅", f"Connected to {n}!")
                    
                    with trio.fail_after(10):
                        await do_identify(host, i.peer_id)
                    event("✅", f"Identified {n}!")
                    
                    # Start periodic ping loop
                    nursery.start_soon(keep_pinging, ping_svc, i.peer_id, n)
                except Exception as e:
                    event("❌", f"Failed {n}: {e}")
                    
            nursery.start_soon(connect_and_identify)

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

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