/**
 * Which fires this box serves — which is either California's or none.
 *
 * The same shape as `satellites/index.ts`, and it takes the same two decisions
 * for the same reasons, so it is worth naming them rather than leaving them to
 * be inferred from the code.
 *
 * ### One body, one cache key, no region parameter
 *
 * A weather body is per region because a rain shower over Oakland says nothing
 * about Long Beach. A fire body is not, and the arithmetic is on its side: the
 * whole state's live incident set is small — five drawable fires on the day this
 * was written, seventy-five rows before the gate — and the boards this build
 * draws are rectangles *inside* that set. A server that filtered by board would
 * be doing a worse version of a clip the client has to do anyway (`promote()` in
 * `src/server/fires.ts`), and it would trade the one property that makes this
 * cheap: one body, every viewer, no variation, one key.
 *
 * It also keeps the key space bounded by the environment rather than by the
 * caller, which is the invariant `upstream.ts` is written against.
 *
 * ### Ten minutes, floored at five
 *
 * The collector upstream runs on a ten-minute cron, and the endpoint in front of
 * it holds its own sixty-second cache. Asking faster than the data changes
 * spends two machines' work to receive identical bytes. The floor exists because
 * `TERA_FIRES_TTL=0` reads like "as fresh as possible" and means "one fetch pair
 * per inbound request" — the same trap `TERA_FLIGHTS_TTL=0` and
 * `TERA_SATELLITES_TTL=0` both were.
 *
 * ### `none` serves an empty body and never a synthetic fire
 *
 * The flights service falls back to a simulated plan, because an empty sky over
 * a city reads as a bug. Nothing here does, and the asymmetry is the same one
 * `satellites/index.ts` argues: an invented aeroplane is a plausible aeroplane,
 * and an invented wildfire is a claim that a named place is burning, made to
 * somebody who may live there.
 *
 * `fetchedAt` on the empty body is the Unix epoch rather than "now", because
 * "now" would be a claim that this was fetched a moment ago. It was never
 * fetched, and the board says so in words.
 */

import { fetchProjection, type FiresSnapshot } from "./cloud1.ts";
import { createUpstream } from "../upstream.ts";
import type { Config } from "../config.ts";
import type { FiresBody } from "../../../src/server/wire.ts";

export interface FiresService {
  current(): Promise<FiresBody>;
}

export interface FiresLog {
  warn(msg: string): void;
}

/** See the note above. One state, one key. */
const CALIFORNIA_KEY = "california";

/**
 * Five minutes. Half the collector's own cadence, which is already twice as
 * often as anything can change.
 */
const MIN_TTL_SECONDS = 300;

/** Bounds on what may be asked of the projection, so one bad env cannot flood it. */
const MIN_WINDOW_HOURS = 1;
const MAX_WINDOW_HOURS = 48;

function emptyBody(ttlSeconds: number): FiresBody {
  return {
    source: "none",
    fetchedAt: new Date(0).toISOString(),
    latestSeen: null,
    incidents: [],
    detections: [],
    detectionWindowHours: 0,
    ttlSeconds,
  };
}

export function createFiresService(config: Config, log: FiresLog): FiresService {
  const { source, url, key, ttlSeconds, detectionWindowHours } = config.fires;
  const ttl = Math.max(MIN_TTL_SECONDS, ttlSeconds);
  const windowHours = Math.min(
    MAX_WINDOW_HOURS,
    Math.max(MIN_WINDOW_HOURS, Math.round(detectionWindowHours)),
  );

  const upstream = createUpstream<FiresSnapshot>({
    label: "fires:cloud1",
    ttlSeconds: ttl,
    log,
  });

  return {
    async current(): Promise<FiresBody> {
      if (source === "none") return emptyBody(ttl);

      const snapshot = await upstream.get(CALIFORNIA_KEY, () =>
        fetchProjection(url, key, windowHours, log),
      );
      // Never once answered. An empty body with an epoch-zero `fetchedAt`, which
      // is what makes "the feed is dead" and "nothing is burning" tell apart on
      // the board rather than in the log.
      if (snapshot === null) return emptyBody(ttl);

      return {
        source,
        fetchedAt: new Date(snapshot.fetchedAt).toISOString(),
        latestSeen: snapshot.latestSeen,
        incidents: snapshot.incidents,
        detections: snapshot.detections,
        detectionWindowHours: snapshot.detectionWindowHours,
        ttlSeconds: ttl,
        ...(snapshot.attribution.length > 0 ? { attribution: snapshot.attribution } : {}),
      };
    },
  };
}
