#!/usr/bin/env node
/**
 * One-shot Rubytech-side entitlement fixture generator.
 *
 * RUN BY FOUNDER ONCE; NEVER ON CUSTOMER DEVICES.
 *
 * Produces:
 *   - platform/lib/entitlement/rubytech-pubkey.pem      (vendored, committed)
 *   - platform/lib/entitlement/PUBKEY-HASH.txt          (sha256 of pubkey, committed)
 *   - .secrets/rubytech-private-key.pem                  (LOCAL ONLY — never commit)
 *   - <out-dir>/entitlement.json                         (signed payload fixture)
 *
 * Usage:
 *   node platform/scripts/generate-entitlement-fixture.mjs init
 *     → generates a fresh keypair + writes the pubkey + hash
 *
 *   node platform/scripts/generate-entitlement-fixture.mjs sign \
 *        --account-id <id> --email <email> --tier <solo|family|pro> \
 *        --plugins <comma-list> --days <n> --out <path>
 *     → signs a payload using the existing private key
 *
 * The verifier at platform/lib/entitlement/src/index.ts has PUBKEY_SHA256
 * baked into source. After running `init` the script prints the new hash —
 * paste it into the verifier and rebuild.
 *
 * (Rubytech-side issuance endpoint), this script will be
 * replaced by a server-side equivalent. Until then, it stands in.
 */

import {
  generateKeyPairSync,
  createPublicKey,
  createPrivateKey,
  createHash,
  sign as cryptoSign,
} from "node:crypto";
import { writeFileSync, readFileSync, existsSync, mkdirSync } from "node:fs";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const PLATFORM_ROOT = resolve(__dirname, "..");
const REPO_ROOT = resolve(PLATFORM_ROOT, "..");
const PUBKEY_PATH = resolve(PLATFORM_ROOT, "lib", "entitlement", "rubytech-pubkey.pem");
const HASH_PATH = resolve(PLATFORM_ROOT, "lib", "entitlement", "PUBKEY-HASH.txt");
const SECRET_DIR = resolve(REPO_ROOT, ".secrets");
const PRIVKEY_PATH = resolve(SECRET_DIR, "rubytech-private-key.pem");

function canonicalize(value) {
  if (value === null) return "null";
  if (typeof value === "boolean") return value ? "true" : "false";
  if (typeof value === "string") return JSON.stringify(value);
  if (typeof value === "number") {
    if (!Number.isFinite(value)) throw new Error("non-finite number");
    if (Object.is(value, -0)) throw new Error("negative zero");
    return JSON.stringify(value);
  }
  if (Array.isArray(value)) return "[" + value.map(canonicalize).join(",") + "]";
  if (typeof value === "object") {
    const keys = Object.keys(value).sort();
    return "{" + keys.map((k) => JSON.stringify(k) + ":" + canonicalize(value[k])).join(",") + "}";
  }
  throw new Error(`unsupported value type ${typeof value}`);
}

function init() {
  if (existsSync(PRIVKEY_PATH)) {
    console.error(`ABORT: private key already exists at ${PRIVKEY_PATH}.`);
    console.error("Delete it manually if you really want to regenerate (ALL EXISTING SIGNED PAYLOADS WILL BREAK).");
    process.exit(1);
  }
  const { publicKey, privateKey } = generateKeyPairSync("ed25519");
  const pubPem = publicKey.export({ type: "spki", format: "pem" });
  const privPem = privateKey.export({ type: "pkcs8", format: "pem" });

  if (!existsSync(SECRET_DIR)) mkdirSync(SECRET_DIR, { recursive: true });
  writeFileSync(PRIVKEY_PATH, privPem, { mode: 0o600 });
  writeFileSync(PUBKEY_PATH, pubPem, "utf-8");

  const hash = createHash("sha256").update(pubPem).digest("hex");
  writeFileSync(HASH_PATH, hash + "\n", "utf-8");

  console.log(`Generated keypair.`);
  console.log(`  Public key  : ${PUBKEY_PATH}`);
  console.log(`  Hash file   : ${HASH_PATH}  (${hash})`);
  console.log(`  Private key : ${PRIVKEY_PATH}  (DO NOT COMMIT — verify .gitignore)`);
  console.log("");
  console.log(`NEXT: paste the hash into platform/lib/entitlement/src/index.ts:`);
  console.log(`  export const PUBKEY_SHA256 = "${hash}";`);
  console.log(`Then run npm --prefix platform run build:lib`);
}

function parseArgs(argv) {
  const out = {};
  for (let i = 0; i < argv.length; i += 2) {
    const key = argv[i].replace(/^--/, "");
    out[key] = argv[i + 1];
  }
  return out;
}

function sign(args) {
  const required = ["account-id", "email", "tier", "out"];
  for (const k of required) {
    if (!args[k]) {
      console.error(`Missing --${k}`);
      process.exit(1);
    }
  }
  if (!existsSync(PRIVKEY_PATH)) {
    console.error(`Private key missing at ${PRIVKEY_PATH}. Run 'init' first.`);
    process.exit(1);
  }
  const days = Number(args.days ?? "365");
  const plugins = (args.plugins ?? "")
    .split(",")
    .map((s) => s.trim())
    .filter(Boolean);

  const now = Date.now();
  const issued = new Date(now).toISOString();
  const expires = new Date(now + days * 86400 * 1000).toISOString();

  const payload = {
    accountId: args["account-id"],
    customerEmail: args.email,
    tier: args.tier,
    purchasedPlugins: plugins,
    issued,
    expires,
  };
  const canonical = canonicalize(payload);
  const privKey = createPrivateKey(readFileSync(PRIVKEY_PATH, "utf-8"));
  const sigBuf = cryptoSign(null, Buffer.from(canonical, "utf-8"), privKey);
  const envelope = { payload, signature: sigBuf.toString("base64") };
  writeFileSync(args.out, JSON.stringify(envelope, null, 2) + "\n", "utf-8");
  console.log(`Signed payload written to ${args.out}`);
  console.log(`  tier=${payload.tier} plugins=${plugins.length} expires=${expires}`);
}

const cmd = process.argv[2];
if (cmd === "init") {
  init();
} else if (cmd === "sign") {
  sign(parseArgs(process.argv.slice(3)));
} else {
  console.error("Usage:");
  console.error("  generate-entitlement-fixture.mjs init");
  console.error("  generate-entitlement-fixture.mjs sign --account-id <id> --email <e> --tier <t> [--plugins a,b] [--days N] --out <path>");
  process.exit(1);
}
