/**
 * The sync oneshot — the only second process, and the only holder of a credential.
 *
 * It runs on a timer, asks Workie for the public marker set, puts every row
 * through the same gate the API serves behind, and writes a snapshot the API
 * reads off disk. The isolation is the point: the public box never holds a
 * Workie token, never opens an outbound connection to a private service, and a
 * compromise of it yields a file that was already public. CONTRACT.md §5.
 *
 * ### Provenance is not optional
 *
 * Every row must say where its coordinate came from, and the gate refuses
 * anything whose answer is not on the non-ODbL allowlist. A row that arrives
 * without a provenance is refused rather than assumed — `TERA_SYNC_PROVENANCE`
 * exists so an operator can *assert* one for a feed that predates the field, and
 * setting it is a licence claim they are making on the record.
 *
 * This is fail-closed by design: **one refused row aborts the whole sync and
 * leaves the previous snapshot in place.** Publishing coordinates whose licence
 * nobody can vouch for is the failure that cannot be undone by a later fix,
 * because ODbL §4.4 would already have attached to everything served alongside
 * them. A stale map is a much cheaper mistake. CONTRACT.md §8.
 *
 * Usage:  TERA_SYNC_SOURCE_URL=... TERA_SYNC_TOKEN=... TERA_MARKERS_FILE=... npm run sync
 */

import { rename, writeFile } from "node:fs/promises";
import { getJson } from "../http.ts";
import { assertPublicShape } from "../markers/gate.ts";
import { DEFAULT_PROVENANCE_ALLOWLIST } from "../config.ts";

interface UpstreamBody {
  markers?: unknown;
  generatedAt?: string;
}

async function main(): Promise<number> {
  const sourceUrl = process.env["TERA_SYNC_SOURCE_URL"] ?? "";
  const token = process.env["TERA_SYNC_TOKEN"] ?? "";
  const target = process.env["TERA_MARKERS_FILE"] ?? "";
  const stamp = process.env["TERA_SYNC_PROVENANCE"] ?? "";
  const allowlist = (process.env["TERA_MARKERS_PROVENANCE_ALLOWLIST"] ?? "")
    .split(",")
    .map((s) => s.trim())
    .filter((s) => s !== "");

  if (sourceUrl === "" || target === "") {
    console.error("sync: TERA_SYNC_SOURCE_URL and TERA_MARKERS_FILE are both required.");
    return 2;
  }

  const body = await getJson<UpstreamBody>(sourceUrl, {
    headers: token === "" ? {} : { authorization: `Bearer ${token}` },
    timeoutMs: 20_000,
  });
  if (body === null) {
    console.error(`sync: ${sourceUrl} did not answer; leaving the snapshot alone.`);
    return 1;
  }

  const rows = Array.isArray(body.markers) ? body.markers : [];
  const stamped = stamp === "" ? rows : rows.map((row) => withProvenance(row, stamp));

  const { accepted, refused } = assertPublicShape(
    stamped,
    allowlist.length > 0 ? allowlist : DEFAULT_PROVENANCE_ALLOWLIST,
  );

  if (refused.length > 0) {
    for (const entry of refused) console.error(`sync: REFUSED ${entry.count} row(s) — ${entry.reason}`);
    console.error("sync: aborting without writing. The previous snapshot is untouched.");
    return 1;
  }

  const snapshot = {
    generatedAt: body.generatedAt ?? new Date().toISOString(),
    markers: accepted,
  };

  // Write beside the target and rename, so the API can never read a half-written
  // file — `rename` within a directory is atomic and the reader has no lock.
  const temp = `${target}.tmp`;
  await writeFile(temp, `${JSON.stringify(snapshot, null, 2)}\n`, "utf8");
  await rename(temp, target);

  console.log(`sync: wrote ${accepted.length} marker(s) to ${target}`);
  return 0;
}

/** Assert a provenance on rows that do not carry one. Never overwrites. */
function withProvenance(row: unknown, provenance: string): unknown {
  if (row === null || typeof row !== "object" || Array.isArray(row)) return row;
  const record = row as Record<string, unknown>;
  return "provenance" in record ? record : { ...record, provenance };
}

process.exitCode = await main();
