/**
 * Which migration this box serves — California's, or none.
 *
 * The same shape as `fires/index.ts` and `radar/index.ts`. One body, one cache
 * key, no region parameter: the whole state's granule is 58 rows and every board
 * is a rectangle inside it, so a server that filtered by board would be doing a
 * worse version of a clip the client has to do anyway (`promoteBirds` in
 * `src/server/birds.ts`).
 *
 * ### The empty body carries a reason, and that is the difference from fires
 *
 * A fire body with nothing in it needs no explanation beyond its own age:
 * nothing is burning. An empty *migration* body needs one, because the layer is
 * absent about fourteen hours in every twenty-four by construction and a viewer
 * arriving at noon would otherwise read a working feed as a broken one. So
 * `quiet` is not optional on the wire and is filled in here even for a box with
 * no source at all.
 *
 * ### `none` is a quiet sky, not an invented flock
 *
 * The same asymmetry `fires/index.ts` argues. An invented aeroplane is a
 * plausible aeroplane; three hundred thousand invented birds over a named county
 * is a claim about a real place. So the synthetic case is the honest quiet one —
 * which is also what keeps `scripts/check-zero-config-boot.mjs` green with
 * `TERA_BIRDS_SOURCE` unset.
 */

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

export interface BirdsService {
  current(): Promise<BirdsBody>;
}

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

const CALIFORNIA_KEY = "california";

/**
 * Five minutes.
 *
 * Granules are ten minutes apart and the collector reads them in batches about
 * twenty minutes behind, so anything faster is two machines' work for identical
 * bytes. Floored for the reason every other feed here is floored:
 * `TERA_BIRDS_TTL=0` reads like "fresh" and means "one fetch pair per request".
 */
const MIN_TTL_SECONDS = 300;

/**
 * What a box with no source answers with.
 *
 * Note the `quiet` reason: `no-data`, not `daylight`. Nobody has looked, which
 * is a different thing from having looked and found the sun up, and the panel
 * says so in words rather than showing the same blank for both.
 */
function emptyBody(ttlSeconds: number): BirdsBody {
  return {
    source: "none",
    fetchedAt: new Date(0).toISOString(),
    observedAt: null,
    counties: [],
    statewide: null,
    quiet: {
      reason: "no-data",
      message:
        "No migration feed is configured on this box, so nothing is drawn over " +
        "the state. That is a fact about this box, not about the sky.",
    },
    ttlSeconds,
  };
}

export function createBirdsService(config: Config, log: BirdsLog): BirdsService {
  const { source, url, key, ttlSeconds } = config.birds;
  const ttl = Math.max(MIN_TTL_SECONDS, ttlSeconds);

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

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

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

      // Whether the sky is quiet is decided in the browser, where the sun's
      // elevation is already known to a hundredth of a degree
      // (`atmosphere.ts`). The server would have to compute a second opinion
      // from a clock the scrubber does not own, and the two would disagree on
      // exactly the frames that matter. So `quiet` is `null` here whenever
      // anything at all arrived, and `promoteBirds` fills it in.
      const quiet: BirdsBody["quiet"] =
        snapshot.counties.length > 0
          ? null
          : {
              reason: "no-data",
              message:
                "The migration feed answered with no counties in it. " +
                "Either the newest granule has not landed yet or nothing was measured.",
            };

      return {
        source,
        fetchedAt: new Date(snapshot.fetchedAt).toISOString(),
        observedAt: snapshot.observedAt === "" ? null : snapshot.observedAt,
        counties: snapshot.counties,
        statewide: snapshot.statewide,
        quiet,
        ttlSeconds: ttl,
        ...(snapshot.attribution.length > 0 ? { attribution: snapshot.attribution } : {}),
      };
    },
  };
}
