/**
 * Which reflectivity this box serves — which is California's, or none.
 *
 * The same shape as `fires/index.ts`, and it takes the same decisions for the
 * same reasons: one body, one cache key, no region parameter, a TTL with a
 * floor, and `none` serving a real empty body rather than an invented shower.
 *
 * ### The lattice is built here, on the server, and that is deliberate
 *
 * `RadarBody.field` is a lattice rather than a list of cells, so somebody has to
 * turn one into the other. Doing it here means the anomalous-propagation rule,
 * the coverage-hole rule and the promotion threshold are applied **once, in one
 * place, by the module that can be tested without a browser** — and it means a
 * quiet day costs the wire an empty body rather than 1,596 texels' worth of JSON
 * that the client would then throw away. `src/server/radar.ts` is imported by
 * both ends precisely so there is one statement about the data and not two.
 *
 * The board's own coastline comes from the city pack, which is data and types
 * and imports no renderer. That is the same arrangement `media/bindings.ts`
 * already uses for the office packs, and it is what keeps the land/sea test from
 * being a second, drifting copy of the shoreline.
 *
 * ### `none` serves an empty sky and never an invented one
 *
 * The flights service falls back to a simulated plan, because an empty sky over
 * a city reads as a bug. Nothing here does. An invented aeroplane is a plausible
 * aeroplane; an invented thunderstorm is a claim that it is raining on somebody
 * who can look out of the window. So the synthetic case *is* the clear sky —
 * `field: null`, `source: "none"`, `fetchedAt` at the Unix epoch — which is
 * exactly what `weather/synthetic.ts` does when it answers with
 * `precipitation: 0`, and it is what keeps `scripts/check-zero-config-boot.mjs`
 * green on a keyless clone.
 *
 * `fetchedAt` is the epoch and not "now", because "now" would be a claim that
 * this was fetched a moment ago. It was never fetched, and the board says so.
 */

import { fetchProjection, type RadarSnapshot } from "./cloud1.ts";
import { createUpstream } from "../upstream.ts";
import { buildRadarField } from "../../../src/server/radar.ts";
import CALIFORNIA_CITY from "../../../src/cities/california.ts";
import type { Config } from "../config.ts";
import type { RadarBody } from "../../../src/server/wire.ts";

export interface RadarService {
  current(): Promise<RadarBody>;
}

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

/** One state, one key. See `fires/index.ts` for the argument. */
const CALIFORNIA_KEY = "california";

/**
 * Two minutes.
 *
 * The composite advances every ten minutes and reaches the collector about five
 * minutes late, so asking faster than this spends two machines' work to receive
 * identical bytes. The floor exists because `TERA_RADAR_TTL=0` reads like "as
 * fresh as possible" and means "one fetch pair per inbound request" — the trap
 * `TERA_FLIGHTS_TTL=0` and `TERA_FIRES_TTL=0` both were.
 */
const MIN_TTL_SECONDS = 120;

function emptyBody(ttlSeconds: number): RadarBody {
  return {
    source: "none",
    fetchedAt: new Date(0).toISOString(),
    field: null,
    ttlSeconds,
  };
}

export function createRadarService(config: Config, log: RadarLog): RadarService {
  const { source, url, key, ttlSeconds } = config.radar;
  const ttl = Math.max(MIN_TTL_SECONDS, ttlSeconds);

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

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

      const snapshot = await upstream.get(CALIFORNIA_KEY, () => fetchProjection(url, key, log));
      if (snapshot === null) return emptyBody(ttl);

      const built = buildRadarField({
        cells: snapshot.cells,
        stations: snapshot.stations,
        bounds: CALIFORNIA_CITY.bounds,
        coast: CALIFORNIA_CITY.landmasses,
        observedAt: snapshot.observedAt,
        wetFraction: snapshot.wetFraction,
      });

      return {
        source,
        fetchedAt: new Date(snapshot.fetchedAt).toISOString(),
        // `null` on a quiet day, which is nine days in ten and is not a fault.
        // The age beside it is what tells a viewer apart from a dead feed.
        field: built.field,
        ttlSeconds: ttl,
        ...(snapshot.attribution.length > 0 ? { attribution: snapshot.attribution } : {}),
      };
    },
  };
}
