openapi: 3.1.0
info:
  title: Fluvigil Public API
  version: 1.0.0
  description: >
    Water levels and flood classifications of German gauging stations.
    Data sources: PEGELONLINE (WSV) and Länderübergreifendes Hochwasserportal
    (LHP, CC BY 4.0 — attribution required), weather by Deutscher Wetterdienst (via Bright Sky).

    Anonymous access is allowed at free-tier limits (90-day readings range,
    600 requests/hour per IP). An API key (created in the account area, sent
    as `X-Api-Key` or `Authorization: Bearer pm_...`) applies the limits of
    the account's tier; exceeding the hourly budget returns 429.
servers:
  - url: /api/v1
security:
  - {}
  - apiKey: []
paths:
  /stations:
    get:
      summary: List stations
      parameters:
        - name: q
          in: query
          description: Case-insensitive substring match on station name or water body.
          schema: { type: string }
        - name: state
          in: query
          description: Federal state ISO code (e.g. DE-BY). Only LHP stations carry one.
          schema: { type: string }
        - name: source
          in: query
          schema: { type: string, enum: [pegelonline, lhp] }
        - name: floodClass
          in: query
          description: Minimum LHP flood class (-1..4).
          schema: { type: integer, minimum: -1, maximum: 4 }
        - name: condition
          in: query
          description: >
            Cross-source water condition: 'flood' matches any flood signal (LHP class >= 1 or
            PEGELONLINE state 'high'), 'flood-1'..'flood-4' match an exact LHP class,
            'normal' and 'low' map to the respective states.
          schema: { type: string, enum: [low, normal, flood, flood-1, flood-2, flood-3, flood-4] }
        - name: threshold
          in: query
          description: >
            Latest value compared against the station's stored characteristic thresholds
            (only stations carrying the respective threshold can match).
          schema:
            { type: string, enum: [above-m3, above-m2, above-m1, above-mhw, above-mw, below-mnw] }
        - name: bbox
          in: query
          description: Bounding box "south,west,north,east" in WGS84.
          schema: { type: string }
        - name: includeInactive
          in: query
          schema: { type: boolean, default: false }
      responses:
        '200':
          description: Matching stations (max 5000).
          content:
            application/json:
              schema:
                type: object
                properties:
                  count: { type: integer }
                  stations:
                    type: array
                    items: { $ref: '#/components/schemas/StationListItem' }
        '400': { $ref: '#/components/responses/BadRequest' }
  /stations/{id}:
    get:
      summary: Station detail
      parameters:
        - $ref: '#/components/parameters/StationId'
      responses:
        '200':
          description: Station master data with latest values.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/StationDetail' }
        '404': { $ref: '#/components/responses/NotFound' }
  /stations/{id}/readings:
    get:
      summary: Water level time series
      description: >-
        History depth depends on the tier (anonymous/free: last 90 days) — `from` is clamped
        to that window; ranges entirely outside it return 400. Defaults to the last 7 days.
      parameters:
        - $ref: '#/components/parameters/StationId'
        - name: from
          in: query
          schema: { type: string, format: date-time }
        - name: to
          in: query
          schema: { type: string, format: date-time }
      responses:
        '200':
          description: Readings ordered by time ascending.
          content:
            application/json:
              schema:
                type: object
                properties:
                  stationId: { type: string }
                  count: { type: integer }
                  readings:
                    type: array
                    items: { $ref: '#/components/schemas/ReadingPoint' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }
  /stations/{id}/weather:
    get:
      summary: Weather forecast at the station location
      description: DWD forecast via Bright Sky (7 days, daily aggregates), cached server-side for 30 minutes.
      parameters:
        - $ref: '#/components/parameters/StationId'
      responses:
        '200':
          description: Forecast with attribution.
        '404': { $ref: '#/components/responses/NotFound' }
        '502': { description: Upstream weather API unavailable. }
  /health:
    get:
      summary: Service health
      responses:
        '200':
          description: Database reachability and per-source poll staleness.
  /stats:
    get:
      summary: Dataset statistics
      description: Aggregated counts (stations, readings, thresholds, flood-state changes), cached for 5 minutes.
      responses:
        '200':
          description: Current dataset numbers.
components:
  parameters:
    StationId:
      name: id
      in: path
      required: true
      description: Namespaced station id, e.g. "pegelonline:<uuid>" or "lhp:BY_16005701".
      schema: { type: string }
  responses:
    BadRequest: { description: Invalid query parameters. }
    NotFound: { description: Station does not exist. }
  schemas:
    StationListItem:
      type: object
      properties:
        id: { type: string }
        source: { type: string, enum: [pegelonline, lhp] }
        name: { type: string }
        water: { type: [string, 'null'] }
        latitude: { type: number }
        longitude: { type: number }
        federalState: { type: [string, 'null'] }
        infoLink: { type: [string, 'null'] }
        latestValueCentimeters: { type: [number, 'null'] }
        latestMeasuredAt: { type: [string, 'null'], format: date-time }
        levelState: { type: [string, 'null'], description: PEGELONLINE MNW/MHW classification }
        lhpClass: { type: [integer, 'null'], minimum: -1, maximum: 4 }
        lhpClassName: { type: [string, 'null'] }
        isActive: { type: boolean }
    StationDetail:
      allOf:
        - $ref: '#/components/schemas/StationListItem'
        - type: object
          properties:
            sourceStationId: { type: string }
            agency: { type: [string, 'null'] }
            riverKm: { type: [number, 'null'] }
            gaugeZeroMeters: { type: [number, 'null'] }
            firstSeenAt: { type: string, format: date-time }
            lastSeenAt: { type: string, format: date-time }
    ReadingPoint:
      type: object
      properties:
        measuredAt: { type: string, format: date-time }
        valueCentimeters: { type: number }
  securitySchemes:
    apiKey:
      type: apiKey
      in: header
      name: X-Api-Key
