#!/usr/bin/env node
/**
 * demo_javascript.js — comprehensive JavaScript 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
 *   - nostr-tools and @noble/secp256k1 npm packages installed
 *
 * Install dependencies (from n_signer repo root):
 *   npm install nostr-tools @noble/secp256k1
 *
 * Usage:
 *   node client/demo_javascript.js <target_qube> [nostr_index]
 *   node client/demo_javascript.js nostr_signer 1
 *
 * If no nostr_index is given, defaults to 0.
 */

const { spawn } = require("child_process");
const crypto = require("crypto");
const secp = require("@noble/secp256k1");
const { nip19 } = require("nostr-tools");

// @noble/secp256k1 v3 requires sync sha256/hmacSha256
secp.hashes.sha256 = (msg) => new Uint8Array(crypto.createHash("sha256").update(msg).digest());
secp.hashes.hmacSha256 = (key, msg) =>
  new Uint8Array(crypto.createHmac("sha256", key).update(msg).digest());

/**
 * 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).
 */
function callNsigner(targetQube, request) {
  return new Promise((resolve, reject) => {
    const payload = Buffer.from(JSON.stringify(request), "utf8");
    const header = Buffer.alloc(4);
    header.writeUInt32BE(payload.length, 0);
    const framed = Buffer.concat([header, payload]);

    const proc = spawn("qrexec-client-vm", [targetQube, "qubes.NsignerRpc"], {
      stdio: ["pipe", "pipe", "pipe"],
    });

    const stdoutChunks = [];
    const stderrChunks = [];

    proc.stdout.on("data", (chunk) => stdoutChunks.push(chunk));
    proc.stderr.on("data", (chunk) => stderrChunks.push(chunk));

    proc.on("error", (err) => {
      reject(new Error(`failed to spawn qrexec-client-vm: ${err.message}`));
    });

    proc.on("close", (code) => {
      if (code !== 0) {
        const stderr = Buffer.concat(stderrChunks).toString("utf8");
        reject(new Error(`qrexec-client-vm exited with code ${code}: ${stderr.trim()}`));
        return;
      }
      const buf = Buffer.concat(stdoutChunks);
      if (buf.length < 4) {
        reject(new Error("short response (missing frame header)"));
        return;
      }
      const len = buf.readUInt32BE(0);
      const body = buf.subarray(4, 4 + len);
      if (body.length !== len) {
        reject(new Error(`short response payload: expected ${len}, got ${body.length}`));
        return;
      }
      try {
        resolve(JSON.parse(body.toString("utf8")));
      } catch (e) {
        reject(new Error(`failed to parse response: ${e.message}`));
      }
    });

    proc.stdin.write(framed);
    proc.stdin.end();
  });
}

/**
 * Demo 1: Get a public key by nostr_index.
 */
async function demoGetPublicKey(targetQube, nostrIndex) {
  console.log(`\n=== Demo 1: get_public_key (nostr_index=${nostrIndex}) ===`);

  const response = await callNsigner(targetQube, {
    id: "1",
    method: "get_public_key",
    params: [{ nostr_index: nostrIndex }],
  });

  if (response.error) {
    throw new Error(`get_public_key failed: ${JSON.stringify(response.error)}`);
  }

  const pubkeyHex = response.result;
  const npub = nip19.npubEncode(pubkeyHex);
  console.log(`  pubkey hex: ${pubkeyHex}`);
  console.log(`  npub:       ${npub}`);
  return pubkeyHex;
}

/**
 * Demo 2: Sign a Nostr event (kind 1 text note).
 */
async function demoSignEvent(targetQube, nostrIndex, pubkeyHex) {
  console.log("\n=== Demo 2: sign_event (kind 1 text note) ===");

  const unsignedEvent = {
    kind: 1,
    content: "Hello from n_signer JavaScript demo!",
    created_at: Math.floor(Date.now() / 1000),
    tags: [],
    pubkey: pubkeyHex,
  };

  console.log("  Unsigned event:");
  console.log(`    ${JSON.stringify(unsignedEvent)}`);

  const response = await callNsigner(targetQube, {
    id: "2",
    method: "sign_event",
    params: [JSON.stringify(unsignedEvent), { nostr_index: nostrIndex }],
  });

  if (response.error) {
    throw new Error(`sign_event failed: ${JSON.stringify(response.error)}`);
  }

  const signedEvent = JSON.parse(response.result);
  console.log("  Signed event:");
  console.log(`    ${JSON.stringify(signedEvent)}`);
  console.log(`  event id: ${signedEvent.id}`);
  console.log(`  signature: ${signedEvent.sig}`);
  return signedEvent;
}

/**
 * Demo 3: NIP-44 encrypt and decrypt.
 * Encrypts a message to ourselves (using our own pubkey as the peer),
 * then decrypts it to verify round-trip.
 */
async function demoNip44(targetQube, nostrIndex, pubkeyHex) {
  const plaintext = "Secret message from n_signer JavaScript demo!";

  console.log("\n=== Demo 3: nip44_encrypt / nip44_decrypt ===");
  console.log(`  plaintext:  "${plaintext}"`);
  console.log(`  peer pubkey: ${pubkeyHex} (self)`);

  // Encrypt
  const encResponse = await callNsigner(targetQube, {
    id: "3",
    method: "nip44_encrypt",
    params: [pubkeyHex, plaintext, { nostr_index: nostrIndex }],
  });

  if (encResponse.error) {
    throw new Error(`nip44_encrypt failed: ${JSON.stringify(encResponse.error)}`);
  }

  const ciphertext = encResponse.result;
  console.log(`  ciphertext: ${ciphertext}`);

  // Decrypt
  const decResponse = await callNsigner(targetQube, {
    id: "4",
    method: "nip44_decrypt",
    params: [pubkeyHex, ciphertext, { nostr_index: nostrIndex }],
  });

  if (decResponse.error) {
    throw new Error(`nip44_decrypt failed: ${JSON.stringify(decResponse.error)}`);
  }

  const decrypted = decResponse.result;
  console.log(`  decrypted:  "${decrypted}"`);

  if (plaintext === decrypted) {
    console.log("  ✓ Round-trip verified: plaintext matches decrypted");
  } else {
    throw new Error("Round-trip FAILED: plaintext does not match decrypted");
  }
}

async function demoMineEvent(targetQube, nostrIndex) {
  console.log("\n--- Demo 4: mine_event (NIP-13 Proof-of-Work) ---");

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

  console.log("  Mining with difficulty=4, threads=4, timeout_sec=30...");
  const response = await callNsigner(targetQube, {
    id: "5",
    method: "mine_event",
    params: [JSON.stringify(event), {
      difficulty: 4,
      threads: 4,
      timeout_sec: 30,
      nostr_index: nostrIndex,
    }],
  });

  if (response.error) {
    throw new Error(`mine_event failed: ${JSON.stringify(response.error)}`);
  }

  const result = JSON.parse(response.result);
  console.log(`  achieved_difficulty: ${result.achieved_difficulty}`);
  console.log(`  target_reached:      ${result.target_reached}`);
  console.log(`  elapsed_sec:         ${result.elapsed_sec}`);
  console.log(`  attempts:            ${result.attempts}`);

  const minedEvent = JSON.parse(result.event);
  console.log(`  event id:            ${minedEvent.id}`);
  console.log(`  nonce tag:           ${JSON.stringify(minedEvent.tags[0])}`);

  if (result.target_reached) {
    console.log("  ✓ Target difficulty reached!");
  } else {
    console.log(`  (Target not reached, best effort: ${result.achieved_difficulty} bits)`);
  }
}

async function main() {
  const targetQube = process.argv[2] || "nostr_signer";
  const nostrIndex = parseInt(process.argv[3] || "0", 10);

  console.log("=== n_signer JavaScript Demo ===");
  console.log(`Target qube: ${targetQube}`);
  console.log(`Service:     qubes.NsignerRpc`);
  console.log(`nostr_index: ${nostrIndex}`);
  console.log("\nConnecting to n_signer via qrexec...");

  try {
    const pubkeyHex = await demoGetPublicKey(targetQube, nostrIndex);
    await demoSignEvent(targetQube, nostrIndex, pubkeyHex);
    await demoNip44(targetQube, nostrIndex, pubkeyHex);
    await demoMineEvent(targetQube, nostrIndex);

    console.log("\n=== Summary ===");
    console.log("All demos completed successfully.");
  } catch (e) {
    console.error("\n=== Summary ===");
    console.error(`Demo failed: ${e.message}`);
    process.exit(1);
  }
}

main();
