#!/usr/bin/env python3
"""
CLI peer ↔ local Kubo interop test (offline mode)
===================================================

Uses the exact same `create_and_start_peer` context-manager from cli.py to
spin up a py-ipfs-lite peer in **offline mode**, then:

  1. Connects manually to a fresh local Kubo daemon
  2. Runs /ipfs/id/1.0.0 (identify) and measures latency
  3. Sends 10 pings via /ipfs/ping/1.0.0 and reports RTTs

This exercises the real CLI startup path (key derivation, listen addresses,
blockstore, exchange) — offline=True simply skips DHT bootstrap.

Run:  uv run python test_cli_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("cli_interop")
LOG.setLevel(logging.DEBUG)
_h = logging.StreamHandler(sys.stdout)
_h.setFormatter(logging.Formatter("%(message)s"))
LOG.addHandler(_h)
LOG.propagate = False

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.cli import create_and_start_peer
from py_ipfs_lite.config import Config

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]:
    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("  CLI Peer (offline) ↔ Kubo  (identify + 10× ping)")
    print("=" * 65)

    results: dict = {}

    with tempfile.TemporaryDirectory(prefix="kubo_cli_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. Start peer via cli.create_and_start_peer ────────────
            #    offline=True  → no DHT, no random-walk, no public bootstrap
            #    blockstore_type="memory" → no disk I/O
            ev("🔧", "Starting peer via cli.create_and_start_peer (offline) …")
            config = Config(
                offline=True,
                blockstore_type="memory",
            )

            # port=0 → find_free_port() inside create_and_start_peer
            async with create_and_start_peer(
                port=0,
                seed=None,          # ephemeral identity
                config=config,
                bootstrap=False,    # offline=True already blocks it; explicit for clarity
            ) as peer:
                raw_host = peer.host._host   # HostAdapter → IHost
                ev("✅", f"Peer ready   id={raw_host.get_id()}")
                ev("   ", f"             addrs={[str(a) for a in raw_host.get_addrs()]}")
                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 CLI 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 negotiated 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'))}]  CLI peer started (offline mode)")
    print(
        f"  [{ok(results.get('connected'))}]  TCP + Noise + 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("  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 — CLI peer (offline) <-> Kubo fully working!")
    else:
        print("  SOME CHECKS FAILED — see details above.")
    print("=" * 65)


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