#!/usr/bin/env python3
"""
aip — Unified CLI for the Agent Identity Protocol.

Commands:
  register   Register a new agent DID
  verify     Verify a signed artifact
  vouch      Vouch for another agent
  sign       Sign a skill directory or file
  message    Send an encrypted message
  rotate-key Rotate your signing key
  badge      Show trust badge for a DID
  whoami     Show your current identity
"""

import argparse
import base64
import hashlib
import json
import os
import sys
import time
from pathlib import Path

from aip_identity import __version__

AIP_SERVICE = os.environ.get("AIP_SERVICE_URL", "https://aip-service.fly.dev")
CREDENTIALS_PATHS = [
    Path.home() / ".aip" / "credentials.json",
    Path.home() / ".openclaw" / "workspace" / "credentials" / "aip_credentials.json",
]
SIGNATURE_FILE = ".aip-signature.json"
IGNORE_PATTERNS = {
    ".git", "__pycache__", ".aip-signature.json", ".DS_Store",
    "node_modules", ".env", "*.pyc", "*.pyo",
}


# ── Helpers ──────────────────────────────────────────────────────────

def find_credentials():
    """Find AIP credentials from known locations.
    
    Checks AIP_CREDENTIALS_PATH env var first, then standard locations.
    """
    # Check env var override first
    env_path = os.environ.get("AIP_CREDENTIALS_PATH")
    if env_path:
        p = Path(env_path)
        if p.exists():
            try:
                with open(p) as f:
                    creds = json.load(f)
                if "did" in creds and "private_key" in creds:
                    return creds
            except (json.JSONDecodeError, KeyError):
                pass
        # If env var is set but file doesn't exist/is invalid, don't fall through
        return None

    for path in CREDENTIALS_PATHS:
        if path.exists():
            try:
                with open(path) as f:
                    creds = json.load(f)
                if "did" in creds and "private_key" in creds:
                    return creds
            except (json.JSONDecodeError, KeyError):
                continue
    return None


def save_credentials(creds, path=None):
    """Save credentials to disk."""
    if path is None:
        path = CREDENTIALS_PATHS[0]
    path.parent.mkdir(parents=True, exist_ok=True)
    with open(path, "w") as f:
        json.dump(creds, f, indent=2)
    os.chmod(path, 0o600)
    print(f"Credentials saved to {path}")
    print(f"  ⚠️  Private key stored in plaintext (file permissions set to 600).")


def require_credentials():
    """Return credentials or exit with an error, offering quickstart."""
    creds = find_credentials()
    if not creds:
        print("\n🦞 Welcome to AIP! You don't have an identity yet.")
        print()
        print("  Get started in 30 seconds:")
        print("    aip quickstart              Auto-setup (fastest)")
        print("    aip init github my_agent    Choose your platform + name")
        print()
        print("  Or explore first:")
        print("    aip demo                    See AIP in action (60-second demo)")
        print("    aip list                    See agents in the network")
        print()

        # Auto-offer quickstart for interactive terminals
        if sys.stdin.isatty():
            try:
                answer = input("  Run quickstart now? [Y/n] ").strip().lower()
                if answer in ("", "y", "yes"):
                    # Simulate quickstart
                    import argparse
                    qs_args = argparse.Namespace(
                        platform=None, username=None, service=None,
                        name=None, bio=None, force=False
                    )
                    cmd_quickstart(qs_args)
                    # Try again after quickstart
                    creds = find_credentials()
                    if creds:
                        return creds
            except (EOFError, KeyboardInterrupt):
                print()

        sys.exit(1)
    return creds


def get_client(creds=None, service_url=None):
    """Build an AIPClient from stored credentials."""
    from aip_identity.client import AIPClient
    if creds is None:
        creds = require_credentials()
    return AIPClient(
        did=creds["did"],
        public_key=creds.get("public_key", ""),
        private_key=creds.get("private_key", ""),
        service_url=service_url or creds.get("service", AIP_SERVICE),
    )


def hash_file(filepath):
    """SHA-256 hash of a file."""
    h = hashlib.sha256()
    with open(filepath, "rb") as f:
        for chunk in iter(lambda: f.read(8192), b""):
            h.update(chunk)
    return h.hexdigest()


def should_ignore(path, base):
    rel = os.path.relpath(path, base)
    parts = Path(rel).parts
    for part in parts:
        if part in IGNORE_PATTERNS:
            return True
        for pattern in IGNORE_PATTERNS:
            if pattern.startswith("*") and part.endswith(pattern[1:]):
                return True
    return os.path.basename(path) == SIGNATURE_FILE


def collect_files(target_path):
    target = Path(target_path)
    if target.is_file():
        return {str(target.name): str(target)}
    files = {}
    for root, dirs, filenames in os.walk(target):
        dirs[:] = [d for d in dirs if d not in IGNORE_PATTERNS]
        for fname in sorted(filenames):
            fpath = os.path.join(root, fname)
            if not should_ignore(fpath, target):
                rel_path = os.path.relpath(fpath, target)
                files[rel_path] = fpath
    return dict(sorted(files.items()))


def decode_key(key_str):
    """Decode a key from hex or base64."""
    try:
        return bytes.fromhex(key_str)
    except ValueError:
        return base64.b64decode(key_str)


# ── Commands ─────────────────────────────────────────────────────────

def cmd_register(args):
    """Register a new agent DID."""
    try:
        import nacl.signing
    except ImportError:
        print("Error: PyNaCl required. pip install pynacl")
        sys.exit(1)
    try:
        import requests
    except ImportError:
        print("Error: requests required. pip install requests")
        sys.exit(1)

    service_url = args.service or AIP_SERVICE

    if args.secure:
        # Local key generation (private key never leaves machine)
        signing_key = nacl.signing.SigningKey.generate()
        pub_bytes = bytes(signing_key.verify_key)
        priv_bytes = bytes(signing_key)

        pub_b64 = base64.b64encode(pub_bytes).decode()
        priv_b64 = base64.b64encode(priv_bytes).decode()
        did = "did:aip:" + hashlib.sha256(pub_bytes).hexdigest()[:32]

        resp = requests.post(
            f"{service_url}/register",
            json={
                "did": did,
                "public_key": pub_b64,
                "platform": args.platform,
                "username": args.username,
            },
            timeout=10,
        )
        if not resp.ok:
            print(f"Registration failed: {resp.text}")
            sys.exit(1)

        data = resp.json()
        if "did" in data:
            did = data["did"]

        creds = {
            "did": did,
            "public_key": pub_b64,
            "private_key": priv_b64,
            "platform": args.platform,
            "username": args.username,
            "service": service_url,
            "registered_at": int(time.time()),
        }
        save_credentials(creds)
        print(f"\n✅ Registered (secure)!")
        print(f"   DID: {did}")
        print(f"   Private key never left your machine.")
        print(f"\n   Tip: Run `aip quickstart` for a guided setup.")
    else:
        from aip_identity.client import AIPClient
        client = AIPClient.register(
            platform=args.platform,
            platform_id=args.username,
            service_url=service_url,
        )
        creds = {
            "did": client.did,
            "public_key": client.public_key,
            "private_key": client.private_key,
            "platform": args.platform,
            "username": args.username,
            "service": service_url,
            "registered_at": int(time.time()),
        }
        save_credentials(creds)
        print(f"\n✅ Registered!")
        print(f"   DID: {client.did}")
        print(f"\n   Tip: Run `aip quickstart` for a guided setup.")

    if args.output:
        save_credentials(creds, Path(args.output))


def cmd_verify(args):
    """Verify a signed artifact."""
    try:
        import nacl.signing
        import nacl.exceptions
    except ImportError:
        print("Error: PyNaCl required. pip install pynacl")
        sys.exit(1)

    target = Path(args.path)
    sig_path = (target.parent if target.is_file() else target) / SIGNATURE_FILE

    if not sig_path.exists():
        print(f"No signature found at {sig_path}")
        sys.exit(1)

    with open(sig_path) as f:
        signed = json.load(f)

    signature = signed.pop("signature")
    manifest = signed

    # Verify cryptographic signature
    verify_key = nacl.signing.VerifyKey(decode_key(manifest["public_key"]))
    payload = json.dumps(manifest, sort_keys=True, separators=(",", ":")).encode()
    try:
        verify_key.verify(payload, bytes.fromhex(signature))
    except nacl.exceptions.BadSignatureError:
        print("❌ SIGNATURE INVALID — files may have been tampered with!")
        sys.exit(1)

    # Verify file hashes
    files = collect_files(args.path)
    mismatches = [f for f in manifest["files"] if f in files and hash_file(files[f]) != manifest["files"][f]]
    missing = [f for f in manifest["files"] if f not in files]

    if mismatches or missing:
        print("❌ VERIFICATION FAILED")
        for f in mismatches:
            print(f"   Modified: {f}")
        for f in missing:
            print(f"   Missing: {f}")
        sys.exit(1)

    new_files = [f for f in files if f not in manifest["files"]]
    print(f"✅ Signature valid!")
    print(f"   Signer: {manifest['did']}")
    print(f"   Signed: {time.strftime('%Y-%m-%d %H:%M UTC', time.gmtime(manifest['timestamp']))}")
    print(f"   Files: {len(manifest['files'])} verified")
    if new_files:
        print(f"   ⚠️  {len(new_files)} new file(s) not in original signature")
    print(f"\n   Tip: Run `aip vouch {manifest['did']}` to vouch for this agent.")


def cmd_vouch(args):
    """Vouch for another agent."""
    creds = require_credentials()
    client = get_client(creds, args.service)

    vouch_id = client.vouch(
        target_did=args.target_did,
        scope=args.scope or "GENERAL",
        statement=args.statement,
        ttl_days=args.ttl_days,
    )
    print(f"✅ Vouched for {args.target_did}")
    print(f"   Vouch ID: {vouch_id}")
    print(f"   Scope: {args.scope or 'GENERAL'}")


def cmd_revoke(args):
    """Revoke a vouch you previously issued."""
    creds = require_credentials()
    client = get_client(creds, args.service)

    result = client.revoke(vouch_id=args.vouch_id)
    print(f"✅ Vouch revoked: {args.vouch_id}")
    if result.get("message"):
        print(f"   {result['message']}")


def cmd_sign(args):
    """Sign a skill directory or file."""
    try:
        import nacl.signing
    except ImportError:
        print("Error: PyNaCl required. pip install pynacl")
        sys.exit(1)

    creds = require_credentials()
    files = collect_files(args.path)
    if not files:
        print(f"No files found in {args.path}")
        sys.exit(1)

    print(f"Signing {len(files)} file(s) as {creds['did']}...")

    file_hashes = {rel: hash_file(abs_p) for rel, abs_p in files.items()}
    manifest = {
        "version": "1.0",
        "did": creds["did"],
        "public_key": creds.get("public_key", ""),
        "timestamp": int(time.time()),
        "files": file_hashes,
    }

    signing_key = nacl.signing.SigningKey(decode_key(creds["private_key"]))
    payload = json.dumps(manifest, sort_keys=True, separators=(",", ":")).encode()
    sig = signing_key.sign(payload).signature.hex()

    output = {**manifest, "signature": sig}
    target = Path(args.path)
    sig_path = (target.parent if target.is_file() else target) / SIGNATURE_FILE
    with open(sig_path, "w") as f:
        json.dump(output, f, indent=2)

    print(f"✅ Signed! Manifest written to {sig_path}")
    print(f"   DID: {creds['did']}")
    print(f"   Files: {len(files)}")


def cmd_message(args):
    """Send an encrypted message to another agent."""
    creds = require_credentials()
    client = get_client(creds, args.service)

    import requests as req
    import base64 as b64
    from datetime import datetime, timezone

    # Fetch recipient's public key for encryption
    lookup_resp = req.get(f"{client.service_url}/lookup/{args.recipient}", timeout=10)
    if not lookup_resp.ok:
        print(f"❌ Could not find recipient: {lookup_resp.text}")
        sys.exit(1)
    pub_key_b64 = lookup_resp.json()["public_key"]

    # Encrypt with SealedBox
    from nacl.signing import VerifyKey
    from nacl.public import SealedBox
    vk = VerifyKey(b64.b64decode(pub_key_b64))
    box = SealedBox(vk.to_curve25519_public_key())
    encrypted_b64 = b64.b64encode(box.encrypt(args.content.encode())).decode()

    # Sign with timestamp (domain-separated)
    timestamp = datetime.now(timezone.utc).isoformat()
    sig_payload = f"{client.did}|{args.recipient}|{timestamp}|{encrypted_b64}"
    signature = client.sign(sig_payload.encode())

    resp = req.post(
        f"{client.service_url}/message",
        json={
            "sender_did": client.did,
            "recipient_did": args.recipient,
            "encrypted_content": encrypted_b64,
            "signature": signature,
            "timestamp": timestamp,
        },
        timeout=10,
    )
    if resp.ok:
        print(f"✅ Message sent to {args.recipient}")
    else:
        print(f"❌ Failed: {resp.text}")
        sys.exit(1)


def cmd_messages(args):
    """Retrieve and optionally decrypt your messages."""
    creds = require_credentials()
    service = args.service or creds.get("service", AIP_SERVICE)

    import requests as req

    # Step 1: Get challenge
    ch_resp = req.post(f"{service}/challenge", json={"did": creds["did"]}, timeout=10)
    if not ch_resp.ok:
        print(f"❌ Challenge failed: {ch_resp.text}")
        sys.exit(1)
    challenge = ch_resp.json().get("challenge")

    # Step 2: Sign challenge
    client = get_client(creds, service)
    signature = client.sign(challenge.encode())

    # Step 3: Retrieve messages
    msg_resp = req.post(
        f"{service}/messages",
        json={
            "did": creds["did"],
            "challenge": challenge,
            "signature": signature,
            "unread_only": args.unread,
        },
        timeout=15,
    )
    if not msg_resp.ok:
        print(f"❌ Failed to retrieve messages: {msg_resp.text}")
        sys.exit(1)

    data = msg_resp.json()
    messages = data.get("messages", [])
    count = data.get("count", len(messages))

    if count == 0:
        print("📭 No messages.")
        return

    print(f"📬 {count} message(s):\n")

    for i, msg in enumerate(messages, 1):
        sender = msg.get("sender_did", "unknown")
        ts = msg.get("created_at", msg.get("timestamp", "?"))
        content = msg.get("encrypted_content", msg.get("content", ""))
        encrypted = bool(msg.get("encrypted_content")) or msg.get("encrypted", False)
        msg_id = msg.get("id", "?")

        print(f"── Message {i} ──")
        print(f"  From:    {sender}")
        print(f"  Date:    {ts}")
        print(f"  ID:      {msg_id}")

        if encrypted and args.decrypt:
            try:
                import nacl.public
                import nacl.signing
                priv_bytes = base64.b64decode(creds["private_key"])
                # Ed25519 signing key → Curve25519 for decryption
                signing_key = nacl.signing.SigningKey(priv_bytes)
                curve_priv = signing_key.to_curve25519_private_key()
                sealed_box = nacl.public.SealedBox(curve_priv)
                plaintext = sealed_box.decrypt(base64.b64decode(content))
                print(f"  Content: {plaintext.decode()}")
                print(f"  🔓 (decrypted)")
            except ImportError:
                print(f"  Content: [encrypted — pip install pynacl to decrypt]")
            except Exception as e:
                print(f"  Content: [decryption failed: {e}]")
                print(f"  Raw:     {content[:80]}...")
        elif encrypted:
            print(f"  Content: [encrypted — use --decrypt to read]")
        else:
            print(f"  Content: {content}")
        print()

    # Step 4: Mark as read if requested
    if args.mark_read and messages:
        import nacl.signing as _ns
        priv_bytes = base64.b64decode(creds["private_key"])
        sk = _ns.SigningKey(priv_bytes)
        marked = 0
        for msg in messages:
            msg_id = msg.get("id")
            if msg_id:
                try:
                    sig = base64.b64encode(sk.sign(msg_id.encode()).signature).decode()
                    resp = req.patch(
                        f"{service}/message/{msg_id}/read",
                        params={"did": creds["did"], "signature": sig},
                        timeout=5,
                    )
                    if resp.ok:
                        marked += 1
                except Exception as e:
                    print(f"  ⚠️ Failed to mark {msg_id}: {e}")
        print(f"✅ Marked {marked}/{len(messages)} message(s) as read.")


def cmd_reply(args):
    """Reply to a received message by ID."""
    creds = require_credentials()
    service = args.service or creds.get("service", AIP_SERVICE)
    client = get_client(creds, service)

    import requests as req

    # Step 1: Retrieve the original message to get sender DID
    ch_resp = req.post(f"{service}/challenge", json={"did": creds["did"]}, timeout=10)
    if not ch_resp.ok:
        print(f"❌ Challenge failed: {ch_resp.text}")
        sys.exit(1)
    challenge = ch_resp.json().get("challenge")
    signature = client.sign(challenge.encode())

    msg_resp = req.post(
        f"{service}/messages",
        json={
            "did": creds["did"],
            "challenge": challenge,
            "signature": signature,
            "unread_only": False,
        },
        timeout=15,
    )
    if not msg_resp.ok:
        print(f"❌ Failed to retrieve messages: {msg_resp.text}")
        sys.exit(1)

    messages = msg_resp.json().get("messages", [])
    original = None
    for msg in messages:
        if msg.get("id") == args.message_id:
            original = msg
            break

    if not original:
        print(f"❌ Message {args.message_id} not found in your inbox.")
        sys.exit(1)

    recipient_did = original.get("sender_did")
    if not recipient_did:
        print("❌ Could not determine sender DID from original message.")
        sys.exit(1)

    # Step 2: Send the reply
    content = args.content
    reply_prefix = f"[Re: {args.message_id[:8]}] "
    full_content = reply_prefix + content

    import base64 as b64
    from datetime import datetime, timezone

    # Encrypt with recipient's public key
    lookup_resp = req.get(f"{service}/lookup/{recipient_did}", timeout=10)
    if not lookup_resp.ok:
        print(f"❌ Could not find recipient: {lookup_resp.text}")
        sys.exit(1)
    pub_key_b64 = lookup_resp.json()["public_key"]

    from nacl.signing import VerifyKey
    from nacl.public import SealedBox
    vk = VerifyKey(b64.b64decode(pub_key_b64))
    box = SealedBox(vk.to_curve25519_public_key())
    encrypted_b64 = b64.b64encode(box.encrypt(full_content.encode())).decode()

    timestamp = datetime.now(timezone.utc).isoformat()
    sig_payload = f"{client.did}|{recipient_did}|{timestamp}|{encrypted_b64}"
    signature_reply = client.sign(sig_payload.encode())

    resp = req.post(
        f"{service}/message",
        json={
            "sender_did": client.did,
            "recipient_did": recipient_did,
            "encrypted_content": encrypted_b64,
            "signature": signature_reply,
            "timestamp": timestamp,
        },
        timeout=10,
    )
    if resp.ok:
        print(f"✅ Reply sent to {recipient_did}")
        print(f"   In reply to: {args.message_id[:12]}...")
    else:
        print(f"❌ Failed: {resp.text}")
        sys.exit(1)


def cmd_rotate_key(args):
    """Rotate your signing key."""
    try:
        import nacl.signing
    except ImportError:
        print("Error: PyNaCl required. pip install pynacl")
        sys.exit(1)

    creds = require_credentials()
    client = get_client(creds, args.service)

    # Generate new keypair
    new_key = nacl.signing.SigningKey.generate()
    new_pub = base64.b64encode(bytes(new_key.verify_key)).decode()
    new_priv = base64.b64encode(bytes(new_key)).decode()

    # Sign rotation request with old key
    rotation_payload = f"rotate|{creds['did']}|{new_pub}|{int(time.time())}"
    old_sig = client.sign(rotation_payload.encode())

    import requests as req
    resp = req.post(
        f"{client.service_url}/rotate-key",
        json={
            "did": creds["did"],
            "new_public_key": new_pub,
            "old_signature": old_sig,
        },
        timeout=10,
    )
    if resp.ok:
        creds["public_key"] = new_pub
        creds["private_key"] = new_priv
        save_credentials(creds)
        print(f"✅ Key rotated for {creds['did']}")
    else:
        print(f"❌ Rotation failed: {resp.text}")
        sys.exit(1)


def cmd_badge(args):
    """Show trust badge for a DID."""
    creds = find_credentials()
    client = get_client(creds or {"did": "did:aip:anon", "public_key": "", "private_key": ""}, args.service)

    try:
        info = client.lookup(args.did)
        trust = client.get_trust(args.did)
        print(f"🪪  {args.did}")
        print(f"   Platform: {info.get('platform', '?')}/{info.get('username', '?')}")
        print(f"   Vouches: {trust.get('vouch_count', 0)}")
        print(f"   Scopes: {', '.join(trust.get('scopes', [])) or 'none'}")
        if trust.get("vouched_by"):
            print(f"   Vouched by: {', '.join(trust['vouched_by'][:5])}")
    except Exception as e:
        print(f"❌ Could not fetch badge: {e}")
        sys.exit(1)


def cmd_trust_score(args):
    """Calculate transitive trust score between two agents."""
    import requests
    service = getattr(args, "service", None) or AIP_SERVICE

    source = args.source
    target = args.target
    scope = getattr(args, "scope", None)

    params = {"source_did": source, "target_did": target}
    if scope:
        params["scope"] = scope

    try:
        resp = requests.get(f"{service}/trust-path", params=params, timeout=15)
        resp.raise_for_status()
        data = resp.json()
    except requests.RequestException as e:
        print(f"❌ Error querying trust path: {e}")
        sys.exit(1)

    if not data.get("path_exists"):
        print(f"❌ No trust path found between:")
        print(f"   Source: {source}")
        print(f"   Target: {target}")
        print(f"   Trust score: 0.0")
        return

    score = data.get("trust_score", 0.0)
    path = data.get("path", [])
    chain = data.get("trust_chain", [])
    length = data.get("path_length", 0)

    # Score bar
    bar_len = 20
    filled = int(score * bar_len)
    bar = "█" * filled + "░" * (bar_len - filled)

    print(f"🔗 Trust Path Found")
    print(f"   Score: {score:.4f} [{bar}]")
    print(f"   Hops:  {length}")
    print()

    if path:
        print("   Path:")
        for i, did in enumerate(path):
            prefix = "   → " if i > 0 else "     "
            print(f"{prefix}{did}")

    if chain:
        print()
        print("   Trust Chain:")
        for v in chain:
            voucher = v.get("voucher_did", "?")[:20]
            target_d = v.get("target_did", "?")[:20]
            vscope = v.get("scope", "GENERAL")
            print(f"     {voucher}… → {target_d}… [{vscope}]")


def cmd_trust_graph(args):
    """Visualize the AIP trust graph as ASCII art, DOT, or JSON."""
    import requests
    service = getattr(args, "service", None) or AIP_SERVICE

    # Fetch all registrations
    try:
        resp = requests.get(f"{service}/admin/registrations", params={"limit": 100}, timeout=15)
        resp.raise_for_status()
        regs = resp.json().get("registrations", [])
    except requests.RequestException as e:
        print(f"Error fetching registrations: {e}")
        sys.exit(1)

    # Build DID→name map and collect all edges
    did_name = {}
    for r in regs:
        platforms = r.get("platforms", [])
        name = platforms[0].get("username", r["did"][:20]) if platforms else r["did"][:20]
        did_name[r["did"]] = name

    edges = []  # (voucher_name, target_name, scope)
    for r in regs:
        try:
            g = requests.get(f"{service}/trust-graph", params={"did": r["did"]}, timeout=10)
            if g.ok:
                data = g.json()
                for v in data.get("vouches_for", []):
                    src = did_name.get(v["voucher_did"], v["voucher_did"][:16])
                    tgt = did_name.get(v["target_did"], v["target_did"][:16])
                    edges.append((src, tgt, v.get("scope", "GENERAL")))
        except requests.RequestException:
            continue

    all_names = set(did_name.values())
    fmt = getattr(args, "format", "ascii")

    if fmt == "json":
        out = {"nodes": sorted(all_names), "edges": [{"from": s, "to": t, "scope": sc} for s, t, sc in edges]}
        print(json.dumps(out, indent=2))
        return

    if fmt == "dot":
        print("digraph trust {")
        print('  rankdir=LR;')
        print('  node [shape=box, style=rounded];')
        for s, t, sc in edges:
            label = f' [label="{sc}"]' if sc != "GENERAL" else ""
            print(f'  "{s}" -> "{t}"{label};')
        for name in all_names:
            if not any(name == s or name == t for s, t, _ in edges):
                print(f'  "{name}";')
        print("}")
        return

    # ASCII art (default)
    if not edges and not all_names:
        print("No agents registered yet.")
        return

    print("AIP Trust Graph")
    print("=" * 50)
    print()

    # Group by voucher
    from collections import defaultdict
    voucher_targets = defaultdict(list)
    received_vouches = defaultdict(list)
    for s, t, sc in edges:
        voucher_targets[s].append((t, sc))
        received_vouches[t].append((s, sc))

    if edges:
        for voucher in sorted(voucher_targets):
            targets = voucher_targets[voucher]
            print(f"  {voucher}")
            for i, (tgt, sc) in enumerate(targets):
                prefix = "└──" if i == len(targets) - 1 else "├──"
                scope_str = f" [{sc}]" if sc != "GENERAL" else ""
                print(f"    {prefix} vouches for → {tgt}{scope_str}")
            print()

    # Show isolated nodes (no vouches given or received)
    connected = set()
    for s, t, _ in edges:
        connected.add(s)
        connected.add(t)
    isolated = all_names - connected
    if isolated:
        print("  Unconnected agents:")
        for name in sorted(isolated):
            print(f"    ○ {name}")
        print()

    print(f"  Total: {len(all_names)} agents, {len(edges)} vouches")


def cmd_vouch_vc(args):
    """Export vouches as W3C Verifiable Credentials."""
    import requests
    from .vc import vouch_to_vc, vc_to_json

    creds = require_credentials()
    service = getattr(args, "service", None) or AIP_SERVICE
    my_did = creds["did"]

    # Fetch trust graph for our DID
    try:
        resp = requests.get(f"{service}/trust-graph", params={"did": my_did}, timeout=15)
        resp.raise_for_status()
        data = resp.json()
    except requests.RequestException as e:
        print(f"Error fetching trust graph: {e}")
        sys.exit(1)

    # Get vouches we've made (or all if --received)
    vouch_key = "vouches_received" if getattr(args, "received", False) else "vouches_for"
    vouches_data = data.get(vouch_key, [])

    if not vouches_data:
        direction = "received" if getattr(args, "received", False) else "issued"
        print(f"No vouches {direction} found.")
        return

    # Filter by target DID if specified
    target = getattr(args, "target", None)
    if target:
        vouches_data = [v for v in vouches_data if v.get("target_did") == target or v.get("voucher_did") == target]
        if not vouches_data:
            print(f"No vouches found involving {target}")
            return

    # Convert each to a Vouch object and then to VC
    from .trust import Vouch, TrustLevel
    vcs = []
    for v in vouches_data:
        try:
            level_val = v.get("level", v.get("trust_level", 3))
            if isinstance(level_val, str):
                level_val = getattr(TrustLevel, level_val, TrustLevel.STRONG).value
            vouch = Vouch(
                voucher_did=v.get("voucher_did", ""),
                voucher_pubkey=v.get("voucher_pubkey", v.get("public_key", "")),
                target_did=v.get("target_did", ""),
                target_pubkey=v.get("target_pubkey", v.get("target_public_key", "")),
                scope=v.get("scope", "GENERAL"),
                level=TrustLevel(level_val) if level_val in [l.value for l in TrustLevel] else TrustLevel.STRONG,
                statement=v.get("statement", v.get("reason", "")),
                created_at=v.get("created_at", ""),
                expires_at=v.get("expires_at"),
                signature=v.get("signature", ""),
            )
            vc = vouch_to_vc(vouch)
            vcs.append(vc)
        except Exception as e:
            print(f"⚠ Skipped vouch: {e}", file=sys.stderr)

    if not vcs:
        print("No vouches could be converted to VCs.")
        return

    # Output
    output_file = getattr(args, "output", None)
    if len(vcs) == 1:
        result = vc_to_json(vcs[0])
    else:
        result = json.dumps(vcs, indent=2, ensure_ascii=False)

    if output_file:
        with open(output_file, "w") as f:
            f.write(result)
        print(f"✅ Exported {len(vcs)} vouch(es) as Verifiable Credential(s) → {output_file}")
    else:
        print(result)

    if not output_file:
        print(f"\n--- {len(vcs)} vouch(es) exported as W3C Verifiable Credentials ---", file=sys.stderr)


def cmd_list(args):
    """List all registered agents on the AIP service."""
    import requests
    service = getattr(args, "service", None) or AIP_SERVICE

    # Try public lookup first, fall back to admin endpoint
    try:
        # Try admin endpoint with auth if available
        admin_key = None
        try:
            admin_path = os.path.expanduser("~/.aip/admin_key.txt")
            if os.path.exists(admin_path):
                admin_key = open(admin_path).read().strip()
        except Exception:
            pass

        url = f"{service}/admin/registrations"
        params = {"limit": args.limit, "offset": args.offset}
        headers = {}
        if admin_key:
            headers["Authorization"] = f"Bearer {admin_key}"

        resp = requests.get(url, params=params, headers=headers, timeout=15)

        if resp.status_code == 401 or resp.status_code == 403:
            # No admin access — show network overview from public /stats
            print("📊 AIP Network Overview (public stats)\n")
            stats_resp = requests.get(f"{service}/stats", timeout=10)
            if stats_resp.ok:
                data = stats_resp.json()
                s = data.get("stats", {})
                print(f"  Registered agents: {s.get('registrations', '?')}")
                print(f"  Active vouches:    {s.get('active_vouches', '?')}")
                print(f"  Messages sent:     {s.get('messages', '?')}")
                by_platform = s.get("by_platform", {})
                if by_platform:
                    print(f"\n  By platform:")
                    for p, c in sorted(by_platform.items(), key=lambda x: -x[1]):
                        print(f"    {p}: {c}")
                print(f"\n  Use 'aip search <query>' to find specific agents.")
            else:
                print("Could not reach AIP service.")
            return

        resp.raise_for_status()
        data = resp.json()
        regs = data.get("registrations", [])
        if not regs:
            print("No registrations found.")
            return
        print(f"{'DID':<45} {'Platform':<12} {'Username':<25} {'Created'}")
        print("-" * 100)
        for r in regs:
            platforms = r.get("platforms", [])
            if platforms:
                for p in platforms:
                    print(f"{r['did']:<45} {p.get('platform','?'):<12} {p.get('username','?'):<25} {r.get('created_at','?')}")
            else:
                print(f"{r['did']:<45} {'—':<12} {'—':<25} {r.get('created_at','?')}")
        print(f"\nShowing {data['count']} of {data['limit']} (offset {data['offset']})")
    except requests.RequestException as e:
        print(f"Error: {e}")
        sys.exit(1)


def cmd_search(args):
    """Search agents by platform or username."""
    import requests
    service = getattr(args, "service", None) or AIP_SERVICE
    url = f"{service}/admin/registrations"
    params = {"limit": args.limit}
    if args.platform:
        params["platform"] = args.platform
    try:
        resp = requests.get(url, params=params, timeout=15)
        resp.raise_for_status()
        data = resp.json()
        regs = data.get("registrations", [])
        query = args.query.lower()
        # Filter by query matching platform or username
        matches = []
        for r in regs:
            platforms = r.get("platforms", [])
            for p in platforms:
                if query in p.get("platform", "").lower() or query in p.get("username", "").lower():
                    matches.append((r, p))
                    break
            else:
                if query in r.get("did", "").lower():
                    matches.append((r, {"platform": "—", "username": "—"}))
        if not matches:
            print(f"No agents matching '{args.query}' found.")
            return
        print(f"{'DID':<45} {'Platform':<12} {'Username':<25} {'Created'}")
        print("-" * 100)
        for r, p in matches:
            print(f"{r['did']:<45} {p.get('platform','?'):<12} {p.get('username','?'):<25} {r.get('created_at','?')}")
        print(f"\n{len(matches)} result(s)")
    except requests.RequestException as e:
        print(f"Error: {e}")
        sys.exit(1)


def cmd_directory(args):
    """Discover other agents on the AIP network."""
    import urllib.request
    service = getattr(args, 'service', None) or AIP_SERVICE
    limit = getattr(args, 'limit', 10)

    print("🦞 AIP Agent Directory\n")
    try:
        url = f"{service}/directory?limit={limit}"
        with urllib.request.urlopen(url, timeout=10) as resp:
            data = json.loads(resp.read().decode())
    except Exception as e:
        print(f"  ❌ Could not reach AIP service: {e}")
        return

    agents = data.get("agents", [])
    total = data.get("total", 0)
    system_did = data.get("system_identity")

    print(f"  {total} agents registered\n")

    for agent in agents:
        did = agent["did"]
        # Skip system identity in listing
        if did == system_did:
            continue

        platforms = agent.get("platforms", [])
        name = agent.get("display_name") or (
            f"{platforms[0]['username']}@{platforms[0]['platform']}" if platforms else did[:24]
        )
        vouch_count = agent.get("vouch_count", 0)
        bio = agent.get("bio", "")

        print(f"  {name}")
        print(f"    DID: {did}")
        if bio:
            print(f"    Bio: {bio[:80]}")
        print(f"    Vouches: {vouch_count}  Scopes: {', '.join(agent.get('scopes', []))}")
        print()

    if total > limit:
        print(f"  Showing {min(limit, len(agents))} of {total}. Use --limit to see more.")
    print(f"  Send a message: aip message <did> \"Hello!\"")


def cmd_whoami(args):
    """Show your current identity."""
    creds = require_credentials()
    print(f"DID: {creds['did']}")

    # Show did:key (W3C interoperable identifier)
    try:
        from .identity import public_key_to_did_key
        pub_bytes = base64.b64decode(creds.get("public_key", ""))
        if len(pub_bytes) == 32:
            print(f"DID (key): {public_key_to_did_key(pub_bytes)}")
    except Exception:
        pass  # base58 not installed or key issue — skip silently

    print(f"Platform: {creds.get('platform', '?')}/{creds.get('username', '?')}")
    pub = creds.get("public_key", "")
    print(f"Public key: {pub[:16]}..." if len(pub) > 16 else f"Public key: {pub}")

    try:
        import requests as req
        service = creds.get("service", AIP_SERVICE)
        resp = req.get(f"{service}/identity/{creds['did']}", timeout=5)
        if resp.ok:
            info = resp.json()
            print(f"Vouches: {info.get('vouches_received', 0)}")
            print(f"Trust score: {info.get('trust_score', 0):.2f}")
    except Exception:
        print("(Could not reach AIP service for trust info)")


# ── Status (dashboard) ───────────────────────────────────────────────


def cmd_status(args):
    """Show a dashboard: your identity + network health + unread messages."""
    import urllib.request

    service = args.service or AIP_SERVICE

    # 1. Service health
    print("═══ AIP Status ═══\n")
    try:
        with urllib.request.urlopen(f"{service}/health", timeout=5) as resp:
            health = json.loads(resp.read().decode())
        ver = health.get("version", "?")
        regs = health.get("metrics", {}).get("registrations", "?")
        vouches = health.get("metrics", {}).get("active_vouches", "?")
        db_ok = health.get("checks", {}).get("database", {}).get("ok", False)
        print(f"  Service: {service}")
        print(f"  Version: {ver}  |  DB: {'✅' if db_ok else '❌'}")
        print(f"  Network: {regs} agents, {vouches} active vouches")
    except Exception as e:
        print(f"  Service: ❌ unreachable ({e})")
        print()
        return

    # 2. Identity (if credentials exist)
    creds_path = Path.home() / ".aip" / "credentials.json"
    alt_path = Path("aip_credentials.json")
    creds = None
    for p in [creds_path, alt_path, Path("credentials/aip_credentials.json")]:
        if p.exists():
            with open(p) as f:
                creds = json.load(f)
            break

    if creds:
        did = creds.get("did", "?")
        plat = creds.get("platform", creds.get("platform_id", "?"))
        user = creds.get("username", creds.get("platform_username", "?"))
        print(f"\n  Identity: {plat}/{user}")
        print(f"  DID: {did}")

        # Fetch trust info
        try:
            with urllib.request.urlopen(f"{service}/identity/{did}", timeout=5) as resp:
                info = json.loads(resp.read().decode())
            score = info.get("trust_score", 0)
            v_recv = info.get("vouches_received", 0)
            print(f"  Trust: {score:.2f}  |  Vouches received: {v_recv}")
        except Exception:
            pass

        # Unread messages
        try:
            with urllib.request.urlopen(f"{service}/messages/count?did={did}", timeout=5) as resp:
                msg_data = json.loads(resp.read().decode())
            unread = msg_data.get("unread", 0)
            sent = msg_data.get("sent", 0)
            print(f"  Messages: {unread} unread, {sent} sent")
        except Exception:
            pass
    else:
        print("\n  Identity: not configured (run `aip quickstart` to get started)")

    print()


def cmd_stats(args):
    """Show public network statistics with growth data."""
    import urllib.request

    service = args.service or AIP_SERVICE

    print("═══ AIP Network Stats ═══\n")
    try:
        with urllib.request.urlopen(f"{service}/stats", timeout=10) as resp:
            data = json.loads(resp.read().decode())
    except Exception as e:
        print(f"  ❌ Could not fetch stats: {e}")
        return

    stats = data.get("stats", {})
    print(f"  Agents:      {stats.get('registrations', '?')}")
    print(f"  Vouches:     {stats.get('active_vouches', '?')}")
    print(f"  Messages:    {stats.get('messages', '?')}")
    print(f"  Signatures:  {stats.get('skill_signatures', '?')}")
    print(f"  Verified:    {stats.get('verifications_completed', '?')}")

    by_plat = stats.get("by_platform", {})
    if by_plat:
        print(f"\n  By platform:")
        for plat, count in by_plat.items():
            print(f"    {plat}: {count}")

    growth = stats.get("growth", {})
    daily = growth.get("daily_registrations", [])
    if daily:
        print(f"\n  Registration growth (last 30 days):")
        for entry in daily:
            bar = "█" * entry["count"]
            print(f"    {entry['date']}: {bar} {entry['count']}")

    print()


# ── Webhook ──────────────────────────────────────────────────────────

def cmd_webhook(args):
    """Manage webhook subscriptions."""
    import urllib.request

    creds = require_credentials()
    service = args.service or creds.get("service", AIP_SERVICE)
    sub = args.webhook_action

    if sub == "list":
        url = f"{service}/webhooks/{creds['did']}"
        try:
            with urllib.request.urlopen(url, timeout=10) as resp:
                data = json.loads(resp.read())
        except Exception as e:
            print(f"❌ Failed to list webhooks: {e}")
            return
        hooks = data.get("webhooks", [])
        if not hooks:
            print("No webhooks registered.")
            return
        for h in hooks:
            status = "✅ active" if h["active"] else "❌ inactive"
            events = ", ".join(h["events"])
            print(f"  {h['id']}  {status}  events=[{events}]  failures={h['failure_count']}")
            print(f"    → {h['url']}")

    elif sub == "add":
        import nacl.signing
        priv = decode_key(creds["private_key"])
        signing_key = nacl.signing.SigningKey(priv)
        url = args.url
        msg = f"webhook:{url}"
        sig = base64.b64encode(signing_key.sign(msg.encode()).signature).decode()
        events = args.events.split(",") if args.events else ["registration"]
        payload = json.dumps({
            "owner_did": creds["did"],
            "url": url,
            "events": events,
            "signature": sig,
        }).encode()
        req = urllib.request.Request(
            f"{service}/webhooks",
            data=payload,
            headers={"Content-Type": "application/json"},
            method="POST",
        )
        try:
            with urllib.request.urlopen(req, timeout=10) as resp:
                data = json.loads(resp.read())
            print(f"✅ Webhook created: {data['id']}")
            print(f"   URL: {url}")
            print(f"   Events: {', '.join(events)}")
        except urllib.error.HTTPError as e:
            body = e.read().decode()
            print(f"❌ Failed ({e.code}): {body}")
        except Exception as e:
            print(f"❌ Failed: {e}")

    elif sub == "delete":
        import nacl.signing
        priv = decode_key(creds["private_key"])
        signing_key = nacl.signing.SigningKey(priv)
        wh_id = args.webhook_id
        msg = f"delete-webhook:{wh_id}"
        sig = base64.b64encode(signing_key.sign(msg.encode()).signature).decode()
        payload = json.dumps({
            "owner_did": creds["did"],
            "signature": sig,
        }).encode()
        req = urllib.request.Request(
            f"{service}/webhooks/{wh_id}",
            data=payload,
            headers={"Content-Type": "application/json"},
            method="DELETE",
        )
        try:
            with urllib.request.urlopen(req, timeout=10) as resp:
                print(f"✅ Webhook {wh_id} deleted.")
        except urllib.error.HTTPError as e:
            body = e.read().decode()
            print(f"❌ Failed ({e.code}): {body}")
        except Exception as e:
            print(f"❌ Failed: {e}")

    else:
        print("Usage: aip webhook {list|add|delete}")
        print("  list                    List your webhooks")
        print("  add <url> [--events ..]  Register a webhook")
        print("  delete <id>             Delete a webhook")


# ── Audit ─────────────────────────────────────────────────────────────

def cmd_audit(args):
    """Comprehensive self-audit: trust, vouches, messages, profile completeness."""
    import urllib.request

    service = args.service or AIP_SERVICE
    creds = find_credentials()
    if not creds:
        print("No AIP credentials found. Run: aip quickstart")
        return

    did = creds["did"]
    plat = creds.get("platform", creds.get("platform_id", "?"))
    user = creds.get("username", creds.get("platform_username", "?"))

    print("═══ AIP Self-Audit ═══\n")
    print(f"  DID:      {did}")
    print(f"  Platform: {plat}/{user}")

    issues = []
    score = 0
    max_score = 0

    # 1. Identity registration
    max_score += 1
    try:
        with urllib.request.urlopen(f"{service}/identity/{did}", timeout=5) as resp:
            identity = json.loads(resp.read().decode())
        trust = identity.get("trust_score", 0)
        verified = identity.get("verified", False)
        v_recv = identity.get("vouches_received", 0)
        v_given = identity.get("vouches_given", 0)
        score += 1
        print(f"\n  ✅ Registered on network")
        print(f"     Trust score: {trust:.2f}")
        print(f"     Verified:    {'✅ yes' if verified else '❌ no'}")
        print(f"     Vouches:     {v_recv} received, {v_given} given")
        if not verified:
            issues.append("Not verified — complete platform verification")
    except Exception as e:
        print(f"\n  ❌ Could not fetch identity: {e}")
        issues.append("Identity not found on network")

    # 2. Trust score
    max_score += 1
    if trust >= 0.5:
        score += 1
        print(f"  ✅ Trust score healthy ({trust:.2f})")
    elif trust > 0:
        score += 0.5
        print(f"  ⚠️  Trust score low ({trust:.2f}) — get more vouches")
        issues.append("Low trust score — seek vouches from established agents")
    else:
        print(f"  ❌ No trust score — you need vouches")
        issues.append("Zero trust — register and get vouched")

    # 3. Vouches received
    max_score += 1
    if v_recv >= 3:
        score += 1
        print(f"  ✅ Good vouch coverage ({v_recv} vouches)")
    elif v_recv >= 1:
        score += 0.5
        print(f"  ⚠️  Only {v_recv} vouch(es) — aim for 3+")
        issues.append(f"Only {v_recv} vouch(es) received — seek more")
    else:
        print(f"  ❌ No vouches received")
        issues.append("No vouches — ask trusted agents to vouch for you")

    # 4. Vouches given (contributing to network)
    max_score += 1
    if v_given >= 1:
        score += 1
        print(f"  ✅ Contributing vouches ({v_given} given)")
    else:
        print(f"  ⚠️  No vouches given — help grow the trust network")
        issues.append("No vouches given — vouch for agents you trust")

    # 5. Messages
    max_score += 1
    try:
        with urllib.request.urlopen(f"{service}/messages/count?did={did}", timeout=5) as resp:
            msg_data = json.loads(resp.read().decode())
        unread = msg_data.get("unread", 0)
        sent = msg_data.get("sent", 0)
        score += 1
        print(f"  ✅ Messaging active ({unread} unread, {sent} sent)")
        if unread > 0:
            issues.append(f"{unread} unread message(s) — check with `aip messages`")
    except Exception:
        print(f"  ⚠️  Could not check messages")

    # 6. Profile completeness
    max_score += 1
    try:
        with urllib.request.urlopen(f"{service}/agent/{did}/profile", timeout=5) as resp:
            profile = json.loads(resp.read().decode())
        filled = sum(1 for k in ["display_name", "bio", "website", "avatar_url"] if profile.get(k))
        tags = profile.get("tags", [])
        if tags:
            filled += 1
        total_fields = 5
        if filled >= 4:
            score += 1
            print(f"  ✅ Profile well-filled ({filled}/{total_fields} fields)")
        elif filled >= 2:
            score += 0.5
            print(f"  ⚠️  Profile partially filled ({filled}/{total_fields})")
            missing = [k for k in ["display_name", "bio", "website", "avatar_url"] if not profile.get(k)]
            if not tags:
                missing.append("tags")
            issues.append(f"Profile incomplete — missing: {', '.join(missing)}")
        else:
            print(f"  ❌ Profile mostly empty ({filled}/{total_fields})")
            issues.append("Profile nearly empty — run `aip profile set`")
    except Exception:
        print(f"  ⚠️  Could not fetch profile")
        issues.append("Set up your profile with `aip profile set`")

    # Summary
    pct = int((score / max_score) * 100) if max_score > 0 else 0
    bar_len = 20
    filled_bar = int(bar_len * score / max_score)
    bar = "█" * filled_bar + "░" * (bar_len - filled_bar)
    print(f"\n  Score: [{bar}] {pct}% ({score:.1f}/{max_score})")

    if issues:
        print(f"\n  📋 Recommendations ({len(issues)}):")
        for i, issue in enumerate(issues, 1):
            print(f"     {i}. {issue}")
    else:
        print(f"\n  🎉 Perfect score! Your identity is fully set up.")

    print()


# ── Changelog ────────────────────────────────────────────────────────

def cmd_changelog(args):
    """Show recent AIP changes and version history."""
    import urllib.request

    url = "https://raw.githubusercontent.com/The-Nexus-Guard/aip/main/CHANGELOG.md"
    try:
        with urllib.request.urlopen(url, timeout=10) as resp:
            text = resp.read().decode()
    except Exception as e:
        print(f"❌ Could not fetch changelog: {e}")
        return

    lines = text.split("\n")
    n = args.entries
    count = 0
    output = []
    for line in lines:
        if line.startswith("## ") and count > 0:
            count += 1
            if count > n:
                break
        elif line.startswith("## "):
            count = 1
        if count >= 1:
            output.append(line)

    print("\n".join(output) if output else text[:2000])


# ── Export / Import ──────────────────────────────────────────────────

def cmd_export(args):
    """Export your identity as portable JSON."""
    creds = require_credentials()
    export_data = {
        "aip_version": "1.0",
        "did": creds["did"],
        "public_key": creds.get("public_key", ""),
        "platform": creds.get("platform_id", creds.get("platform", "")),
        "username": creds.get("username", creds.get("platform_username", "")),
        "exported_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
    }
    if args.include_private:
        if "private_key" in creds:
            export_data["private_key"] = creds["private_key"]
            print("⚠️  WARNING: Private key included. Keep this file secret!", file=sys.stderr)
        else:
            print("No private key found in credentials.", file=sys.stderr)

    output = json.dumps(export_data, indent=2)
    if args.output:
        out_path = Path(args.output)
        out_path.parent.mkdir(parents=True, exist_ok=True)
        with open(out_path, "w") as f:
            f.write(output + "\n")
        if args.include_private:
            os.chmod(out_path, 0o600)
        print(f"✅ Identity exported to {out_path}", file=sys.stderr)
    else:
        print(output)


def cmd_import(args):
    """Import another agent's public key for offline verification."""
    import urllib.request

    keyring_dir = Path(args.keyring_dir) if args.keyring_dir else Path.home() / ".aip" / "keyring"
    keyring_dir.mkdir(parents=True, exist_ok=True)

    source = args.source

    # If it looks like a DID, fetch from service
    if source.startswith("did:aip:"):
        url = f"{AIP_SERVICE}/lookup/{source}"
        try:
            with urllib.request.urlopen(url, timeout=10) as resp:
                data = json.loads(resp.read().decode())
        except Exception as e:
            print(f"❌ Failed to fetch DID from service: {e}", file=sys.stderr)
            sys.exit(1)
        agent_data = {
            "did": data.get("did", source),
            "public_key": data.get("public_key", ""),
            "fetched_from": AIP_SERVICE,
            "imported_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
        }
    elif Path(source).exists():
        # Read from JSON file
        with open(source) as f:
            agent_data = json.load(f)
        if "did" not in agent_data or "public_key" not in agent_data:
            print("❌ Invalid identity file: must contain 'did' and 'public_key'", file=sys.stderr)
            sys.exit(1)
        agent_data["imported_at"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
    else:
        print(f"❌ Source not found: {source} (expected a file path or did:aip:... DID)", file=sys.stderr)
        sys.exit(1)

    # Save to keyring
    did_slug = agent_data["did"].replace(":", "_")
    out_path = keyring_dir / f"{did_slug}.json"
    with open(out_path, "w") as f:
        json.dump(agent_data, f, indent=2)

    name = agent_data.get("username", agent_data["did"])
    print(f"✅ Imported {name} ({agent_data['did']})")
    print(f"   Saved to {out_path}")


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

def cmd_profile(args):
    """View or update agent profiles."""
    action = getattr(args, "profile_action", None)

    if action == "show":
        did = args.did
        if not did:
            creds = find_credentials()
            if not creds:
                print("No DID specified and no credentials found. Usage: aip profile show <did>")
                return
            did = creds["did"]

        try:
            import requests
            resp = requests.get(f"{AIP_SERVICE}/agent/{did}/profile")
            resp.raise_for_status()
            profile = resp.json()
            print(f"\n  Agent Profile: {did[:20]}...")
            print(f"  {'─' * 40}")
            print(f"  Name:    {profile.get('display_name') or '(not set)'}")
            print(f"  Bio:     {profile.get('bio') or '(not set)'}")
            print(f"  Avatar:  {profile.get('avatar_url') or '(not set)'}")
            print(f"  Website: {profile.get('website') or '(not set)'}")
            tags = profile.get("tags", [])
            print(f"  Tags:    {', '.join(tags) if tags else '(none)'}")
            if profile.get("updated_at"):
                print(f"  Updated: {profile['updated_at']}")
            print()
        except Exception as e:
            print(f"Error: {e}")

    elif action == "set":
        creds = require_credentials()
        client = get_client(creds)

        fields = {}
        if args.display_name:
            fields["display_name"] = args.display_name
        if args.bio:
            fields["bio"] = args.bio
        if args.avatar_url:
            fields["avatar_url"] = args.avatar_url
        if args.website:
            fields["website"] = args.website
        if args.tags:
            fields["tags"] = [t.strip() for t in args.tags.split(",")]

        if not fields:
            print("No fields specified. Use --name, --bio, --avatar, --website, or --tags")
            return

        try:
            result = client.update_profile(**fields)
            print("✅ Profile updated!")
            profile = result.get("profile", {})
            print(f"  Name:    {profile.get('display_name') or '(not set)'}")
            print(f"  Bio:     {profile.get('bio') or '(not set)'}")
            tags = profile.get("tags", [])
            print(f"  Tags:    {', '.join(tags) if tags else '(none)'}")
        except Exception as e:
            print(f"Error updating profile: {e}")
    else:
        print("Usage: aip profile show [did] | aip profile set --name '...' --bio '...'")


def cmd_quickstart(args):
    """Get your agent identity in 30 seconds."""
    existing = find_credentials()
    if existing:
        print(f"✅ You're already registered!")
        print(f"   DID: {existing['did']}")
        print(f"   Platform: {existing.get('platform', '?')}/{existing.get('username', '?')}")
        _print_quickstart_next_steps()
        return

    try:
        import nacl.signing
    except ImportError:
        print("Error: PyNaCl required. pip install pynacl")
        sys.exit(1)

    service_url = args.service or AIP_SERVICE
    platform = getattr(args, 'platform', None) or 'cli'
    username = getattr(args, 'username', None) or f"agent-{hashlib.sha256(os.urandom(8)).hexdigest()[:8]}"

    print("🦞 AIP Quickstart — Get your agent identity in 30 seconds\n")

    # Step 1: Create identity
    print("Step 1: Creating your cryptographic identity...")
    signing_key = nacl.signing.SigningKey.generate()
    pub_bytes = bytes(signing_key.verify_key)
    priv_bytes = bytes(signing_key)
    pub_b64 = base64.b64encode(pub_bytes).decode()
    priv_b64 = base64.b64encode(priv_bytes).decode()
    did = "did:aip:" + hashlib.sha256(pub_bytes).hexdigest()[:32]
    print("  ✓ Generated Ed25519 keypair")
    print(f"  ✓ Your DID: {did}")

    # Step 2: Register
    print("\nStep 2: Registering with AIP service...")
    creds = {
        "did": did,
        "public_key": pub_b64,
        "private_key": priv_b64,
        "platform": platform,
        "username": username,
        "service": service_url,
        "registered_at": int(time.time()),
    }
    try:
        import requests
        resp = requests.post(
            f"{service_url}/register",
            json={"did": did, "public_key": pub_b64, "platform": platform, "username": username},
            timeout=10,
        )
        if resp.ok:
            data = resp.json()
            if "did" in data:
                did = data["did"]
                creds["did"] = did
            print(f'  ✓ Registered as "{username}" on platform "{platform}"')
        else:
            print(f"  ⚠️  Registration failed ({resp.status_code}), but your keys are saved locally")
    except Exception as e:
        print(f"  ⚠️  Could not reach service ({e}), but your keys are saved locally")

    save_credentials(creds)
    print(f"  ✓ Credentials saved to {CREDENTIALS_PATHS[0]}")

    # Step 3: Verify
    print("\nStep 3: Verifying your identity...")
    try:
        import requests
        resp = requests.get(f"{service_url}/trust/{did}", timeout=5)
        if resp.ok:
            print("  ✓ Identity verified on AIP service")
        else:
            print("  ⚠️  Could not verify (service may be slow)")
    except Exception:
        print("  ⚠️  Could not verify (offline)")

    # Step 4: Trust score (should now show 1 vouch from welcome)
    print("\nStep 4: Checking your trust score...")
    try:
        import requests
        resp = requests.get(f"{service_url}/trust/{did}", timeout=5)
        if resp.ok:
            data = resp.json()
            vouch_count = data.get("vouch_count", 0)
            if vouch_count > 0:
                print(f"  ✓ Welcome vouch received! You have {vouch_count} vouch(es)")
            else:
                print(f"  ✓ Trust score: 0.0 (new agent — get vouched to increase!)")
    except Exception:
        print("  ✓ Checking trust score... (service may be slow)")

    print("\n🎉 You're set up! Your agent now has a cryptographic identity.")
    _print_quickstart_next_steps()


def _print_quickstart_next_steps():
    """Print the 'what's next' section for quickstart."""
    print("\nWhat's next:")
    print("  aip whoami          — see your identity")
    print("  aip directory       — discover other agents")
    print("  aip message <did>   — send an encrypted message")
    print("  aip sign <file>     — sign an artifact")
    print("  aip vouch <did>     — vouch for another agent")
    print(f"\n  Full docs: https://the-nexus-guard.github.io/aip/")


def cmd_init(args):
    """One-command setup: register + set profile."""
    # Check if already registered
    existing = find_credentials()
    if existing and not args.force:
        print(f"⚠️  Already registered as {existing['did']}")
        print(f"   Use --force to re-register with a new identity.")
        print(f"   Or use 'aip profile set' to update your profile.")
        return

    try:
        import nacl.signing
        import requests
    except ImportError:
        print("Error: Required packages missing. pip install pynacl requests")
        sys.exit(1)

    service_url = args.service or AIP_SERVICE

    # Step 1: Register (always secure — key never leaves machine)
    print("\n🚀 AIP Quick Setup")
    print("=" * 40)

    platform = args.platform
    username = args.username

    if not platform:
        platform = input("\n  Platform (e.g. github, moltbook): ").strip()
        if not platform:
            print("❌ Platform is required.")
            sys.exit(1)
    if not username:
        username = input(f"  Username on {platform}: ").strip()
        if not username:
            print("❌ Username is required.")
            sys.exit(1)

    print(f"\n📝 Registering on {platform} as {username}...")

    signing_key = nacl.signing.SigningKey.generate()
    pub_bytes = bytes(signing_key.verify_key)
    priv_bytes = bytes(signing_key)

    pub_b64 = base64.b64encode(pub_bytes).decode()
    priv_b64 = base64.b64encode(priv_bytes).decode()
    did = "did:aip:" + hashlib.sha256(pub_bytes).hexdigest()[:32]

    resp = requests.post(
        f"{service_url}/register",
        json={
            "did": did,
            "public_key": pub_b64,
            "platform": platform,
            "username": username,
        },
        timeout=10,
    )
    if not resp.ok:
        print(f"❌ Registration failed: {resp.text}")
        sys.exit(1)

    data = resp.json()
    if "did" in data:
        did = data["did"]

    creds = {
        "did": did,
        "public_key": pub_b64,
        "private_key": priv_b64,
        "platform": platform,
        "username": username,
        "service": service_url,
        "registered_at": int(time.time()),
    }
    save_credentials(creds)
    print(f"   ✅ DID: {did}")

    # Step 2: Set profile if provided
    profile_fields = {}
    if args.name:
        profile_fields["display_name"] = args.name
    if args.bio:
        profile_fields["bio"] = args.bio
    if args.tags:
        profile_fields["tags"] = [t.strip() for t in args.tags.split(",")]

    if profile_fields:
        print(f"\n👤 Setting profile...")
        try:
            client = get_client(creds)
            result = client.update_profile(**profile_fields)
            profile = result.get("profile", {})
            if profile.get("display_name"):
                print(f"   Name: {profile['display_name']}")
            if profile.get("bio"):
                print(f"   Bio:  {profile['bio']}")
            tags = profile.get("tags", [])
            if tags:
                print(f"   Tags: {', '.join(tags)}")
            print(f"   ✅ Profile set!")
        except Exception as e:
            print(f"   ⚠️  Profile update failed: {e}")
            print(f"   You can set it later with: aip profile set --name '...'")

    # Check for welcome vouch
    welcome_vouch = False
    try:
        trust_resp = requests.get(f"{service_url}/trust/{did}", timeout=5)
        if trust_resp.ok:
            trust_data = trust_resp.json()
            score = trust_data.get("trust_score", 0)
            vouches = trust_data.get("vouches_received", 0)
            if score > 0 or vouches > 0:
                welcome_vouch = True
    except Exception:
        pass

    # Summary
    print(f"\n{'=' * 40}")
    print(f"🎉 You're on AIP!")
    print(f"   DID:      {did}")
    print(f"   Platform: {platform}/{username}")

    if welcome_vouch:
        print(f"\n   🤝 Welcome vouch received! Trust score: {score:.2f}")
        print(f"      You already have {vouches} vouch(es) from the network.")

    # Show a peek at the directory
    try:
        dir_resp = requests.get(f"{service_url}/directory?limit=5", timeout=5)
        if dir_resp.ok:
            dir_data = dir_resp.json()
            agents = dir_data.get("agents", [])
            total = dir_data.get("total", 0)
            system_did = dir_data.get("system_identity")
            others = [a for a in agents if a["did"] != did and a["did"] != system_did]
            if others:
                print(f"\n   📡 {total} agents on the network. Here are some:")
                for a in others[:3]:
                    aname = a.get("display_name") or (
                        a["platforms"][0]["username"] if a.get("platforms") else a["did"][:20]
                    )
                    abio = a.get("bio", "")
                    bio_str = f" — {abio[:50]}" if abio else ""
                    print(f"      • {aname}{bio_str}")
                    print(f"        {a['did']}")
    except Exception:
        pass

    print(f"\n   Next steps:")
    print(f"   • aip directory       — discover agents to connect with")
    print(f"   • aip message <did>   — send an encrypted message")
    print(f"   • aip sign <dir>      — sign a skill or artifact")
    print(f"   • aip status          — your full dashboard")
    print()


def _demo_interactive(args):
    """Interactive demo: create identities, sign, encrypt — all local."""
    import time
    from .identity import AgentIdentity

    print("╔══════════════════════════════════════════════════════════╗")
    print("║  🔐 AIP in 60 Seconds — Live Cryptographic Demo        ║")
    print("║  Everything below runs locally. No server needed.       ║")
    print("╚══════════════════════════════════════════════════════════╝")
    print()

    # Step 1: Create two agent identities
    print("━━━ Step 1: Create Agent Identities ━━━")
    print("  Generating Ed25519 keypairs...")
    time.sleep(0.3)
    alice = AgentIdentity.create("Alice", {"role": "code-reviewer"})
    bob = AgentIdentity.create("Bob", {"role": "developer"})
    print(f"  🤖 Alice: {alice.did}")
    print(f"     Public key: {alice.public_key[:32]}...")
    print(f"  🤖 Bob:   {bob.did}")
    print(f"     Public key: {bob.public_key[:32]}...")
    print(f"  ✅ Two unique identities created from keypairs")
    print()

    # Step 2: Sign and verify
    print("━━━ Step 2: Digital Signatures ━━━")
    message = "I reviewed commit abc123 and approve it."
    print(f'  Alice signs: "{message}"')
    time.sleep(0.2)
    signature = alice.sign(message.encode())
    sig_preview = signature[:40] + "..." if len(signature) > 40 else signature
    print(f"  ✍️  Signature: {sig_preview}")
    time.sleep(0.2)

    # Verify with correct key
    valid = AgentIdentity.verify(alice.public_key, message.encode(), signature)
    print(f"  🔍 Verify with Alice's key: {'✅ VALID' if valid else '❌ INVALID'}")

    # Verify with wrong key (should fail)
    wrong = AgentIdentity.verify(bob.public_key, message.encode(), signature)
    print(f"  🔍 Verify with Bob's key:   {'❌ INVALID (expected)' if not wrong else '⚠️ unexpected'}")
    print(f"  ✅ Only the signer's identity can produce a valid signature")
    print()

    # Step 3: Encrypted messaging
    print("━━━ Step 3: End-to-End Encrypted Messaging ━━━")
    try:
        from nacl.public import SealedBox
        from nacl.signing import VerifyKey
        from nacl.encoding import Base64Encoder

        secret_msg = "API key rotation: new key is sk-abc123xyz"
        print(f'  Alice encrypts for Bob: "{secret_msg}"')
        time.sleep(0.2)

        # Encrypt for Bob using his public key
        bob_vk = VerifyKey(base64.b64decode(bob.public_key))
        box = SealedBox(bob_vk.to_curve25519_public_key())
        encrypted = box.encrypt(secret_msg.encode())
        enc_b64 = base64.b64encode(encrypted).decode()
        print(f"  🔒 Encrypted: {enc_b64[:48]}...")
        print(f"     ({len(encrypted)} bytes — original was {len(secret_msg)} bytes)")
        time.sleep(0.2)

        # Bob decrypts
        from nacl.signing import SigningKey
        bob_sk = bob._key  # SigningKey
        bob_curve_priv = bob_sk.to_curve25519_private_key()
        bob_box = SealedBox(bob_curve_priv)
        decrypted = bob_box.decrypt(encrypted).decode()
        print(f'  🔓 Bob decrypts: "{decrypted}"')
        print(f"  ✅ Only Bob can read it — not even Alice can decrypt")
    except ImportError:
        print("  ⚠️  Encryption demo requires PyNaCl: pip install pynacl")
        print("  (Signatures above work with pure Python — no dependencies needed)")
    print()

    # Step 4: Trust vouching
    print("━━━ Step 4: Trust & Vouching ━━━")
    vouch_statement = f"vouch:{alice.did}:{bob.did}:CODE_REVIEW"
    print(f"  Alice vouches for Bob's CODE_REVIEW ability")
    time.sleep(0.2)
    vouch_sig = alice.sign(vouch_statement.encode())
    vouch_preview = vouch_sig[:40] + "..." if len(vouch_sig) > 40 else vouch_sig
    print(f"  ✍️  Vouch signature: {vouch_preview}")
    print(f"  🔗 Vouch chain: Alice → Bob (CODE_REVIEW)")
    print(f"  ✅ Trust is cryptographic — can't be faked or transferred")
    print()

    # Summary
    print("━━━ What Just Happened ━━━")
    print("  In under 60 seconds, you saw:")
    print("  • 2 unique agent identities created (Ed25519 keypairs → DIDs)")
    print("  • A message signed and verified (tamper-proof provenance)")
    print("  • An encrypted message only the recipient could read (SealedBox)")
    print("  • A cryptographic trust vouch (verifiable reputation)")
    print()
    print("  All of this ran locally — no server, no accounts, no API keys.")
    print()

    # CTA
    print("━━━ Ready to Join the Network? ━━━")
    print()
    print("  Connect your identity to the live AIP network:")
    print("    aip init <platform> <username>")
    print()
    print("  Then other agents can find you, verify you, and message you.")
    print()
    print("  📖 Docs:     https://the-nexus-guard.github.io/aip/")
    print("  🌐 Explorer: https://the-nexus-guard.github.io/aip/explorer.html")
    print("  📦 PyPI:     https://pypi.org/project/aip-identity/")
    print()


def _demo_network(args):
    """Network overview demo: show live stats from the AIP service."""
    import urllib.request

    service = getattr(args, "service", None) or AIP_SERVICE

    print("╔══════════════════════════════════════════════════════╗")
    print("║     🔐 AIP — Agent Identity Protocol Demo          ║")
    print("║     Live network overview                           ║")
    print("╚══════════════════════════════════════════════════════╝")
    print()

    # Step 1: Network stats
    print("━━━ Network Overview ━━━")
    print(f"Querying {service}/stats ...")
    try:
        req = urllib.request.Request(f"{service}/stats")
        with urllib.request.urlopen(req, timeout=10) as resp:
            data = json.loads(resp.read())
        stats = data.get("stats", data)
        print(f"  📊 Agents registered:  {stats.get('registrations', stats.get('total_agents', '?'))}")
        print(f"  🤝 Active vouches:     {stats.get('active_vouches', '?')}")
        print(f"  📬 Messages exchanged: {stats.get('messages', stats.get('total_messages', '?'))}")
        print(f"  ✍️  Skills signed:      {stats.get('skill_signatures', '?')}")
    except Exception as e:
        print(f"  ⚠️  Could not reach service: {e}")
    print()

    # Agent directory
    print("━━━ Agent Directory ━━━")
    print("Fetching network info...")
    try:
        req = urllib.request.Request(f"{service}/stats")
        with urllib.request.urlopen(req, timeout=10) as resp:
            data = json.loads(resp.read())
        by_platform = data.get("stats", {}).get("by_platform", {})
        if by_platform:
            print("  Agents by platform:")
            for p, c in sorted(by_platform.items(), key=lambda x: -x[1]):
                print(f"    {p}: {c} agent{'s' if c != 1 else ''}")
        print(f"\n  🔗 Explore: {service}/docs")
        print(f"  🌐 Explorer: https://the-nexus-guard.github.io/aip/explorer.html")
    except Exception as e:
        print(f"  ⚠️  Could not fetch network info: {e}")
    print()

    # Trust verification
    print("━━━ Trust Verification ━━━")
    sample_did = "did:aip:c1965a89866ecbfaad49803e6ced70fb"
    print(f"Checking trust for {sample_did}...")
    try:
        req = urllib.request.Request(f"{service}/trust/{sample_did}")
        with urllib.request.urlopen(req, timeout=10) as resp:
            result = json.loads(resp.read())
        registered = result.get("registered", False)
        status = "✅ Registered" if registered else "❌ Not found"
        vouches = result.get("vouch_count", len(result.get("vouched_by", [])))
        scopes = result.get("scopes", [])
        print(f"  Status: {status}")
        print(f"  Vouches received: {vouches}")
        if scopes:
            print(f"  Trust scopes: {', '.join(scopes)}")
    except Exception as e:
        print(f"  ⚠️  Could not verify: {e}")
    print()

    # Next steps
    print("━━━ Try the Interactive Demo ━━━")
    print()
    print("  See AIP in action (no registration needed):")
    print("    aip demo --interactive")
    print()
    print("  Ready to join?")
    print("    aip init <platform> <username>")
    print()
    print("  📖 Docs: https://the-nexus-guard.github.io/aip/")
    print("  📦 PyPI: https://pypi.org/project/aip-identity/")
    print()


def cmd_demo(args):
    """Demo command: default is interactive crypto demo, --network for live stats."""
    network = getattr(args, "network", False)
    if network:
        _demo_network(args)
    else:
        _demo_interactive(args)


def cmd_migrate(args):
    """Migrate credentials between locations or upgrade format."""
    print("═══ AIP Credential Migration ═══\n")

    # Find all credential files
    found = []
    for p in CREDENTIALS_PATHS:
        if p.exists():
            try:
                with open(p) as f:
                    data = json.load(f)
                found.append((p, data))
                print(f"  📄 Found: {p}")
                print(f"     DID: {data.get('did', '?')}")
                has_pk = "private_key" in data
                print(f"     Private key: {'✅ yes' if has_pk else '❌ no'}")
            except (json.JSONDecodeError, KeyError) as e:
                print(f"  ⚠️  Found but invalid: {p} ({e})")

    if not found:
        print("  ❌ No credentials found anywhere.")
        print(f"     Expected locations: {', '.join(str(p) for p in CREDENTIALS_PATHS)}")
        print("     Run: aip init")
        return

    # Determine the canonical (best) credentials
    best = None
    for p, data in found:
        if "did" in data and "private_key" in data:
            if best is None:
                best = (p, data)
            elif "public_key" in data and "public_key" not in best[1]:
                best = (p, data)

    if best is None:
        print("\n  ❌ No complete credentials (need did + private_key).")
        return

    best_path, best_data = best

    # Normalize field names (old format compatibility)
    normalized = dict(best_data)
    if "platform_id" in normalized and "platform" not in normalized:
        normalized["platform"] = normalized.pop("platform_id")
    if "platform_username" in normalized and "username" not in normalized:
        normalized["username"] = normalized.pop("platform_username")
    # Ensure public_key exists
    if "public_key" not in normalized and "private_key" in normalized:
        try:
            import nacl.signing
            pk_bytes = base64.b64decode(normalized["private_key"])
            signing_key = nacl.signing.SigningKey(pk_bytes)
            normalized["public_key"] = base64.b64encode(
                bytes(signing_key.verify_key)
            ).decode()
            print("\n  🔑 Derived public key from private key.")
        except Exception as e:
            print(f"\n  ⚠️  Could not derive public key: {e}")

    target = args.target
    if target:
        target_path = Path(target)
    else:
        target_path = CREDENTIALS_PATHS[0]  # default: ~/.aip/credentials.json

    if args.dry_run:
        print(f"\n  🔍 DRY RUN — would write to: {target_path}")
        print(f"     Fields: {', '.join(normalized.keys())}")
        return

    # Write normalized credentials to target
    target_path.parent.mkdir(parents=True, exist_ok=True)
    with open(target_path, "w") as f:
        json.dump(normalized, f, indent=2)
    os.chmod(target_path, 0o600)
    print(f"\n  ✅ Migrated to {target_path}")
    print(f"     DID: {normalized['did']}")
    print(f"     Fields: {', '.join(normalized.keys())}")

    # Clean up old locations if different from target
    if args.cleanup:
        for p, _ in found:
            if p != target_path and p.exists():
                p.unlink()
                print(f"  🗑️  Removed old: {p}")


def cmd_cache(args):
    """Cache agent directory locally for offline verification."""
    import urllib.request

    cache_dir = Path(args.cache_dir) if args.cache_dir else Path.home() / ".aip" / "cache"
    cache_dir.mkdir(parents=True, exist_ok=True)

    service = args.service or AIP_SERVICE

    if args.cache_action == "sync":
        print("═══ AIP Offline Cache Sync ═══\n")
        print(f"  Service: {service}")
        print(f"  Cache:   {cache_dir}\n")

        # Fetch all agents
        try:
            url = f"{service}/admin/registrations?limit=1000"
            with urllib.request.urlopen(url, timeout=15) as resp:
                data = json.loads(resp.read().decode())
        except Exception as e:
            print(f"  ❌ Failed to fetch directory: {e}")
            sys.exit(1)

        agents = data.get("registrations", data if isinstance(data, list) else [])
        print(f"  📡 Fetched {len(agents)} agents from service")

        # Save each agent's public key + metadata
        for agent in agents:
            did = agent.get("did", "")
            if not did:
                continue
            agent_file = cache_dir / f"{did.replace(':', '_')}.json"
            cache_entry = {
                "did": did,
                "public_key": agent.get("public_key", ""),
                "platform": agent.get("platform", ""),
                "username": agent.get("username", ""),
                "cached_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
            }
            with open(agent_file, "w") as f:
                json.dump(cache_entry, f, indent=2)

        # Save directory index
        index = {
            "synced_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
            "agent_count": len(agents),
            "service": service,
        }
        with open(cache_dir / "index.json", "w") as f:
            json.dump(index, f, indent=2)

        print(f"  ✅ Cached {len(agents)} agents to {cache_dir}")
        print(f"     Use `aip cache lookup <DID>` for offline verification")

    elif args.cache_action == "lookup":
        did = args.did
        if not did:
            print("  ❌ Provide a DID: aip cache lookup <DID>")
            sys.exit(1)
        agent_file = cache_dir / f"{did.replace(':', '_')}.json"
        if not agent_file.exists():
            print(f"  ❌ Not in cache: {did}")
            print("     Run `aip cache sync` to update cache")
            sys.exit(1)
        with open(agent_file) as f:
            data = json.load(f)
        print(f"  DID:        {data['did']}")
        print(f"  Public Key: {data.get('public_key', '?')[:20]}...")
        print(f"  Platform:   {data.get('platform', '?')}")
        print(f"  Username:   {data.get('username', '?')}")
        print(f"  Cached:     {data.get('cached_at', '?')}")

    elif args.cache_action == "status":
        index_file = cache_dir / "index.json"
        if not index_file.exists():
            print("  No cache found. Run `aip cache sync`")
            return
        with open(index_file) as f:
            index = json.load(f)
        print(f"  Last sync:  {index.get('synced_at', '?')}")
        print(f"  Agents:     {index.get('agent_count', '?')}")
        print(f"  Cache dir:  {cache_dir}")

    elif args.cache_action == "clear":
        import shutil
        if cache_dir.exists():
            shutil.rmtree(cache_dir)
            print(f"  🗑️  Cache cleared: {cache_dir}")
        else:
            print("  No cache to clear.")

    else:
        print("  Usage: aip cache [sync|lookup|status|clear]")


def cmd_doctor(args):
    """Diagnose AIP setup: connectivity, credentials, service compatibility."""
    import urllib.request
    import platform as plat_mod

    service = args.service or AIP_SERVICE
    checks = []
    warnings = []

    def check(name, ok, detail=""):
        status = "✅" if ok else "❌"
        checks.append((name, ok))
        msg = f"  {status} {name}"
        if detail:
            msg += f" — {detail}"
        print(msg)
        return ok

    print("═══ AIP Doctor ═══\n")
    print(f"  Python: {plat_mod.python_version()}")
    print(f"  OS: {plat_mod.system()} {plat_mod.release()}")
    print(f"  Service: {service}")
    print()

    # 1. Check dependencies
    print("  📦 Dependencies")
    for dep in ["nacl", "requests"]:
        try:
            __import__(dep)
            check(f"  {dep}", True, "installed")
        except ImportError:
            check(f"  {dep}", False, "missing — pip install pynacl requests")
    print()

    # 2. Service connectivity
    print("  🌐 Connectivity")
    svc_ok = False
    svc_version = "?"
    try:
        with urllib.request.urlopen(f"{service}/health", timeout=10) as resp:
            health = json.loads(resp.read().decode())
        svc_ok = health.get("status") == "healthy"
        svc_version = health.get("version", "?")
        db_ok = health.get("checks", {}).get("database", {}).get("ok", False)
        check("  Service reachable", svc_ok, f"v{svc_version}")
        check("  Database", db_ok)
    except Exception as e:
        check("  Service reachable", False, str(e))
    print()

    # 3. Credentials
    print("  🔑 Credentials")
    # Use same logic as find_credentials() — respects AIP_CREDENTIALS_PATH env var
    env_creds_path = os.environ.get("AIP_CREDENTIALS_PATH")
    if env_creds_path:
        search_paths = [Path(env_creds_path)]
    else:
        search_paths = list(CREDENTIALS_PATHS) + [Path("aip_credentials.json"), Path("credentials/aip_credentials.json")]
    creds = None
    creds_file = None
    for p in search_paths:
        if p.exists():
            try:
                with open(p) as f:
                    creds = json.load(f)
                creds_file = p
                break
            except Exception:
                pass

    if creds:
        check("  Credentials file", True, str(creds_file))
        did = creds.get("did", "")
        has_did = bool(did and did.startswith("did:aip:"))
        check("  DID format", has_did, did[:40] if did else "missing")
        has_pk = bool(creds.get("private_key"))
        check("  Private key", has_pk, "present" if has_pk else "MISSING — cannot sign")
        has_pub = bool(creds.get("public_key"))
        check("  Public key", has_pub, "present" if has_pub else "MISSING")

        # 4. Registration check (online)
        if svc_ok and has_did:
            print()
            print("  📡 Registration")
            try:
                with urllib.request.urlopen(f"{service}/trust/{did}", timeout=5) as resp:
                    info = json.loads(resp.read().decode())
                registered = bool(info.get("registered") or info.get("did"))
                check("  Registered on service", registered)
                v_recv = info.get("vouch_count", 0)
                check("  Vouches received", v_recv > 0, f"{v_recv}")
                if v_recv == 0:
                    warnings.append("No vouches received — ask a trusted agent to vouch for you")
            except urllib.request.HTTPError as e:
                if e.code == 404:
                    check("  Registered on service", False, "DID not found — run `aip quickstart`")
                else:
                    check("  Registered on service", False, f"HTTP {e.code}")
            except Exception as e:
                check("  Registered on service", False, str(e))
    else:
        check("  Credentials file", False, "not found — run `aip init` or `aip register`")
    print()

    # 5. Version check
    print("  🔄 Version")
    try:
        from aip_identity import __version__
        check("  CLI version", True, f"v{__version__}")
        if svc_ok and svc_version != "?":
            match = __version__ == svc_version
            if not match:
                warnings.append(f"Version mismatch: CLI v{__version__} ≠ service v{svc_version} — pip install --upgrade aip-identity")
            check("  CLI ↔ service version match", match, f"CLI v{__version__} vs service v{svc_version}")
    except ImportError:
        check("  CLI version", False, "could not determine")
    print()

    # Summary
    total = len(checks)
    passed = sum(1 for _, ok in checks if ok)
    failed = total - passed

    if warnings:
        print("  ⚠️  Warnings:")
        for w in warnings:
            print(f"    • {w}")
        print()

    if failed == 0:
        print(f"  🎉 All {total} checks passed — AIP is healthy!")
    else:
        print(f"  ⚠️  {passed}/{total} checks passed, {failed} failed")
        print("  Run `aip init <platform> <username>` to set up, or check the docs:")
        print("  https://the-nexus-guard.github.io/aip/")
    print()


def cmd_observe(args):
    """Submit behavioral observations or view PDR scores."""
    service = args.service or AIP_SERVICE

    if args.observe_action == "submit":
        creds = find_credentials()
        if not creds:
            print("  ❌ No credentials found. Run `aip quickstart` to get started.")
            sys.exit(1)

        did = creds["did"]

        # Parse observations from JSON file or inline
        if args.file:
            try:
                with open(args.file) as f:
                    obs_data = json.load(f)
            except (json.JSONDecodeError, FileNotFoundError) as e:
                print(f"  ❌ Error reading observations file: {e}")
                sys.exit(1)
        elif args.promised and args.delivered:
            obs_data = [{
                "promised": [p.strip() for p in args.promised.split(",")],
                "delivered": [d.strip() for d in args.delivered.split(",")],
            }]
        else:
            print("  ❌ Provide --promised and --delivered, or --file with JSON observations.")
            sys.exit(1)

        # Normalize: accept list of obs or single obs
        if isinstance(obs_data, dict):
            obs_data = [obs_data]

        # Sign nonce for authentication
        nonce = f"observe:{did}:{int(time.time())}:{hashlib.sha256(os.urandom(16)).hexdigest()[:8]}"
        private_key_b64 = creds.get("private_key", "")
        if not private_key_b64:
            print("  ❌ Private key not found in credentials.")
            sys.exit(1)

        try:
            from nacl.signing import SigningKey
            priv_bytes = base64.b64decode(private_key_b64)
            signing_key = SigningKey(priv_bytes)
            sig = signing_key.sign(nonce.encode()).signature
            sig_hex = sig.hex()
        except Exception as e:
            print(f"  ❌ Signing failed: {e}")
            sys.exit(1)

        # Format observations for API
        observations = []
        for obs in obs_data:
            entry = {
                "promised": obs.get("promised", []),
                "delivered": obs.get("delivered", []),
            }
            if "timestamp" in obs:
                entry["timestamp"] = obs["timestamp"]
            if "conditions" in obs:
                entry["conditions"] = obs["conditions"]
            observations.append(entry)

        payload = {
            "did": did,
            "observations": observations,
            "signature": sig_hex,
            "nonce": nonce,
        }

        import requests
        resp = requests.post(f"{service}/observations", json=payload)
        if resp.status_code == 200:
            result = resp.json()
            print(f"  ✅ Submitted {result.get('observations_stored', 0)} observation(s)")
            print(f"     DID: {did}")
        else:
            print(f"  ❌ Submission failed ({resp.status_code}): {resp.text}")
            sys.exit(1)

    elif args.observe_action == "scores":
        target_did = args.did
        if not target_did:
            creds = find_credentials()
            if creds:
                target_did = creds["did"]
            else:
                print("  ❌ Provide a DID or run `aip quickstart` to set up identity.")
                sys.exit(1)

        import requests
        window = getattr(args, "window", 28)
        resp = requests.get(f"{service}/observations/{target_did}/scores", params={"window_days": window})
        if resp.status_code == 200:
            data = resp.json()
            if data.get("observation_count", 0) == 0:
                print(f"  📊 No observations recorded for {target_did}")
                print(f"     Submit observations with: aip observe submit --promised 'task1,task2' --delivered 'task1'")
            else:
                print(f"  📊 PDR Scores for {target_did}")
                print(f"     Calibration: {data.get('calibration', 'N/A')}")
                print(f"     Robustness:  {data.get('robustness', 'N/A')}")
                print(f"     Observations: {data.get('observation_count', 0)}")
                print(f"     Window: {data.get('window_days', 0)} days")
                if data.get("chain_hash"):
                    print(f"     Chain hash: {data['chain_hash'][:16]}...")
        else:
            print(f"  ❌ Failed to fetch scores ({resp.status_code}): {resp.text}")
            sys.exit(1)

    elif args.observe_action == "list":
        target_did = args.did
        if not target_did:
            creds = find_credentials()
            if creds:
                target_did = creds["did"]
            else:
                print("  ❌ Provide a DID or run `aip quickstart` to set up identity.")
                sys.exit(1)

        import requests
        limit = getattr(args, "limit", 50)
        resp = requests.get(f"{service}/observations/{target_did}", params={"limit": limit})
        if resp.status_code == 200:
            data = resp.json()
            obs_list = data.get("observations", [])
            if not obs_list:
                print(f"  📋 No observations for {target_did}")
            else:
                print(f"  📋 {len(obs_list)} observation(s) for {target_did}\n")
                for i, obs in enumerate(obs_list, 1):
                    promised = ", ".join(obs.get("promised", []))
                    delivered = ", ".join(obs.get("delivered", []))
                    ts = obs.get("timestamp", "unknown")
                    print(f"  {i}. [{ts}]")
                    print(f"     Promised:  {promised}")
                    print(f"     Delivered: {delivered}")
                    if obs.get("conditions"):
                        print(f"     Conditions: {json.dumps(obs['conditions'])}")
                    print()
        else:
            print(f"  ❌ Failed to fetch observations ({resp.status_code}): {resp.text}")
            sys.exit(1)

    else:
        print("Usage: aip observe <submit|scores|list>")
        print("  submit  — Submit behavioral observations (promised vs delivered)")
        print("  scores  — View PDR scores for an agent")
        print("  list    — List stored observations")
        sys.exit(1)


def cmd_wallet(args):
    """Manage wallet-DID bindings for on-chain credential verification."""
    service = args.service or AIP_SERVICE

    if args.wallet_action == "bind":
        creds = find_credentials()
        if not creds:
            print("  ❌ No credentials found. Run `aip quickstart` to get started.")
            sys.exit(1)

        did = creds["did"]
        wallet = args.address
        chain_type = args.chain or "evm"
        ts = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())

        # Sign the binding message
        message = f"bind:{wallet}:{ts}"
        private_key_b64 = creds.get("private_key", "")
        if not private_key_b64:
            print("  ❌ Private key not found in credentials.")
            sys.exit(1)

        import base64
        try:
            from nacl.signing import SigningKey
            priv_bytes = base64.b64decode(private_key_b64)
            signing_key = SigningKey(priv_bytes)
            sig = signing_key.sign(message.encode()).signature
            sig_b64 = base64.b64encode(sig).decode()
        except Exception as e:
            print(f"  ❌ Failed to sign binding: {e}")
            sys.exit(1)

        payload = {
            "did": did,
            "wallet_address": wallet,
            "chain_type": chain_type,
            "did_signature": sig_b64,
            "timestamp": ts,
        }

        try:
            r = requests.post(f"{service}/oracle/wallet/bind", json=payload, timeout=15)
            resp = r.json()
            if resp.get("success"):
                print(f"  ✅ Wallet {wallet[:10]}…{wallet[-4:]} bound to {did}")
                print(f"     Chain: {chain_type}")
                print(f"\n  Next: aip wallet verify <conditions>")
            else:
                print(f"  ❌ {resp.get('detail', resp.get('message', 'Unknown error'))}")
        except Exception as e:
            print(f"  ❌ Failed to bind wallet: {e}")
            sys.exit(1)

    elif args.wallet_action == "list":
        creds = find_credentials()
        did = args.did or (creds["did"] if creds else None)
        if not did:
            print("  ❌ Provide --did or register first.")
            sys.exit(1)

        try:
            r = requests.get(f"{service}/oracle/wallet/{did}", timeout=15)
            resp = r.json()
            wallets = resp.get("wallets", [])
            if not wallets:
                print(f"  No wallets bound to {did}")
                print("  Run `aip wallet bind <address>` to bind one.")
                return
            print(f"  Wallets bound to {did}:\n")
            for w in wallets:
                print(f"    {w['wallet_address']}  ({w['chain_type']})  bound {w['bound_at'][:10]}")
        except Exception as e:
            print(f"  ❌ {e}")
            sys.exit(1)

    elif args.wallet_action == "verify":
        creds = find_credentials()
        did = args.did or (creds["did"] if creds else None)
        if not did:
            print("  ❌ Provide --did or register first.")
            sys.exit(1)

        # Parse conditions from args
        conditions = []
        for cond_str in args.conditions:
            parts = cond_str.split(",")
            cond = {}
            for part in parts:
                k, _, v = part.partition("=")
                if k == "chain_id":
                    cond["chain_id"] = int(v)
                elif k in ("threshold", "decimals"):
                    cond[k] = float(v) if k == "threshold" else int(v)
                else:
                    cond[k] = v
            if "type" not in cond:
                cond["type"] = "token_balance"
            conditions.append(cond)

        payload = {"did": did, "conditions": conditions}
        if args.wallet:
            payload["wallet_address"] = args.wallet

        try:
            r = requests.post(f"{service}/oracle/verify/onchain", json=payload, timeout=30)
            resp = r.json()
            if resp.get("success"):
                passed = resp.get("passed", False)
                emoji = "✅" if passed else "❌"
                print(f"  {emoji} On-chain verification: {'PASSED' if passed else 'FAILED'}")
                print(f"     DID: {resp['did']}")
                print(f"     Wallet: {resp['wallet_address']}")
                if resp.get("attestation_id"):
                    print(f"     Attestation: {resp['attestation_id']}")
                if resp.get("vouch_id"):
                    print(f"     Oracle vouch: {resp['vouch_id']}")
                if resp.get("expires_at"):
                    print(f"     Expires: {resp['expires_at']}")
                for r in resp.get("results", []):
                    met = "✓" if r.get("met") else "✗"
                    label = r.get("label", r.get("type", "?"))
                    print(f"       [{met}] {label}")
            else:
                print(f"  ❌ {resp.get('detail', 'Verification failed')}")
        except Exception as e:
            print(f"  ❌ {e}")
            sys.exit(1)

    else:
        print("  Usage: aip wallet <bind|list|verify>")
        print("  Run `aip wallet bind <address>` to get started.")


def main():
    CATEGORIZED_HELP = """\
AIP — Agent Identity Protocol

Quick start:
  aip quickstart          Get your agent identity in 30 seconds
  aip whoami              Show your current identity
  aip demo                See AIP in action (60-second crypto demo)

Identity:
  aip register            Register a new agent identity
  aip init                One-command setup: register + set profile
  aip verify <path>       Verify a signed artifact
  aip trust-score         Check trust between two agents
  aip trust-graph         Visualize the trust network

Communication:
  aip message <did> <msg> Send an encrypted message
  aip messages            Check your messages
  aip reply <id> <msg>    Reply to a message

Artifacts:
  aip sign <path>         Sign a file or directory
  aip vouch <did>         Vouch for another agent

On-Chain:
  aip wallet bind <addr>  Bind a wallet address to your DID
  aip wallet list         List your bound wallets
  aip wallet verify       Verify on-chain conditions (InsumerAPI)

Tools:
  aip status              Dashboard: identity + network health
  aip doctor              Diagnose your AIP setup
  aip audit               Self-audit: trust, vouches, profile

Run 'aip <command> --help' for details on any command.
Run 'aip commands' for the full command list.
"""

    parser = argparse.ArgumentParser(
        prog="aip",
        description="Agent Identity Protocol — cryptographic identity, trust, and messaging for AI agents",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        usage="aip <command> [options]",
        epilog="Run 'aip quickstart' to get started in 30 seconds.",
    )
    parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
    parser.add_argument("--service", default=None, help=f"AIP service URL (default: {AIP_SERVICE})")
    sub = parser.add_subparsers(dest="command")

    # quickstart
    p_qs = sub.add_parser("quickstart", help="Get your agent identity in 30 seconds")
    p_qs.add_argument("--platform", default="cli", help="Platform name (default: cli)")
    p_qs.add_argument("--username", default=None, help="Username (default: auto-generated)")

    # commands (full list)
    sub.add_parser("commands", help="Show all available commands")

    # init (quick setup)
    p_init = sub.add_parser("init", help="One-command setup: register + set profile")
    p_init.add_argument("platform", nargs="?", default=None, help="Platform name (e.g. github, moltbook)")
    p_init.add_argument("username", nargs="?", default=None, help="Your username on that platform")
    p_init.add_argument("--name", help="Display name for your profile")
    p_init.add_argument("--bio", help="Short bio (max 500 chars)")
    p_init.add_argument("--tags", help="Comma-separated tags (e.g. 'ai,security,builder')")
    p_init.add_argument("--force", action="store_true", help="Re-register even if credentials exist")

    # register
    p_reg = sub.add_parser("register", help="Register a new agent DID")
    p_reg.add_argument("platform", help="Platform name (e.g. moltbook, github)")
    p_reg.add_argument("username", help="Your username on that platform")
    p_reg.add_argument("--secure", action="store_true", help="Generate key locally (private key never leaves machine)")
    p_reg.add_argument("--output", "-o", help="Save credentials to this path")

    # verify
    p_ver = sub.add_parser("verify", help="Verify a signed artifact")
    p_ver.add_argument("path", help="Path to signed directory or file")

    # vouch
    p_vouch = sub.add_parser("vouch", help="Vouch for another agent")
    p_vouch.add_argument("target_did", help="DID to vouch for")
    p_vouch.add_argument("--scope", default="GENERAL", help="Trust scope (default: GENERAL)")
    p_vouch.add_argument("--statement", help="Trust statement")
    p_vouch.add_argument("--ttl-days", type=int, help="Expiration in days")

    # sign
    p_sign = sub.add_parser("sign", help="Sign a skill directory or file")
    p_sign.add_argument("path", help="Path to sign")

    # message (send)
    p_msg = sub.add_parser("message", help="Send a message to another agent")
    p_msg.add_argument("recipient", help="Recipient DID")
    p_msg.add_argument("content", help="Message content")

    # messages (inbox)
    p_msgs = sub.add_parser("messages", help="Retrieve your messages")
    p_msgs.add_argument("--unread", action="store_true", default=False, help="Only show unread messages")
    p_msgs.add_argument("--decrypt", action="store_true", default=True, help="Decrypt encrypted messages (default: yes)")
    p_msgs.add_argument("--no-decrypt", dest="decrypt", action="store_false", help="Don't decrypt messages")
    p_msgs.add_argument("--mark-read", action="store_true", default=False, help="Mark retrieved messages as read")

    # reply
    p_reply = sub.add_parser("reply", help="Reply to a received message")
    p_reply.add_argument("message_id", help="ID of the message to reply to")
    p_reply.add_argument("content", help="Reply content")

    # rotate-key
    p_rot = sub.add_parser("rotate-key", help="Rotate your signing key")

    # badge
    p_badge = sub.add_parser("badge", help="Show trust badge for a DID")
    p_badge.add_argument("did", help="DID to look up")

    # list
    p_list = sub.add_parser("list", help="List registered agents on the AIP service")
    p_list.add_argument("--limit", type=int, default=50, help="Max results (default: 50)")
    p_list.add_argument("--offset", type=int, default=0, help="Pagination offset")

    # trust-score
    p_ts = sub.add_parser("trust-score", help="Calculate transitive trust score between two agents")
    p_ts.add_argument("source", help="Source DID")
    p_ts.add_argument("target", help="Target DID")
    p_ts.add_argument("--scope", default=None, help="Trust scope filter (e.g. GENERAL, CODE_SIGNING)")

    # trust-graph
    p_tg = sub.add_parser("trust-graph", help="Visualize the AIP trust network")
    p_tg.add_argument("--format", choices=["ascii", "dot", "json"], default="ascii", help="Output format (default: ascii)")

    # whoami
    p_revoke = sub.add_parser("revoke", help="Revoke a vouch you previously issued")
    p_revoke.add_argument("vouch_id", help="ID of the vouch to revoke")
    p_revoke.add_argument("--service", default=None, help="AIP service URL")

    # search
    p_search = sub.add_parser("search", help="Search agents by platform or username")
    p_search.add_argument("query", help="Search term (matched against platform or username)")
    p_search.add_argument("--platform", default=None, help="Filter by platform (e.g. moltbook, github)")
    p_search.add_argument("--limit", type=int, default=50, help="Max results (default: 50)")

    sub.add_parser("whoami", help="Show your current identity")

    # directory
    p_dir = sub.add_parser("directory", help="Discover other agents on the AIP network")
    p_dir.add_argument("--limit", type=int, default=10, help="Number of agents to show")

    # status
    sub.add_parser("status", help="Dashboard: identity + network health + unread messages")

    # stats
    sub.add_parser("stats", help="Public network statistics with growth data")

    # webhook
    p_wh = sub.add_parser("webhook", help="Manage webhook subscriptions (list/add/delete)")
    p_wh_sub = p_wh.add_subparsers(dest="webhook_action")
    p_wh_sub.add_parser("list", help="List your webhooks")
    p_wh_add = p_wh_sub.add_parser("add", help="Register a new webhook")
    p_wh_add.add_argument("url", help="HTTPS URL to receive notifications")
    p_wh_add.add_argument("--events", default="registration", help="Comma-separated events (registration,vouch,message or *)")
    p_wh_del = p_wh_sub.add_parser("delete", help="Delete a webhook")
    p_wh_del.add_argument("webhook_id", help="Webhook ID to delete")

    # changelog
    p_cl = sub.add_parser("changelog", help="Show recent AIP changes and version history")
    p_cl.add_argument("-n", "--entries", type=int, default=5, help="Number of versions to show (default: 5)")

    # export
    p_export = sub.add_parser("export", help="Export your identity (DID + public key) as portable JSON")
    p_export.add_argument("-o", "--output", default=None, help="Output file (default: stdout)")
    p_export.add_argument("--include-private", action="store_true", help="Include private key (DANGEROUS — for backup only)")

    # import
    p_import = sub.add_parser("import", help="Import another agent's public key for offline verification")
    p_import.add_argument("source", help="JSON file path or DID to fetch from service")
    p_import.add_argument("--keyring-dir", default=None, help="Directory to store imported keys (default: ~/.aip/keyring/)")

    # migrate
    p_migrate = sub.add_parser("migrate", help="Migrate credentials between locations or upgrade format")
    p_migrate.add_argument("--target", default=None, help="Target path (default: ~/.aip/credentials.json)")
    p_migrate.add_argument("--cleanup", action="store_true", help="Remove old credential files after migration")
    p_migrate.add_argument("--dry-run", action="store_true", help="Show what would happen without changing anything")

    # cache (offline mode)
    p_cache = sub.add_parser("cache", help="Offline cache: sync agent directory for local verification")
    p_cache_sub = p_cache.add_subparsers(dest="cache_action")
    p_cache_sync = p_cache_sub.add_parser("sync", help="Download agent directory to local cache")
    p_cache_sync.add_argument("--service", default=None, help="AIP service URL")
    p_cache_lookup = p_cache_sub.add_parser("lookup", help="Look up an agent from cache")
    p_cache_lookup.add_argument("did", nargs="?", help="DID to look up")
    p_cache_sub.add_parser("status", help="Show cache status")
    p_cache_sub.add_parser("clear", help="Clear local cache")
    p_cache.add_argument("--cache-dir", default=None, help="Cache directory (default: ~/.aip/cache/)")

    # wallet
    p_wallet = sub.add_parser("wallet", help="Manage wallet-DID bindings for on-chain verification")
    p_wallet_sub = p_wallet.add_subparsers(dest="wallet_action")
    p_wallet_bind = p_wallet_sub.add_parser("bind", help="Bind a wallet address to your DID")
    p_wallet_bind.add_argument("address", help="Wallet address (e.g. 0x…)")
    p_wallet_bind.add_argument("--chain", default="evm", help="Chain type: evm, solana, xrpl (default: evm)")
    p_wallet_list = p_wallet_sub.add_parser("list", help="List your bound wallets")
    p_wallet_list.add_argument("--did", default=None, help="DID to list wallets for (default: your own)")
    p_wallet_verify = p_wallet_sub.add_parser("verify", help="Verify on-chain conditions")
    p_wallet_verify.add_argument("conditions", nargs="+", help="Conditions as type=token_balance,chain_id=1,contract_address=0x…,threshold=100,decimals=18")
    p_wallet_verify.add_argument("--did", default=None, help="DID to verify (default: your own)")
    p_wallet_verify.add_argument("--wallet", default=None, help="Specific wallet address (if multiple bound)")

    # demo
    p_observe = sub.add_parser("observe", help="Submit behavioral observations or view PDR scores")
    p_observe_sub = p_observe.add_subparsers(dest="observe_action")
    p_obs_submit = p_observe_sub.add_parser("submit", help="Submit observations (promised vs delivered)")
    p_obs_submit.add_argument("--promised", help="Comma-separated list of promised deliverables")
    p_obs_submit.add_argument("--delivered", help="Comma-separated list of actual deliverables")
    p_obs_submit.add_argument("--file", "-f", help="JSON file with observations array")
    p_obs_scores = p_observe_sub.add_parser("scores", help="View PDR scores for an agent")
    p_obs_scores.add_argument("did", nargs="?", help="DID to look up (default: your own)")
    p_obs_scores.add_argument("--window", type=int, default=28, help="Scoring window in days (default: 28)")
    p_obs_list = p_observe_sub.add_parser("list", help="List stored observations")
    p_obs_list.add_argument("did", nargs="?", help="DID to look up (default: your own)")
    p_obs_list.add_argument("--limit", type=int, default=50, help="Max observations to show")

    p_vouch_vc = sub.add_parser("vouch-vc", help="Export vouches as W3C Verifiable Credentials (MCP-I interop)")
    p_vouch_vc.add_argument("--target", help="Filter by target DID")
    p_vouch_vc.add_argument("--received", action="store_true", help="Export received vouches instead of issued")
    p_vouch_vc.add_argument("--output", "-o", help="Write to file instead of stdout")

    p_demo = sub.add_parser("demo", help="See AIP in action — identity, signatures, encryption")
    p_demo.add_argument("--interactive", "-i", action="store_true", default=False,
                         help="Local crypto demo (default)")
    p_demo.add_argument("--network", "-n", action="store_true", default=False,
                         help="Show live network stats instead")

    # audit
    sub.add_parser("audit", help="Self-audit: trust, vouches, messages, profile completeness")
    sub.add_parser("doctor", help="Diagnose AIP setup: connectivity, credentials, service version")

    p_profile = sub.add_parser("profile", help="View or update agent profiles")
    p_profile_sub = p_profile.add_subparsers(dest="profile_action")
    p_profile_get = p_profile_sub.add_parser("show", help="Show an agent's profile")
    p_profile_get.add_argument("did", nargs="?", help="DID to look up (default: your own)")
    p_profile_set = p_profile_sub.add_parser("set", help="Update your profile")
    p_profile_set.add_argument("--name", dest="display_name", help="Display name")
    p_profile_set.add_argument("--bio", help="Short bio (max 500 chars)")
    p_profile_set.add_argument("--avatar", dest="avatar_url", help="Avatar URL")
    p_profile_set.add_argument("--website", help="Website URL")
    p_profile_set.add_argument("--tags", help="Comma-separated tags (max 10)")

    args = parser.parse_args()

    commands = {
        "quickstart": cmd_quickstart,
        "init": cmd_init,
        "demo": cmd_demo,
        "register": cmd_register,
        "profile": cmd_profile,
        "verify": cmd_verify,
        "vouch": cmd_vouch,
        "vouch-vc": cmd_vouch_vc,
        "revoke": cmd_revoke,
        "sign": cmd_sign,
        "message": cmd_message,
        "messages": cmd_messages,
        "reply": cmd_reply,
        "rotate-key": cmd_rotate_key,
        "badge": cmd_badge,
        "whoami": cmd_whoami,
        "directory": cmd_directory,
        "list": cmd_list,
        "trust-score": cmd_trust_score,
        "trust-graph": cmd_trust_graph,
        "search": cmd_search,
        "audit": cmd_audit,
        "doctor": cmd_doctor,
        "status": cmd_status,
        "stats": cmd_stats,
        "webhook": cmd_webhook,
        "changelog": cmd_changelog,
        "migrate": cmd_migrate,
        "cache": cmd_cache,
        "export": cmd_export,
        "import": cmd_import,
        "wallet": cmd_wallet,
        "observe": cmd_observe,
    }

    if args.command == "commands":
        parser.print_help()
    elif args.command in commands:
        commands[args.command](args)
    else:
        # First-run experience: if no credentials exist, show welcome + quickstart
        creds = find_credentials()
        if not creds:
            print("🦞 Welcome to AIP — Agent Identity Protocol\n")
            print("You don't have an agent identity yet. Let's fix that!\n")
            print("  Quick setup (30 seconds):")
            print("    aip quickstart                        — auto-generate identity")
            print("    aip init github my_agent --name 'Me'  — register with a profile\n")
            print("  Just exploring?")
            print("    aip demo     — see AIP in action (60-second crypto demo)")
            print("    aip stats    — see the live network")
            print("    aip list     — browse registered agents\n")
            print(f"  Docs: https://the-nexus-guard.github.io/aip/")
            print(f"  Playground: https://the-nexus-guard.github.io/aip/playground.html\n")
        else:
            print(CATEGORIZED_HELP)


if __name__ == "__main__":
    main()
