#!/usr/bin/env python3
"""
CLI peer ↔ Kubo  — QUIC-v1 transport, zero custom handlers
===========================================================

Starts the peer exactly as cli.py does (create_and_start_peer), configures
Kubo to listen on QUIC-v1, then bootstraps only to Kubo's QUIC address.
No identify/ping handlers are registered manually — built-ins only.

Checks:
  1. Peer starts and connects to Kubo over QUIC-v1
  2. Connection is stable over 60 s (keep-alive loop fires every 15 s)
  3. Kubo confirms our peer in its swarm-peers list
  4. Verify ping works end-to-end using Kubo's own CLI

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

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

OBSERVE_SECS = 60
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 helpers ──────────────────────────────────────────────────────────────

def start_kubo(ipfs_path: str):
    env = {**os.environ, "IPFS_PATH": ipfs_path}
    for cmd in [
        ["ipfs", "init", "--profile=test"],
        # Listen on both TCP and QUIC-v1 so Kubo announces a QUIC address
        ["ipfs", "config", "--json", "Addresses.Swarm",
         '["/ip4/127.0.0.1/tcp/0", "/ip4/127.0.0.1/udp/0/quic-v1"]'],
        ["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:\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()

    # Prefer QUIC-v1 address; fall back to TCP if unavailable
    kubo_addr = next(
        (a.strip() for a in addrs_raw if "127.0.0.1" in a and "quic-v1" in a), None
    ) or 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 loopback addr found")
    return proc, ID.from_base58(peer_id_str), kubo_addr


def kubo_swarm_peers(env: dict) -> list[str]:
    """Return list of peer IDs currently in Kubo's swarm."""
    try:
        out = subprocess.check_output(
            ["ipfs", "swarm", "peers"], env=env, stderr=subprocess.DEVNULL
        ).decode().strip()
        return [line.strip() for line in out.splitlines() if line.strip()]
    except Exception:
        return []


def kubo_ping(env: dict, peer_id: str, count: int = 3) -> list[str]:
    """Run `ipfs ping` from Kubo's side targeting our peer. Returns output lines."""
    try:
        out = subprocess.check_output(
            ["ipfs", "ping", "-n", str(count), peer_id],
            env=env, stderr=subprocess.DEVNULL, timeout=15,
        ).decode().strip()
        return [line.strip() for line in out.splitlines() if line.strip()]
    except Exception as exc:
        return [f"ERROR: {exc}"]


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

async def main() -> None:
    print()
    print("=" * 65)
    print("  CLI Peer ↔ Kubo  (QUIC-v1, no custom handlers)")
    print("=" * 65)

    results: dict = {}

    with tempfile.TemporaryDirectory(prefix="kubo_cli_only_") as ipfs_path:
        kubo_env = {**os.environ, "IPFS_PATH": 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 CLI peer (no bootstrap yet) ──────────────────
            ev("🔧", "Starting CLI peer via create_and_start_peer …")
            ev("   ", "(QUIC-v1 listen addr included — no custom handlers)")
            config = Config(
                offline=False,          # DHT on, but no public peers to walk
                blockstore_type="memory",
            )

            async with create_and_start_peer(
                port=0,
                seed=None,
                config=config,
                bootstrap=False,        # skip public bootstrap; we control it
            ) as peer:
                raw_host = peer.host._host
                our_id = raw_host.get_id()
                our_addrs = raw_host.get_addrs()
                ev("✅", f"Peer ready   id={our_id}")
                ev("   ", f"             protocols={raw_host.get_mux().get_protocols()}")

                # ── 3. Bootstrap to Kubo only ──────────────────────────
                ev("🔗", f"Bootstrapping to Kubo QUIC addr: {kubo_addr}")
                t_conn = time.monotonic()
                try:
                    with trio.fail_after(10):
                        await peer.bootstrap([kubo_addr])
                    conn_ms = int((time.monotonic() - t_conn) * 1000)
                    ev("✅", f"Bootstrap complete  ({conn_ms} ms)")
                    results["connected"] = True
                    results["conn_ms"] = conn_ms
                except Exception as exc:
                    ev("❌", f"Bootstrap failed: {type(exc).__name__}: {exc}")
                    results["connected"] = False
                    return

                # Introspect the negotiated transport
                conns = raw_host.get_network().connections.get(kubo_id, [])
                alive_our_side = any(not c.is_closed for c in conns)
                ev("   ", f"             Our side sees Kubo: {'connected' if alive_our_side else 'NOT connected'}")
                if conns:
                    conn = conns[0]
                    muxed = getattr(conn, "muxed_conn", None)
                    mux_name = type(muxed).__name__ if muxed else "unknown"
                    transport = type(getattr(conn, "transport", None)).__name__ if hasattr(conn, "transport") else "unknown"
                    ev("   ", f"             Muxer={mux_name}  Transport={transport}")

                # Check Kubo's side
                peers_in_kubo = kubo_swarm_peers(kubo_env)
                our_id_b58 = our_id.to_base58()
                kubo_sees_us = any(our_id_b58 in p for p in peers_in_kubo)
                ev("   ", f"             Kubo sees us:       {'yes' if kubo_sees_us else 'NO'}")
                results["kubo_sees_us_at_start"] = kubo_sees_us

                # ── 4. Kubo pings us (their CLI → our built-in handler) ─
                ev("📤", f"Kubo → us: running `ipfs ping -n 3 {our_id_b58}` …")
                ping_lines = await trio.to_thread.run_sync(
                    lambda: kubo_ping(kubo_env, our_id_b58, count=3)
                )
                for line in ping_lines:
                    ev("   ", f"  {line}")
                kubo_ping_ok = any("Average latency" in l or "Pong" in l for l in ping_lines)
                results["kubo_ping_ok"] = kubo_ping_ok
                ev("✅" if kubo_ping_ok else "❌", f"Kubo→us ping: {'OK' if kubo_ping_ok else 'FAILED'}")

                # ── 5. Monitor connection stability for OBSERVE_SECS ───
                print()
                print(f"  Monitoring connection stability for {OBSERVE_SECS} s …")
                print(f"  (Peer keep-alive loop fires every ~15 s automatically)")
                print()

                deadline = trio.current_time() + OBSERVE_SECS
                snapshots = []

                while trio.current_time() < deadline:
                    remaining = int(deadline - trio.current_time())
                    conns = raw_host.get_network().connections.get(kubo_id, [])
                    alive = any(not c.is_closed for c in conns)
                    peers_now = kubo_swarm_peers(kubo_env)
                    kubo_still = any(our_id_b58 in p for p in peers_now)
                    snapshots.append(alive and kubo_still)
                    status = "connected" if alive else "DISCONNECTED"
                    kubo_status = "yes" if kubo_still else "NO"
                    print(
                        f"\r  [{ts()}]  our-side: {status:12s}  "
                        f"kubo-sees-us: {kubo_status}  "
                        f"remaining: {remaining:3d}s   ",
                        end="", flush=True,
                    )
                    await trio.sleep(5)

                print()  # newline after status line
                results["stability_snapshots"] = snapshots
                results["stable"] = all(snapshots)

                # ── 6. Final Kubo ping after monitoring ────────────────
                ev("📤", "Final Kubo→us ping after monitoring period …")
                ping_lines2 = await trio.to_thread.run_sync(
                    lambda: kubo_ping(kubo_env, our_id_b58, count=3)
                )
                for line in ping_lines2:
                    ev("   ", f"  {line}")
                final_ping_ok = any("Average latency" in l or "Pong" in l for l in ping_lines2)
                results["final_ping_ok"] = final_ping_ok
                ev("✅" if final_ping_ok else "❌",
                   f"Final ping: {'OK' if final_ping_ok else 'FAILED'}")

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

    # ── 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('connected'))}]  CLI peer connected to Kubo via QUIC-v1"
          + (f"  ({results.get('conn_ms')} ms)" if results.get("conn_ms") else ""))
    print(f"  [{ok(results.get('kubo_sees_us_at_start'))}]  Kubo sees our peer in swarm (at connect)")
    print(f"  [{ok(results.get('kubo_ping_ok'))}]  Kubo→us ping (built-in handler, no custom registration)")

    snaps = results.get("stability_snapshots", [])
    stable = results.get("stable", False)
    drop_count = snaps.count(False)
    print(f"  [{ok(stable)}]  Connection stable over {OBSERVE_SECS} s"
          f"  ({len(snaps)} checks, {drop_count} drops)")
    print(f"  [{ok(results.get('final_ping_ok'))}]  Kubo→us ping after {OBSERVE_SECS} s monitoring")

    all_ok = (
        results.get("kubo")
        and results.get("connected")
        and results.get("kubo_sees_us_at_start")
        and results.get("kubo_ping_ok")
        and stable
        and results.get("final_ping_ok")
    )
    print()
    if all_ok:
        print("  ALL CHECKS PASSED — CLI peer holds a stable QUIC-v1 connection to Kubo")
        print("  with zero custom handler registration.")
    else:
        print("  SOME CHECKS FAILED — see details above.")
    print("=" * 65)


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