// qID Connect express demo
//
// A complete site where register AND login are wallet-only. There is no email
// field, no password, no signup form anywhere. The first successful sign-in
// IS the registration: the account is created from the verified address.
//
//   bun server.js
//   open http://localhost:8788
//
// To sign in without a wallet build, generate a proof with the test signer:
//   bun ../../tools/signer/btx-sign-ownership.mjs --random \
//     --origin http://localhost:8788 --nonce <nonce> --ts <ts>
//
// SPDX-License-Identifier: MIT

import express from "express";
import { readFileSync, writeFileSync, existsSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
import { randomBytes } from "node:crypto";

import { createQidConnect } from "@qid/connect-server";
import { qidMiddleware, requireQidSession } from "@qid/connect-server/express";

const here = dirname(fileURLToPath(import.meta.url));
const PORT = Number(process.env.PORT || 8788);
const ORIGIN = process.env.QID_ORIGIN || `http://localhost:${PORT}`;

// Accounts persisted to a JSON file so restarts keep them. The address is the
// primary key; displayName is the only profile field, and it is optional.
// This is deliberately the entire account model.
class JsonFileAccounts {
  constructor(file) {
    this.file = file;
    this.map = new Map(
      existsSync(file) ? Object.entries(JSON.parse(readFileSync(file, "utf8"))) : []
    );
  }
  _save() {
    writeFileSync(this.file, JSON.stringify(Object.fromEntries(this.map), null, 2));
  }
  async getOrCreate(address, now = Date.now()) {
    let acct = this.map.get(address);
    if (!acct) {
      acct = { address, displayName: null, createdAt: now };
      this.map.set(address, acct);
      this._save();
    }
    return acct;
  }
  async get(address) {
    return this.map.get(address) || null;
  }
  async setDisplayName(address, name) {
    const acct = this.map.get(address);
    if (!acct) return null;
    acct.displayName = name;
    this._save();
    return acct;
  }
}

const accounts = new JsonFileAccounts(join(here, "accounts.json"));

const qid = createQidConnect({
  origin: ORIGIN,
  // In production, set QID_SESSION_SECRET in the environment. The demo makes
  // one per run, which just means sessions reset when the server restarts.
  sessionSecret: process.env.QID_SESSION_SECRET || randomBytes(32).toString("hex"),
  accounts,
});

const app = express();
app.use(express.json({ limit: "64kb" }));

// The whole login system is this one line.
app.use("/qid", qidMiddleware(qid));

// A protected API route: req.qidSession.address is the verified user.
app.get("/api/me", requireQidSession(qid), (req, res) => {
  res.json({ ok: true, ...req.qidSession });
});

app.post("/api/profile", requireQidSession(qid), async (req, res) => {
  const name = typeof req.body?.displayName === "string" ? req.body.displayName.trim() : "";
  if (name.length > 40) return res.status(400).json({ ok: false, reason: "name_too_long" });
  const account = await accounts.setDisplayName(req.qidSession.address, name || null);
  res.json({ ok: true, account });
});

// Serve the widget straight from the workspace (two files: the widget and
// its vendored QR encoder).
app.get("/qid-connect-widget.js", (_req, res) => {
  res.type("application/javascript");
  res.send(readFileSync(join(here, "../../packages/widget/src/qid-connect-widget.js")));
});
app.get("/vendor/qrcode.mjs", (_req, res) => {
  res.type("application/javascript");
  res.send(readFileSync(join(here, "../../packages/widget/src/vendor/qrcode.mjs")));
});

app.use(express.static(join(here, "public")));

app.listen(PORT, () => {
  console.log(`qID Connect demo running at ${ORIGIN}`);
  console.log("No email. No password. The address is the account.");
});
