/**
 * The public-shape gate — the last thing between a synced row and the internet.
 *
 * Two checks, and the second one is the one CONTRACT.md §8 exists for.
 *
 * **Fields.** Only the names on `PUBLIC_MARKER_FIELDS` may cross. A row carrying
 * anything else is refused whole rather than trimmed: an unrecognised field
 * means the upstream shape changed without anyone reviewing it, and quietly
 * dropping it turns a thing somebody should look at into a thing nobody ever
 * sees. This is the same fail-closed rule the fleet's `export-site.ts` already
 * applies, restated here because this is a different process.
 *
 * **Provenance.** A row whose coordinate came from a source not on the allowlist
 * is refused. Serving a snapshot of OSM-derived coordinates is Public Use of a
 * Derivative Database, which brings ODbL §4.3 attribution and §4.4 share-alike
 * onto everything served alongside it — regardless of where the rows are stored,
 * which is exactly the reasoning ARCHITECTURE.md §3.2 originally got wrong.
 * Google, Mapbox and HERE do not help either: their terms restrict storing and
 * redistributing returned coordinates, which is what a public snapshot is. The
 * sanctioned geocoder is the US Census Geocoder, whose output is a US government
 * work in the public domain.
 *
 * The gate refuses rows; it never repairs them. Anything it drops is a bug in
 * the sync oneshot, and the refusal counts are served on the wire so that the
 * bug is visible from outside rather than only in a log nobody tails.
 */

import type { WireMarker } from "../../../src/server/wire.ts";

/**
 * Every field a marker may carry over the public wire.
 *
 * This tracks `Marker` in `src/engine/types.ts` plus `provenance`. When that
 * type gains a field, this list has to gain it too — and until it does, rows
 * carrying the new field are refused, which is the correct direction for a list
 * whose job is to be conservative.
 */
export const PUBLIC_MARKER_FIELDS = [
  "id",
  "label",
  "colorKey",
  "url",
  "blurb",
  "lat",
  "lng",
  "located",
  "provenance",
];

export interface GateResult {
  accepted: WireMarker[];
  /** Aggregated so a broken sync produces one line, not ten thousand. */
  refused: { reason: string; count: number }[];
}

export function assertPublicShape(rows: unknown, provenanceAllowlist: string[]): GateResult {
  const accepted: WireMarker[] = [];
  const refusals = new Map<string, number>();
  const refuse = (reason: string): void => {
    refusals.set(reason, (refusals.get(reason) ?? 0) + 1);
  };

  if (!Array.isArray(rows)) {
    return { accepted, refused: [{ reason: "snapshot is not an array of markers", count: 1 }] };
  }

  for (const row of rows) {
    if (row === null || typeof row !== "object" || Array.isArray(row)) {
      refuse("row is not an object");
      continue;
    }
    const record = row as Record<string, unknown>;

    const unknown = Object.keys(record).find((key) => !PUBLIC_MARKER_FIELDS.includes(key));
    if (unknown !== undefined) {
      refuse(`unknown field "${unknown}"`);
      continue;
    }

    const provenance = record["provenance"];
    if (typeof provenance !== "string" || !provenanceAllowlist.includes(provenance)) {
      refuse(
        `provenance ${JSON.stringify(provenance)} is not on the non-ODbL allowlist ` +
          `(${provenanceAllowlist.join(", ")})`,
      );
      continue;
    }

    const marker = readMarker(record, provenance);
    if (marker === null) {
      refuse("missing or malformed id, label, colorKey, lat or lng");
      continue;
    }
    accepted.push(marker);
  }

  return {
    accepted,
    refused: [...refusals].map(([reason, count]) => ({ reason, count })),
  };
}

function readMarker(record: Record<string, unknown>, provenance: string): WireMarker | null {
  const id = record["id"];
  const label = record["label"];
  const colorKey = record["colorKey"];
  const lat = record["lat"];
  const lng = record["lng"];

  if (typeof id !== "string" || id === "") return null;
  if (typeof label !== "string") return null;
  if (typeof colorKey !== "string") return null;
  if (!isDegrees(lat, 90) || !isDegrees(lng, 180)) return null;

  const marker: WireMarker = { id, label, colorKey, lat, lng, provenance };

  const url = record["url"];
  if (typeof url === "string") marker.url = url;
  const blurb = record["blurb"];
  if (typeof blurb === "string") marker.blurb = blurb;
  const located = record["located"];
  if (typeof located === "boolean") marker.located = located;

  return marker;
}

function isDegrees(value: unknown, limit: number): value is number {
  return typeof value === "number" && Number.isFinite(value) && Math.abs(value) <= limit;
}
