/**
 * The HTTP contract: every body that crosses between the browser build and the
 * Tera API, and nothing else.
 *
 * **This file is types only.** It compiles to nothing, which is the whole point.
 * The browser can import it without paying for a runtime module, and the server
 * can import it without the browser package becoming one of its dependencies —
 * so one declaration of each body serves both sides and there is no second
 * implementation to drift. Everything that imports from here must use
 * `import type`, which `verbatimModuleSyntax` already enforces.
 *
 * Two types here deliberately mirror engine types rather than importing them:
 * `WireSimRoute` is structurally `SimRoute` from `engine/flights.ts`. The server
 * must be able to build one and it must not pull three.js in to do it, so the
 * shape is restated. `WireMarker`, by contrast, genuinely *extends* `Marker`,
 * because a marker off the wire is handed straight to `setMarkers()` and the two
 * being the same type is what guarantees that stays true.
 *
 * Routes, all under `/api/v1`:
 *
 * | route            | body            | cacheable |
 * | ---------------- | --------------- | --------- |
 * | `GET /health`    | `HealthBody`    | no        |
 * | `GET /flights`   | `FlightsBody`   | yes       |
 * | `GET /satellites` | `SatellitesBody` | yes    |
 * | `GET /weather`   | `WeatherBody`   | yes       |
 * | `GET /markers`   | `MarkersBody`   | yes       |
 * | `GET /offices/:id` | `OfficeDoc`   | public offices only |
 * | `GET /offices/:id/presence` | `PresenceBody` | never |
 *
 * See CONTRACT.md §5.
 */

import type { Marker, SatelliteGroup } from "../engine/types.ts";
import type { Office, Presence } from "../interiors/types.ts";

export type { SatelliteGroup };

/** Path prefix every route lives under. Stated here so both sides read it once. */
export type ApiBase = "/api/v1";

// ---- Errors ---------------------------------------------------------------

/**
 * The only error shape. `error` is a stable machine token; `message` is for a
 * human reading a log and may change without notice.
 *
 * Note what is absent: there is no `403`. A request for something the caller may
 * not see gets `not_found`, because an endpoint that distinguishes "does not
 * exist" from "exists, but not for you" is an enumeration oracle. See
 * CONTRACT.md §6.
 */
export interface ErrorBody {
  error: "not_found" | "bad_request" | "unauthorized" | "upstream_unavailable" | "internal";
  message: string;
}

// ---- Health ---------------------------------------------------------------

export type WeatherSourceId = "none" | "nws" | "metno" | "openmeteo";
export type FlightsSourceId = "sim" | "adsb" | "dump1090";
export type SatellitesSourceId = "none" | "celestrak";
export type MarkersSourceId = "none" | "file";
export type AuthMode = "none" | "sso" | "jwt";

/**
 * What this deployment turned out to be, once the environment had its say.
 *
 * `degraded` is the load-bearing field. A source configured without what it
 * needs is demoted rather than fatal (CONTRACT.md §5.1), and this is where the
 * demotion is visible to anybody who did not read the boot log — without it, a
 * misconfigured contact string looks exactly like a clear day.
 */
export interface HealthBody {
  ok: true;
  service: "tera-api";
  version: string;
  uptimeSeconds: number;
  sources: {
    weather: WeatherSourceId;
    flights: FlightsSourceId;
    satellites: SatellitesSourceId;
    markers: MarkersSourceId;
  };
  auth: {
    mode: AuthMode;
    /** Where a browser sends someone to sign in. `null` unless mode is `sso`. */
    entryUrl: string | null;
  };
  /** One human sentence per demotion. Empty on a fully-configured box. */
  degraded: string[];
}

// ---- Flights --------------------------------------------------------------

/**
 * A leg the simulator flies. Structurally identical to `SimRoute` in
 * `engine/flights.ts`; see the note at the top of this file for why it is
 * restated rather than imported.
 */
export interface WireSimRoute {
  callsign: string;
  from: [number, number];
  to: [number, number];
  /** Metres at the start and end of the leg. */
  fromAlt: number;
  toAlt: number;
  /** Seconds for a full traversal. */
  duration: number;
}

/** One aircraft, as `engine/types.ts` `Aircraft` wants it. */
export interface WireAircraft {
  id: string;
  lat: number;
  lng: number;
  /** Metres. The wire never carries feet, whatever the upstream feed used. */
  altitude: number;
  /** Degrees clockwise from true north. */
  heading: number;
  callsign?: string;
}

/**
 * The simulated sky, sent as a *plan* rather than as positions.
 *
 * The server hands over the routes, a phase origin and a seed, and every browser
 * evaluates the same closed-form function of wall-clock time. That means one
 * cheap cacheable request instead of a poll every second, and — more usefully —
 * two people looking at the map from different machines see the same aircraft in
 * the same places, which a per-client simulation cannot promise.
 *
 * `t0` is a fixed epoch and emphatically **not** the server's start time: if it
 * moved on restart, every aircraft would teleport.
 */
export interface FlightsPlanBody {
  mode: "plan";
  source: "sim";
  /** Epoch milliseconds. The instant at which every route's phase is zero. */
  t0: number;
  /** Seed for the per-route phase offsets, so all viewers agree. */
  seed: number;
  routes: WireSimRoute[];
  /** How long the plan may be cached, in seconds. */
  ttlSeconds: number;
}

/** Real traffic, as positions, because there is no closed form for the sky. */
export interface FlightsLiveBody {
  mode: "live";
  source: "adsb" | "dump1090";
  /** Epoch milliseconds at which this snapshot was taken. */
  observedAt: number;
  aircraft: WireAircraft[];
  ttlSeconds: number;
  /** Attribution the consumer is expected to display, if the feed asks for it. */
  attribution?: string[];
}

export type FlightsBody = FlightsPlanBody | FlightsLiveBody;

// ---- Satellites -----------------------------------------------------------

/**
 * One satellite, sent as its **element set** rather than as a position.
 *
 * The same trick `FlightsPlanBody` plays, for the same reason and with better
 * justification: a TLE is already a closed-form description of an orbit, valid
 * for days either side of its epoch, and SGP4 is the function that evaluates it.
 * Sending positions would mean polling — a satellite crosses the sky in ten
 * minutes — and would mean two people looking at the same overhead pass from
 * different machines disagreeing about where it is. Sending the elements means
 * one cacheable request every few hours and universal agreement, which is
 * exactly the property the flight plan exists to buy.
 *
 * It is also the *honest* shape. CelesTrak publishes element sets; positions are
 * something a consumer computes. A server that computed them would be inserting
 * itself into a calculation it adds nothing to.
 *
 * `line1` and `line2` are the two 69-character TLE lines, verbatim. They are
 * carried as strings rather than parsed into fields because SGP4 implementations
 * take exactly this and every parse in between is a chance to lose a digit.
 */
export interface WireSatellite {
  /** NORAD catalogue number, from columns 3–7 of line 1. Stable for the object's life. */
  noradId: number;
  name: string;
  group: SatelliteGroup;
  /** The first TLE line, 69 characters, unmodified. */
  line1: string;
  /** The second TLE line, 69 characters, unmodified. */
  line2: string;
}

/**
 * The catalogue this box is serving, and when it last managed to fetch one.
 *
 * There is no `mode` discriminant here, unlike `FlightsBody`, because there is
 * only ever one mode: elements. A box with no satellite source configured serves
 * `source: "none"` and an **empty array** rather than a synthetic constellation,
 * and that asymmetry with the flight plan is deliberate. An invented aeroplane is
 * a plausible aeroplane; an invented Starlink is a lie about a specific object
 * with a catalogue number, and somebody standing in a field with a telescope
 * would be entitled to be annoyed about it. The sky either has the real thing in
 * it or it has nothing.
 */
export interface SatellitesBody {
  source: SatellitesSourceId;
  /**
   * ISO-8601, the last time a fetch **succeeded**. Not the time of this response:
   * a body served from a six-hour-old snapshot must say so, because the client
   * has no other way to tell a fresh catalogue from a stale one and SGP4 accuracy
   * degrades with distance from the element epoch.
   */
  fetchedAt: string;
  satellites: WireSatellite[];
  ttlSeconds: number;
  attribution?: string[];
}

// ---- Weather --------------------------------------------------------------

/**
 * Sky conditions, reduced to what a light rig can actually use.
 *
 * Not a met report: no dew point, no pressure, no station id. `Atmosphere`
 * consumes cloud cover, precipitation and visibility and nothing else, and a
 * field that no renderer reads is a field that gets wrong without anyone
 * noticing.
 */
export type WeatherCondition =
  | "clear"
  | "partly-cloudy"
  | "cloudy"
  | "overcast"
  | "fog"
  | "rain"
  | "snow"
  | "thunderstorm";

export interface WeatherBody {
  /** ISO-8601. The observation time, not the fetch time. */
  observedAt: string;
  source: WeatherSourceId;
  /**
   * True when nobody was asked and this is the fallback clear day.
   *
   * A zero-config box serves `synthetic: true` forever and that is a supported
   * state, not an error — which is why this is a field and not a 503.
   */
  synthetic: boolean;
  location: { lat: number; lng: number };
  /** `null` where the source did not report it. Never silently zeroed. */
  temperatureC: number | null;
  windKph: number | null;
  /** Degrees clockwise from true north, the direction the wind blows *from*. */
  windDirDeg: number | null;
  /** 0..1. */
  cloudCover: number;
  /** 0..1, an intensity rather than a rate — the renderer wants a dial. */
  precipitation: number;
  visibilityKm: number | null;
  condition: WeatherCondition;
  /** Attribution the consumer must display for this source, where one is owed. */
  attribution?: string[];
}

// ---- Markers --------------------------------------------------------------

/**
 * Where a coordinate came from, and the only thing standing between this repo
 * and an ODbL share-alike obligation.
 *
 * Publishing a snapshot of geocoded coordinates is Public Use of a Derivative
 * Database. If those coordinates came out of Nominatim, ODbL §4.3 and §4.4
 * attach to everything served here — no matter that the rows live in a private
 * database rather than in the repo. Containment was never the discharge.
 * CONTRACT.md §8.
 *
 * The type is a plain string on purpose. The entire point of the gate is that a
 * value nobody anticipated can arrive and must be *refused at serve time*, which
 * a closed union would quietly turn into a compile error somewhere upstream
 * instead.
 */
export type CoordinateProvenance = KnownProvenance | (string & {});

/**
 * The values the public gate accepts by default.
 *
 * - `us-census` — geocoding.geo.census.gov, a US Government work in the public
 *   domain. The sanctioned geocoder.
 * - `hand-placed` — typed by a human from a published address. Original.
 * - `synthetic` — invented for a demo. Owes nobody anything.
 *
 * Deliberately absent: `nominatim`, `osm`, `google`, `mapbox`, `here`. The first
 * two are share-alike; the rest restrict storing and redistributing what they
 * return, which is exactly what a public snapshot does.
 */
export type KnownProvenance = "us-census" | "hand-placed" | "synthetic";

/**
 * A marker as it crosses the wire: an engine `Marker` plus where its coordinate
 * came from.
 *
 * It extends `Marker` rather than restating it so that the adapter is a cast and
 * not a copy — the engine still knows nothing about provenance, and the day
 * `Marker` gains a field, this follows it.
 */
export interface WireMarker extends Marker {
  provenance: CoordinateProvenance;
}

export interface MarkersBody {
  markers: WireMarker[];
  /** ISO-8601 timestamp of the snapshot these rows came from. */
  generatedAt: string;
  /**
   * Rows the public-shape gate refused, by count and reason. Served rather than
   * only logged: a silent drop looks identical to an empty database.
   */
  refused: { reason: string; count: number }[];
  attribution?: string[];
}

// ---- Offices --------------------------------------------------------------

/**
 * One office pack, addressed and wrapped for delivery.
 *
 * `floor` is the authored `Office` exactly as `src/interiors/types.ts` defines
 * it — the pack a self-hoster writes by hand and the pack that arrives over HTTP
 * are the same bytes, which is the rule that keeps the format from forking.
 * Everything outside `floor` is deployment metadata the renderer never reads.
 */
export interface OfficeDoc {
  id: string;
  name: string;
  floor: Office;
  visibility: OfficeVisibility;
  /** ISO-8601. */
  updated?: string;
}

/**
 * Who is in one office, right now.
 *
 * Separate from `OfficeDoc` on purpose, and the separation is load-bearing
 * rather than tidy. A `Presence` carries a `seatId` and no coordinate precisely
 * so that the geometry can be published while the people are not; putting the
 * roster inside the pack would mean an operator who wants a public floorplan has
 * to strip the people out of it by hand, and the first time they forget, the
 * leak is permanent. Two documents means the safe thing is the default thing.
 *
 * It is also the honest shape for the data: a floorplan changes when somebody
 * moves a wall and a roster changes when somebody sits down, which is why this
 * one carries `observedAt` and the pack does not.
 */
export interface PresenceBody {
  officeId: string;
  people: Presence[];
  /** ISO-8601. Absent when the source did not say, which is not an error. */
  observedAt?: string;
}

/**
 * `private` means the endpoint answers 404 to anyone who may not see it — see
 * `ErrorBody`. `unlisted` is served to anybody with the id but never appears in
 * an index and is never publicly cached.
 */
export type OfficeVisibility = "public" | "unlisted" | "private";
