#!/usr/bin/env python3
"""
Ping test: py-ipfs-lite → local Kubo daemon via /ipfs/ping/1.0.0

Uses new_host() directly (same as test_direct_kubo_dial.py) to avoid
the DHT random-walk continuously marking Kubo as failed in the negative
cache before we can connect.

Sends 5 pings and reports RTT for each one.
"""
import logging, os, subprocess, sys, tempfile, time
import trio

logging.basicConfig(
    level=logging.WARNING,
    format="%(asctime)s [%(levelname)s] [%(name)s] %(message)s",
    stream=sys.stderr,
)
logging.getLogger("libp2p.host.ping").setLevel(logging.DEBUG)

from libp2p import new_host
from libp2p.crypto.ed25519 import create_new_key_pair
from libp2p.crypto.x25519 import create_new_key_pair as x25519_kp
from libp2p.peer.id import ID
from libp2p.peer.peerinfo import info_from_p2p_addr
from libp2p.security.noise.transport import Transport as NoiseTransport
from libp2p.security.tls.transport import TLSTransport
from libp2p.host.ping import PingService, ID as PING_PROTO
from multiaddr import Multiaddr


def start_kubo(ipfs_path: str):
    env = {**os.environ, "IPFS_PATH": ipfs_path}
    subprocess.run(["ipfs", "init", "--profile=test"], env=env, check=True, capture_output=True)
    subprocess.run(["ipfs", "config", "--json", "Addresses.Swarm", '["/ip4/127.0.0.1/tcp/0"]'],
                   env=env, check=True, capture_output=True)
    subprocess.run(["ipfs", "bootstrap", "rm", "--all"], env=env, check=True, capture_output=True)
    subprocess.run(["ipfs", "config", "Addresses.API",     "/ip4/127.0.0.1/tcp/0"], env=env, check=True, capture_output=True)
    subprocess.run(["ipfs", "config", "Addresses.Gateway", "/ip4/127.0.0.1/tcp/0"], env=env, check=True, capture_output=True)

    log_path = os.path.join(ipfs_path, "daemon.log")
    lf = open(log_path, "w")
    proc = subprocess.Popen(["ipfs", "daemon"], env=env, stdout=lf, stderr=lf)
    deadline = time.time() + 30
    while time.time() < deadline:
        time.sleep(0.5)
        if proc.poll() is not None:
            raise RuntimeError("Kubo exited:\n" + open(log_path).read()[-1000:])
        try:
            if "Daemon is ready" in open(log_path).read():
                break
        except FileNotFoundError:
            pass
    else:
        proc.terminate()
        raise RuntimeError("Kubo didn't start in 30 s")

    peer_id_str = subprocess.check_output(["ipfs", "id", "-f=<id>"], env=env).decode().strip()
    addrs = subprocess.check_output(["ipfs", "id", "-f=<addrs>"], env=env).decode().strip().splitlines()
    addr = next((a.strip() for a in addrs if "127.0.0.1" in a and "/tcp/" in a), None)
    if not addr:
        proc.terminate()
        raise RuntimeError("No TCP loopback addr found")
    return proc, ID.from_base58(peer_id_str), addr


async def main():
    results = {}
    with tempfile.TemporaryDirectory(prefix="kubo_ping_") as ipfs_path:
        print("\n" + "=" * 55)
        print("  PING TEST: py-ipfs-lite → Kubo (/ipfs/ping/1.0.0)")
        print("=" * 55)

        # ── 1. Kubo ──────────────────────────────────────────────────
        print("\n[1/4] Starting local Kubo daemon …")
        try:
            kubo_proc, kubo_id, kubo_addr = start_kubo(ipfs_path)
            print(f"      ✅ Kubo  peer={kubo_id}")
            print(f"             addr={kubo_addr}")
            results["kubo"] = True
        except Exception as e:
            print(f"      ❌ {e}")
            return results

        try:
            # ── 2. Build a minimal libp2p host (no DHT, no random walk) ──
            print("\n[2/4] Starting py-libp2p host (no DHT, clean slate) …")
            try:
                host_key  = create_new_key_pair()
                noise_key = x25519_kp()
                sec_opt = {
                    "/noise":     NoiseTransport(host_key, noise_privkey=noise_key.private_key),
                    "/tls/1.0.0": TLSTransport(host_key),
                }
                host = new_host(
                    key_pair=host_key,
                    listen_addrs=[Multiaddr("/ip4/127.0.0.1/tcp/0")],
                    sec_opt=sec_opt,
                )
                print(f"      ✅ Host ready  id={host.get_id()}")
                results["peer"] = True
            except Exception as e:
                print(f"      ❌ {e}")
                return results

            async with host.run([Multiaddr("/ip4/127.0.0.1/tcp/0")]):
                # ── 3. Connect ────────────────────────────────────────
                print(f"\n[3/4] Connecting to Kubo …")
                try:
                    peer_info = info_from_p2p_addr(Multiaddr(kubo_addr))
                    with trio.fail_after(10):
                        await host.connect(peer_info)
                    print("      ✅ Connected (TCP + TLS/Noise + Yamux)")
                    results["connected"] = True
                except Exception as e:
                    print(f"      ❌ {type(e).__name__}: {e}")
                    results["connected"] = False
                    return results

                # ── 4. Ping 5×  ───────────────────────────────────────
                print("\n[4/4] Sending 5 pings via /ipfs/ping/1.0.0 …")

                # Register inbound handler so Kubo can ping us
                ping_svc = PingService(host)
                host.set_stream_handler(PING_PROTO, ping_svc.handle_ping)

                rtts   = []
                errors = []

                # Use ping_iter with ping_amt=5 to send all pings on one stream
                # — avoids the stream-per-ping open/close overhead and the
                # is_closed() callable bug in ping_iter's stream-reuse path
                # (which is only triggered on the *second* call, not the first).
                ping_svc2 = PingService(host)
                try:
                    with trio.fail_after(30):
                        i = 0
                        async for rtt in ping_svc2.ping_iter(kubo_id, ping_amt=5):
                            i += 1
                            rtts.append(rtt)
                            print(f"      ping {i}/5  RTT = {rtt} ms  ✅")
                except trio.TooSlowError:
                    errors.append("timeout waiting for pings")
                    print(f"      TIMEOUT after {len(rtts)} ping(s)  ❌")
                except Exception as e:
                    errors.append(f"{type(e).__name__}: {e}")
                    print(f"      {type(e).__name__}: {e}  ❌")

                results["pings"]  = rtts
                results["errors"] = errors

        finally:
            try:
                kubo_proc.terminate()
                kubo_proc.wait(timeout=5)
            except Exception:
                pass

    # ── Summary ──────────────────────────────────────────────────────
    print("\n" + "=" * 55)
    print("  SUMMARY")
    print("=" * 55)
    ok = lambda v: "✅" if v else "❌"
    print(f"  {ok(results.get('kubo'))}  Kubo daemon started")
    print(f"  {ok(results.get('peer'))}  py-libp2p host started")
    print(f"  {ok(results.get('connected'))}  TCP + TLS + Yamux connection")

    rtts   = results.get("pings",  [])
    errors = results.get("errors", [])
    if rtts:
        avg = sum(rtts) / len(rtts)
        print(f"  ✅  /ipfs/ping/1.0.0  —  {len(rtts)}/5 succeeded")
        print(f"      RTTs        : {rtts} ms")
        print(f"      min/avg/max : {min(rtts)}/{avg:.1f}/{max(rtts)} ms")
    else:
        print(f"  ❌  /ipfs/ping/1.0.0  —  0/5 succeeded")
    if errors:
        print("  Errors:")
        for err in errors:
            print(f"    - {err}")

    success = results.get("connected") and len(rtts) == 5
    print()
    if success:
        print("  🎉 PING WORKS — py-ipfs-lite ↔ Kubo /ipfs/ping/1.0.0 fully operational!")
    else:
        print("  ⚠️  Some checks failed — see above for details.")
    print("=" * 55)
    return results


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