#!/bin/sh
":" //# ; if command -v bun >/dev/null 2>&1; then exec bun "$0" "$@"; elif command -v deno >/dev/null 2>&1; then exec deno run -A "$0" "$@"; else exec node "$0" "$@"; fi

// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2026 Jake Lazaroff https://tangled.org/jakelazaroff.com/tinylex

import { writeFile, mkdir } from "node:fs/promises";
import { dirname, join } from "node:path";

async function resolveTxt(host) {
  const r = await fetch(
    `https://cloudflare-dns.com/dns-query?name=${encodeURIComponent(host)}&type=TXT`,
    { headers: { accept: "application/dns-json" } }
  );
  if (!r.ok) throw new Error(`doh ${r.status}`);
  const { Answer = [] } = await r.json();
  return Answer.filter(a => a.type === 16).map(a => {
    const chunks = [];
    const re = /"((?:[^"\\]|\\.)*)"/g;
    let m;
    while ((m = re.exec(a.data))) chunks.push(m[1].replace(/\\(.)/g, "$1"));
    return chunks;
  });
}

const argv = typeof Deno !== "undefined" ? Deno.args : process.argv.slice(2);
const nsids = [];
let outDir = "lexicons";
let asJs = false;

for (let i = 0; i < argv.length; i++) {
  const a = argv[i];
  if (a === "--out" || a === "-o") outDir = argv[++i];
  else if (a.startsWith("--out=")) outDir = a.slice(6);
  else if (a === "--js") asJs = true;
  else nsids.push(a);
}
if (!nsids.length) {
  console.error("usage: ./install <nsid>... [--out <dir>] [--js]");
  process.exit(1);
}

function authorityDomain(nsid) {
  const segs = nsid.split(".");
  if (segs.length < 3 || segs.some(s => !/^[a-z][a-z0-9-]*$/i.test(s))) {
    throw new Error(`invalid nsid: ${nsid}`);
  }
  return segs.slice(0, -1).reverse().join(".");
}

async function resolveDid(nsid) {
  const host = `_lexicon.${authorityDomain(nsid)}`;
  const records = await resolveTxt(host);
  for (const chunks of records) {
    const m = chunks.join("").match(/^did=(.+)$/);
    if (m) return m[1];
  }
  throw new Error(`no did= TXT at ${host}`);
}

async function resolvePds(did) {
  let doc;
  if (did.startsWith("did:plc:")) {
    const r = await fetch(`https://plc.directory/${did}`);
    if (!r.ok) throw new Error(`plc ${r.status}`);
    doc = await r.json();
  } else if (did.startsWith("did:web:")) {
    const host = did.slice(8).replace(/:/g, "/");
    const r = await fetch(`https://${host}/.well-known/did.json`);
    if (!r.ok) throw new Error(`did:web ${r.status}`);
    doc = await r.json();
  } else {
    throw new Error(`unsupported did method: ${did}`);
  }
  const svc = (doc.service || []).find(
    s => s.id === "#atproto_pds" || s.type === "AtprotoPersonalDataServer"
  );
  if (!svc) throw new Error("no atproto_pds service in did doc");
  return svc.serviceEndpoint.replace(/\/$/, "");
}

async function fetchLexicon(nsid) {
  const did = await resolveDid(nsid);
  const pds = await resolvePds(did);
  const url = `${pds}/xrpc/com.atproto.repo.getRecord`
    + `?repo=${encodeURIComponent(did)}`
    + `&collection=com.atproto.lexicon.schema`
    + `&rkey=${encodeURIComponent(nsid)}`;
  const r = await fetch(url);
  if (!r.ok) throw new Error(`getRecord ${r.status}`);
  const { value } = await r.json();
  return value;
}

let failed = 0;
for (const nsid of nsids) {
  try {
    const lex = await fetchLexicon(nsid);
    const out = join(outDir, `${nsid}.${asJs ? "js" : "json"}`);
    const body = asJs
      ? `export default /** @type {const} */ (${JSON.stringify(lex, null, 2)});\n`
      : JSON.stringify(lex, null, 2) + "\n";
    await mkdir(dirname(out), { recursive: true });
    await writeFile(out, body);
    console.log(`ok    ${nsid}  ->  ${out}`);
  } catch (e) {
    console.error(`fail  ${nsid}  (${e.message})`);
    failed++;
  }
}
process.exit(failed ? 1 : 0);
