#!/usr/bin/env node
/**
 * n_signer_qube_example.js — connect to a running n_signer over TCP (FIPS mesh)
 * and call get_public_key for nostr_index 0 and 1, printing both hex pubkey
 * and bech32 npub for each.
 *
 * This is a cross-qube test client for Qubes OS: the signer runs in the
 * nostr_signer qube listening on tcp:[::]:8080, and this client runs in
 * a different qube connecting to the signer's FIPS address.
 *
 * Usage:
 *   node n_signer_qube_example.js [host] [port]
 *   node n_signer_qube_example.js fd56:d7c3:f605:719d:15b:18a0:fb06:982f 8080
 *
 * If no arguments are given, defaults to localhost:8080.
 *
 * Protocol:
 *   - 4-byte big-endian length prefix + JSON payload (TCP framing)
 *   - JSON-RPC: {"id":"...","method":"get_public_key","params":[{"nostr_index":N}],"auth":{...}}
 *   - Auth envelope: kind 27235 Nostr event with nsigner_rpc, nsigner_method,
 *     nsigner_body_hash tags. body_hash = sha256(json.dumps(params))
 *   - Response: {"id":"...","result":"<pubkey hex>"} or {"id":"...","error":{...}}
 */

const net = require("net");
const crypto = require("crypto");
const secp = require("@noble/secp256k1");

// @noble/secp256k1 v3 requires us to provide 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());

const { schnorr } = secp;

/** Convert a Uint8Array to a hex string. */
function bytesToHex(bytes) {
  return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
}

/** Convert a hex string to a Uint8Array. */
function hexToBytes(hex) {
  const arr = new Uint8Array(hex.length / 2);
  for (let i = 0; i < arr.length; i++) {
    arr[i] = parseInt(hex.substr(i * 2, 2), 16);
  }
  return arr;
}

// Demo caller key (32 bytes). Replace with your stable caller key in real use.
const DEMO_PRIVKEY = new Uint8Array(Array.from({ length: 32 }, (_, i) => i + 1));

/**
 * Compute the body hash for a params array.
 * This must match n_signer's hashing: sha256 of canonical JSON (no spaces).
 */
function computeBodyHash(params) {
  const canonical = JSON.stringify(params);
  return crypto.createHash("sha256").update(canonical).digest("hex");
}

/**
 * Build a kind-27235 auth envelope for the given method and params.
 * Returns the auth object to include in the JSON-RPC request.
 */
function buildAuthEnvelope(privkey, method, params, requestId) {
  const bodyHash = computeBodyHash(params);
  const pubkeyHex = bytesToHex(schnorr.getPublicKey(privkey));
  const createdAt = Math.floor(Date.now() / 1000);

  // nsigner_rpc tag value MUST match the JSON-RPC request "id" field
  const tags = [
    ["nsigner_rpc", requestId],
    ["nsigner_method", method],
    ["nsigner_body_hash", bodyHash],
  ];

  const content = "js-example";

  // Serialize the event for signing: [0, pubkey, created_at, kind, tags, content]
  const serialized = JSON.stringify([
    0,
    pubkeyHex,
    createdAt,
    27235,
    tags,
    content,
  ]);

  const eventId = crypto.createHash("sha256").update(serialized).digest("hex");
  const sig = bytesToHex(schnorr.sign(hexToBytes(eventId), privkey, new Uint8Array(32)));

  return {
    id: eventId,
    pubkey: pubkeyHex,
    created_at: createdAt,
    kind: 27235,
    tags: tags,
    content: content,
    sig: sig,
  };
}

/**
 * Send a framed JSON-RPC request over a TCP socket and receive the response.
 * Framing: 4-byte big-endian length prefix + JSON payload.
 */
function sendRequest(socket, request) {
  return new Promise((resolve, reject) => {
    const payload = Buffer.from(JSON.stringify(request), "utf8");
    const header = Buffer.alloc(4);
    header.writeUInt32BE(payload.length, 0);
    socket.write(Buffer.concat([header, payload]));

    let headerBuf = Buffer.alloc(0);
    let bodyBuf = Buffer.alloc(0);
    let bodyLen = 0;
    let state = "header";

    const onData = (chunk) => {
      if (state === "header") {
        headerBuf = Buffer.concat([headerBuf, chunk]);
        if (headerBuf.length >= 4) {
          bodyLen = headerBuf.readUInt32BE(0);
          const remaining = headerBuf.subarray(4);
          headerBuf = Buffer.alloc(0);
          state = "body";
          if (remaining.length > 0) {
            bodyBuf = Buffer.concat([bodyBuf, remaining]);
          }
          if (bodyBuf.length >= bodyLen) {
            finish();
          }
        }
      } else if (state === "body") {
        bodyBuf = Buffer.concat([bodyBuf, chunk]);
        if (bodyBuf.length >= bodyLen) {
          finish();
        }
      }
    };

    function finish() {
      socket.off("data", onData);
      socket.off("error", onError);
      const body = bodyBuf.subarray(0, bodyLen).toString("utf8");
      try {
        resolve(JSON.parse(body));
      } catch (e) {
        reject(new Error(`Failed to parse response: ${e.message}`));
      }
    }

    function onError(err) {
      socket.off("data", onData);
      reject(err);
    }

    socket.on("data", onData);
    socket.on("error", onError);
  });
}

/**
 * Query get_public_key for a given nostr_index.
 * Opens a fresh TCP connection for each request (n_signer handles one request
 * per connection).
 */
async function getPublicKey(host, port, nostrIndex, privkey) {
  const params = [{ nostr_index: nostrIndex }];
  const requestId = String(nostrIndex);
  const auth = buildAuthEnvelope(privkey, "get_public_key", params, requestId);

  const request = {
    id: requestId,
    method: "get_public_key",
    params: params,
    auth: auth,
  };

  return new Promise((resolve, reject) => {
    const socket = new net.Socket();
    socket.setTimeout(15000);

    socket.connect(port, host, async () => {
      try {
        const response = await sendRequest(socket, request);
        socket.destroy();
        resolve(response);
      } catch (e) {
        socket.destroy();
        reject(e);
      }
    });

    socket.on("timeout", () => {
      socket.destroy();
      reject(new Error("Connection timed out"));
    });

    socket.on("error", (err) => {
      reject(err);
    });
  });
}

/**
 * Convert a 32-byte hex pubkey to bech32 npub format (NIP-19).
 */
function hexToNpub(pubkeyHex) {
  const { nip19 } = require("nostr-tools");
  return nip19.npubEncode(pubkeyHex);
}

async function main() {
  const host = process.argv[2] || "127.0.0.1";
  const port = parseInt(process.argv[3] || "8080", 10);

  console.log(`Connecting to n_signer at ${host}:${port}`);
  console.log("Querying get_public_key for nostr_index 0 and 1...\n");

  let failures = 0;

  for (const index of [0, 1]) {
    // Small delay between requests to avoid auth nonce collision
    // (the auth envelope uses created_at as part of the nonce)
    if (index > 0) await new Promise((r) => setTimeout(r, 1100));
    try {
      const response = await getPublicKey(host, port, index, DEMO_PRIVKEY);

      if (response.error) {
        console.log(`index ${index}: ERROR: ${JSON.stringify(response.error)}`);
        failures++;
        continue;
      }

      const pubkeyHex = response.result;
      const npub = hexToNpub(pubkeyHex);
      console.log(`index ${index}: hex=${pubkeyHex}  npub=${npub}`);
    } catch (e) {
      console.log(`index ${index}: FAILED - ${e.message}`);
      failures++;
    }
  }

  if (failures > 0) {
    console.log(`\n${failures} query(s) failed`);
    process.exit(1);
  }

  console.log("\nAll queries succeeded");
}

main().catch((e) => {
  console.error(e);
  process.exit(1);
});
