#!/usr/bin/env python3
"""
py-ipfs-lite Peer ↔ local Kubo interop test  (TLS security transport)
=======================================================================

Starts a fresh local Kubo daemon, brings up a py-ipfs-lite **Peer** whose
underlying libp2p host is configured with **TLS-only** security (no Noise),
connects it to Kubo, then:

  1. Performs the /ipfs/id/1.0.0 (identify) exchange and measures latency
  2. Sends 10 pings via /ipfs/ping/1.0.0 and reports individual + aggregate RTTs
  3. Prints a full result table at the end

Run:  uv run python test_peer_kubo_interop.py
"""

import logging
import os
import subprocess
import sys
import tempfile
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("peer_kubo")
LOG.setLevel(logging.DEBUG)
_h = logging.StreamHandler(sys.stdout)
_h.setFormatter(logging.Formatter("%(message)s"))
LOG.addHandler(_h)
LOG.propagate = False

from libp2p import new_host
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 libp2p.security.tls.transport import TLSTransport
from multiaddr import Multiaddr

from py_ipfs_lite.config import Config
from py_ipfs_lite.interfaces import HostAdapter
from py_ipfs_lite.peer import Peer

IDENTIFY_PROTO = "/ipfs/id/1.0.0"
PING_COUNT = 10
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 ev(icon: str, msg: str) -> None:
    LOG.info(f"  {ts()}  {icon}  {msg}")


# ── Kubo helper ──────────────────────────────────────────────────────────────

def start_kubo(ipfs_path: str):
    env = {**os.environ, "IPFS_PATH": ipfs_path}
    for cmd in [
        ["ipfs", "init", "--profile=test"],
        ["ipfs", "config", "--json", "Addresses.Swarm", '["/ip4/127.0.0.1/tcp/0"]'],
        ["ipfs", "bootstrap", "rm", "--all"],
        ["ipfs", "config", "Addresses.API",     "/ip4/127.0.0.1/tcp/0"],
        ["ipfs", "config", "Addresses.Gateway", "/ip4/127.0.0.1/tcp/0"],
    ]:
        subprocess.run(cmd, 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 early:\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 did not start within 30 s")

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


# ── Identify helper ──────────────────────────────────────────────────────────

async def do_identify(raw_host, kubo_id: ID) -> tuple[float, int]:
    """Open /ipfs/id/1.0.0, drain the response, return (elapsed_s, bytes)."""
    t0 = time.monotonic()
    stream = await raw_host.new_stream(kubo_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() - t0, len(data)


# ── Main ─────────────────────────────────────────────────────────────────────

async def main() -> None:
    print()
    print("=" * 65)
    print("  py-ipfs-lite Peer ↔ Kubo  (TLS · identify + 10× ping)")
    print("=" * 65)

    results: dict = {}

    with tempfile.TemporaryDirectory(prefix="kubo_peer_interop_") as ipfs_path:

        # ── 1. Start Kubo ────────────────────────────────────────────
        ev("🚀", "Starting local Kubo daemon …")
        try:
            kubo_proc, kubo_id, kubo_addr = start_kubo(ipfs_path)
            ev("✅", f"Kubo ready   peer={kubo_id}")
            ev("   ", f"             addr={kubo_addr}")
            results["kubo"] = True
        except Exception as exc:
            ev("❌", f"Kubo failed: {exc}")
            return

        try:
            # ── 2. Build py-ipfs-lite Peer with TLS-only security ───────
            #    We construct the raw libp2p host ourselves (TLS, no Noise),
            #    wrap it in HostAdapter, then inject it into the Peer so that
            #    Peer._create_host() is skipped entirely.
            ev("🔧", "Building py-ipfs-lite Peer (TLS-only, offline, in-memory) …")
            host_key = create_new_key_pair()
            sec_opt = {
                "/tls/1.0.0": TLSTransport(host_key),
            }
            raw_host = new_host(
                key_pair=host_key,
                listen_addrs=[Multiaddr("/ip4/127.0.0.1/tcp/0")],
                sec_opt=sec_opt,
            )
            host_adapter = HostAdapter(raw_host)

            cfg = Config(
                offline=True,          # no DHT / no random-walk
                blockstore_type="memory",
            )
            peer = Peer(
                config=cfg,
                host=host_adapter,     # inject TLS host — skips _create_host()
                listen_addrs=[Multiaddr("/ip4/127.0.0.1/tcp/0")],
            )

            async with peer:
                raw_host = peer.host._host   # unwrap HostAdapter → IHost
                ev("✅", f"Peer ready   id={raw_host.get_id()}")
                results["peer"] = True

                # ── 3. Register inbound ping handler ─────────────────
                ping_svc = PingService(raw_host)
                raw_host.set_stream_handler(PING_PROTO, ping_svc.handle_ping)
                ev("📋", f"Inbound ping handler registered ({PING_PROTO})")

                # ── 4. Connect to Kubo ────────────────────────────────
                ev("🔗", "Connecting py-ipfs-lite Peer → Kubo …")
                t_conn = time.monotonic()
                try:
                    peer_info = info_from_p2p_addr(Multiaddr(kubo_addr))
                    with trio.fail_after(10):
                        await peer.host.connect(peer_info)
                    conn_ms = int((time.monotonic() - t_conn) * 1000)
                    ev("✅", f"Connected!   ({conn_ms} ms)")
                    results["connected"] = True
                    results["conn_ms"] = conn_ms
                except Exception as exc:
                    ev("❌", f"Connection failed: {type(exc).__name__}: {exc}")
                    results["connected"] = False
                    return

                # Introspect security / muxer
                conns = raw_host.get_network().connections.get(kubo_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",
                        )
                    )
                    ev("   ", f"             Security={sec}  Muxer={mux_name}")

                # ── 5. Identify ───────────────────────────────────────
                ev("🔍", f"Running {IDENTIFY_PROTO} …")
                try:
                    with trio.fail_after(10):
                        id_elapsed, id_bytes = await do_identify(raw_host, kubo_id)
                    ev("✅", f"Identify OK  ({id_elapsed*1000:.0f} ms, {id_bytes} bytes)")
                    results["identify_ms"] = int(id_elapsed * 1000)
                    results["identify_bytes"] = id_bytes
                except Exception as exc:
                    ev("❌", f"Identify failed: {type(exc).__name__}: {exc}")
                    results["identify_ms"] = None

                # ── 6. 10 pings (us → Kubo) ───────────────────────────
                ev("📤", f"Sending {PING_COUNT} pings (us → Kubo) …")
                rtts: list[float] = []
                ping_errors: list[str] = []
                try:
                    with trio.fail_after(PING_COUNT * 5 + 10):
                        i = 0
                        async for rtt in ping_svc.ping_iter(kubo_id, ping_amt=PING_COUNT):
                            i += 1
                            rtts.append(rtt)
                            ev("✅", f"  ping {i:2d}/{PING_COUNT}  RTT = {rtt} ms")
                except trio.TooSlowError:
                    ping_errors.append(f"timeout after {len(rtts)} pings")
                    ev("❌", f"Ping timeout after {len(rtts)}/{PING_COUNT} pings")
                except Exception as exc:
                    ping_errors.append(f"{type(exc).__name__}: {exc}")
                    ev("❌", f"{type(exc).__name__}: {exc}")

                results["pings"] = rtts
                results["ping_errors"] = ping_errors

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

    # ── Final summary ─────────────────────────────────────────────────────
    print()
    print("=" * 65)
    print("  RESULTS")
    print("=" * 65)

    ok = lambda v: "PASS" if v else "FAIL"

    print(f"  [{ok(results.get('kubo'))}]  Kubo daemon started")
    print(f"  [{ok(results.get('peer'))}]  py-ipfs-lite Peer started")
    print(f"  [{ok(results.get('connected'))}]  TCP + TLS + Yamux connected"
          + (f"  ({results.get('conn_ms')} ms)" if results.get("conn_ms") else ""))

    id_ms = results.get("identify_ms")
    id_b  = results.get("identify_bytes")
    if id_ms is not None:
        print(f"  [PASS]  /ipfs/id/1.0.0           {id_ms} ms, {id_b} bytes")
    else:
        print(f"  [FAIL]  /ipfs/id/1.0.0")

    rtts = results.get("pings", [])
    if rtts:
        avg = sum(rtts) / len(rtts)
        print(f"  [PASS]  /ipfs/ping/1.0.0  {len(rtts)}/{PING_COUNT} pings succeeded")
        print(f"          RTTs       : {rtts} ms")
        print(f"          min/avg/max: {min(rtts)} / {avg:.1f} / {max(rtts)} ms")
    else:
        print(f"  [FAIL]  /ipfs/ping/1.0.0  0/{PING_COUNT} pings")

    errs = results.get("ping_errors", [])
    if errs:
        print(f"  Errors:")
        for e in errs:
            print(f"    - {e}")

    all_ok = (
        results.get("kubo")
        and results.get("peer")
        and results.get("connected")
        and id_ms is not None
        and len(rtts) == PING_COUNT
    )
    print()
    if all_ok:
        print("  ALL CHECKS PASSED — py-ipfs-lite Peer <-> Kubo interop is working!")
    else:
        print("  SOME CHECKS FAILED — see details above.")
    print("=" * 65)


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