#!/usr/bin/env python3
"""
demo_python.py — comprehensive Python demo for connecting to a running n_signer
via Qubes qrexec and performing all three core operations:

  1. get_public_key  — retrieve a Nostr public key by nostr_index
  2. sign_event      — sign a Nostr event (kind 1 text note)
  3. nip44_encrypt   — encrypt a message to a peer (and decrypt it back)

Uses qrexec-client-vm (Qubes OS inter-qube IPC). No auth envelope needed —
identity comes from QREXEC_REMOTE_DOMAIN on the server side.

Prerequisites:
  - n_signer running in the target qube with --bridge-source-trusted
  - qubes.NsignerRpc service installed in the target qube
  - dom0 qrexec policy allowing this qube to call the service
  - Python 3 with no external dependencies (uses only stdlib)

Usage:
  python3 client/demo_python.py <target_qube> [nostr_index]
  python3 client/demo_python.py nostr_signer 1

If no nostr_index is given, defaults to 0.
"""

import json
import struct
import subprocess
import sys
import time


# --- Bech32 encoder (NIP-19 npub conversion, no external dependencies) ---

CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"


def bech32_polymod(values):
    generator = [0x3B6A57B2, 0x26508E6D, 0x1EA119FA, 0x3D4233DD, 0x2A1462B3]
    chk = 1
    for v in values:
        b = chk >> 25
        chk = (chk & 0x1FFFFFF) << 5 ^ v
        for i in range(5):
            chk ^= generator[i] if ((b >> i) & 1) else 0
    return chk


def bech32_hrp_expand(hrp):
    return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp]


def bech32_create_checksum(hrp, data):
    values = bech32_hrp_expand(hrp) + data
    polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ 1
    return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)]


def bech32_encode(hrp, data):
    combined = data + bech32_create_checksum(hrp, data)
    return hrp + "1" + "".join([CHARSET[d] for d in combined])


def convertbits(data, frombits, tobits, pad=True):
    acc = 0
    bits = 0
    ret = []
    maxv = (1 << tobits) - 1
    max_acc = (1 << (frombits + tobits - 1)) - 1
    for value in data:
        acc = ((acc << frombits) | value) & max_acc
        bits += frombits
        while bits >= tobits:
            bits -= tobits
            ret.append((acc >> bits) & maxv)
    if pad and bits:
        ret.append((acc << (tobits - bits)) & maxv)
    return ret


def hex_to_npub(pubkey_hex):
    """Convert a 32-byte hex pubkey to bech32 npub format (NIP-19)."""
    pubkey_bytes = bytes.fromhex(pubkey_hex)
    data = convertbits(pubkey_bytes, 8, 5)
    return bech32_encode("npub", data)


# --- n_signer qrexec client ---

def call_nsigner(target_qube, request):
    """
    Call n_signer via qrexec. Sends one framed JSON-RPC request, receives one
    framed response. Each call spawns a fresh qrexec-client-vm process.

    Framing: 4-byte big-endian length prefix + JSON payload.
    No auth envelope needed for qrexec (identity from QREXEC_REMOTE_DOMAIN).
    """
    payload = json.dumps(request, separators=(",", ":")).encode("utf-8")
    frame = struct.pack(">I", len(payload)) + payload

    proc = subprocess.Popen(
        ["qrexec-client-vm", target_qube, "qubes.NsignerRpc"],
        stdin=subprocess.PIPE,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
    )
    out, err = proc.communicate(frame)

    if proc.returncode != 0:
        raise RuntimeError(
            f"qrexec-client-vm exited with code {proc.returncode}: "
            f"{err.decode('utf-8', 'replace').strip()}"
        )

    if len(out) < 4:
        raise RuntimeError("short response (missing frame header)")

    length = struct.unpack(">I", out[:4])[0]
    body = out[4 : 4 + length]
    if len(body) != length:
        raise RuntimeError(
            f"short response payload: expected {length}, got {len(body)}"
        )

    return json.loads(body.decode("utf-8"))


# --- Demos ---

def demo_get_public_key(target_qube, nostr_index):
    """Demo 1: Get a public key by nostr_index."""
    print(f"\n=== Demo 1: get_public_key (nostr_index={nostr_index}) ===")

    response = call_nsigner(
        target_qube,
        {"id": "1", "method": "get_public_key", "params": [{"nostr_index": nostr_index}]},
    )

    if "error" in response:
        raise RuntimeError(f"get_public_key failed: {json.dumps(response['error'])}")

    pubkey_hex = response["result"]
    npub = hex_to_npub(pubkey_hex)
    print(f"  pubkey hex: {pubkey_hex}")
    print(f"  npub:       {npub}")
    return pubkey_hex


def demo_sign_event(target_qube, nostr_index, pubkey_hex):
    """Demo 2: Sign a Nostr event (kind 1 text note)."""
    print("\n=== Demo 2: sign_event (kind 1 text note) ===")

    unsigned_event = {
        "kind": 1,
        "content": "Hello from n_signer Python demo!",
        "created_at": int(time.time()),
        "tags": [],
        "pubkey": pubkey_hex,
    }

    print("  Unsigned event:")
    print(f"    {json.dumps(unsigned_event, separators=(',', ':'))}")

    response = call_nsigner(
        target_qube,
        {
            "id": "2",
            "method": "sign_event",
            "params": [json.dumps(unsigned_event, separators=(",", ":")), {"nostr_index": nostr_index}],
        },
    )

    if "error" in response:
        raise RuntimeError(f"sign_event failed: {json.dumps(response['error'])}")

    signed_event = json.loads(response["result"])
    print("  Signed event:")
    print(f"    {json.dumps(signed_event, separators=(',', ':'))}")
    print(f"  event id: {signed_event['id']}")
    print(f"  signature: {signed_event['sig']}")
    return signed_event


def demo_nip44(target_qube, nostr_index, pubkey_hex):
    """Demo 3: NIP-44 encrypt and decrypt."""
    plaintext = "Secret message from n_signer Python demo!"

    print("\n=== Demo 3: nip44_encrypt / nip44_decrypt ===")
    print(f'  plaintext:  "{plaintext}"')
    print(f"  peer pubkey: {pubkey_hex} (self)")

    # Encrypt
    enc_response = call_nsigner(
        target_qube,
        {
            "id": "3",
            "method": "nip44_encrypt",
            "params": [pubkey_hex, plaintext, {"nostr_index": nostr_index}],
        },
    )

    if "error" in enc_response:
        raise RuntimeError(f"nip44_encrypt failed: {json.dumps(enc_response['error'])}")

    ciphertext = enc_response["result"]
    print(f"  ciphertext: {ciphertext}")

    # Decrypt
    dec_response = call_nsigner(
        target_qube,
        {
            "id": "4",
            "method": "nip44_decrypt",
            "params": [pubkey_hex, ciphertext, {"nostr_index": nostr_index}],
        },
    )

    if "error" in dec_response:
        raise RuntimeError(f"nip44_decrypt failed: {json.dumps(dec_response['error'])}")

    decrypted = dec_response["result"]
    print(f'  decrypted:  "{decrypted}"')

    if plaintext == decrypted:
        print("  ✓ Round-trip verified: plaintext matches decrypted")
    else:
        raise RuntimeError("Round-trip FAILED: plaintext does not match decrypted")


def demo_mine_event(target_qube, nostr_index):
    print("\n--- Demo 4: mine_event (NIP-13 Proof-of-Work) ---")

    event = {"kind": 1, "content": "Hello Nostr with PoW!", "tags": []}

    print("  Mining with difficulty=4, threads=4, timeout_sec=30...")
    response = call_nsigner(
        target_qube,
        {
            "id": "5",
            "method": "mine_event",
            "params": [json.dumps(event), {
                "difficulty": 4,
                "threads": 4,
                "timeout_sec": 30,
                "nostr_index": nostr_index,
            }],
        },
    )

    if "error" in response:
        raise RuntimeError(f"mine_event failed: {json.dumps(response['error'])}")

    result = json.loads(response["result"])
    print(f"  achieved_difficulty: {result['achieved_difficulty']}")
    print(f"  target_reached:      {result['target_reached']}")
    print(f"  elapsed_sec:         {result['elapsed_sec']}")
    print(f"  attempts:            {result['attempts']}")

    mined_event = json.loads(result["event"])
    print(f"  event id:            {mined_event['id']}")
    print(f"  nonce tag:           {mined_event['tags'][0]}")

    if result["target_reached"]:
        print("  ✓ Target difficulty reached!")
    else:
        print(f"  (Target not reached, best effort: {result['achieved_difficulty']} bits)")


# --- Main ---

def main():
    target_qube = sys.argv[1] if len(sys.argv) > 1 else "nostr_signer"
    nostr_index = int(sys.argv[2]) if len(sys.argv) > 2 else 0

    print("=== n_signer Python Demo ===")
    print(f"Target qube: {target_qube}")
    print(f"Service:     qubes.NsignerRpc")
    print(f"nostr_index: {nostr_index}")
    print("\nConnecting to n_signer via qrexec...")

    try:
        pubkey_hex = demo_get_public_key(target_qube, nostr_index)
        demo_sign_event(target_qube, nostr_index, pubkey_hex)
        demo_nip44(target_qube, nostr_index, pubkey_hex)
        demo_mine_event(target_qube, nostr_index)

        print("\n=== Summary ===")
        print("All demos completed successfully.")
    except Exception as e:
        print("\n=== Summary ===")
        print(f"Demo failed: {e}")
        sys.exit(1)


if __name__ == "__main__":
    main()
