{
  "openapi": "3.1.0",
  "info": {
    "title": "AceWeather API",
    "version": "1.0.0",
    "description": "AceWeather weather dashboard API with a plain-text report endpoint and a structured weather payload endpoint."
  },
  "servers": [
    {
      "url": "/",
      "description": "Current host"
    }
  ],
  "paths": {
    "/api": {
      "get": {
        "summary": "API discovery index",
        "description": "Returns a compact JSON index of the available AceWeather endpoints and documentation URLs.",
        "responses": {
          "200": {
            "description": "API index"
          }
        }
      }
    },
    "/api/search": {
      "get": {
        "summary": "Search for places",
        "parameters": [
          {
            "name": "query",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 2
            },
            "description": "Place name or search query."
          }
        ],
        "responses": {
          "200": {
            "description": "Matching locations as JSON"
          },
          "400": {
            "description": "Invalid query"
          }
        }
      }
    },
    "/api/weather": {
      "get": {
        "summary": "Get structured weather payload",
        "parameters": [
          {
            "name": "lat",
            "in": "query",
            "required": true,
            "schema": {
              "type": "number",
              "minimum": -90,
              "maximum": 90
            }
          },
          {
            "name": "lon",
            "in": "query",
            "required": true,
            "schema": {
              "type": "number",
              "minimum": -180,
              "maximum": 180
            }
          },
          {
            "name": "timezone",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "auto"
            },
            "description": "IANA timezone name or auto."
          },
          {
            "name": "label",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "history_days",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          },
          {
            "name": "history_start",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "history_end",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Structured weather payload as JSON"
          },
          "400": {
            "description": "Invalid parameters"
          },
          "502": {
            "description": "Upstream provider unavailable"
          }
        }
      }
    },
    "/api/report": {
      "get": {
        "summary": "Get plain-text weather report",
        "description": "Returns the LLM-friendly weather report as text/plain. Supports flexible historical-data windows via period shortcuts or explicit date ranges, and an optional format=csv|json mode.",
        "parameters": [
          {
            "name": "query",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "minLength": 2
            },
            "description": "Place name. If provided, lat/lon are not required."
          },
          {
            "name": "lat",
            "in": "query",
            "required": false,
            "schema": {
              "type": "number",
              "minimum": -90,
              "maximum": 90
            }
          },
          {
            "name": "lon",
            "in": "query",
            "required": false,
            "schema": {
              "type": "number",
              "minimum": -180,
              "maximum": 180
            }
          },
          {
            "name": "timezone",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "auto"
            }
          },
          {
            "name": "label",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "period",
            "in": "query",
            "required": false,
            "description": "Named historical window. Overrides history_days/history_start/history_end if set.",
            "schema": {
              "type": "string",
              "enum": [
                "last_7d",
                "last_14d",
                "last_30d",
                "last_90d",
                "last_365d",
                "last_week",
                "this_week",
                "last_month",
                "mtd",
                "ytd",
                "same_week_last_year",
                "same_month_last_year",
                "same_period_last_year"
              ]
            }
          },
          {
            "name": "history_days",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 730,
              "default": 7
            },
            "description": "Number of days of history ending yesterday."
          },
          {
            "name": "history_start",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Custom history start date (YYYY-MM-DD). Combine with history_end."
          },
          {
            "name": "history_end",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Custom history end date (YYYY-MM-DD)."
          },
          {
            "name": "format",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": ["md", "csv", "json"],
              "default": "md"
            },
            "description": "md returns the markdown text report (default). csv returns just the observed-archive table as CSV. json returns the full structured weather payload."
          }
        ],
        "responses": {
          "200": {
            "description": "Plain-text weather report (or CSV / JSON depending on format)",
            "content": {
              "text/plain": {},
              "text/csv": {},
              "application/json": {}
            }
          },
          "400": {
            "description": "Invalid parameters"
          },
          "404": {
            "description": "Location not found"
          },
          "502": {
            "description": "Upstream provider unavailable"
          }
        }
      }
    },
    "/api/snapshot": {
      "get": {
        "summary": "Get compact JSON snapshot",
        "description": "Returns a compact weather snapshot intended for AppSheet webhook return values. Use either query by place name or explicit coordinates.",
        "parameters": [
          {
            "name": "query",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "minLength": 2
            },
            "description": "Place name. If provided, lat/lon are not required."
          },
          {
            "name": "lat",
            "in": "query",
            "required": false,
            "schema": {
              "type": "number",
              "minimum": -90,
              "maximum": 90
            }
          },
          {
            "name": "lon",
            "in": "query",
            "required": false,
            "schema": {
              "type": "number",
              "minimum": -180,
              "maximum": 180
            }
          },
          {
            "name": "timezone",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "auto"
            }
          },
          {
            "name": "label",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "history_days",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 730,
              "default": 7
            }
          },
          {
            "name": "period",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "last_7d",
                "last_14d",
                "last_30d",
                "last_90d",
                "last_365d",
                "last_week",
                "this_week",
                "last_month",
                "mtd",
                "ytd",
                "same_week_last_year",
                "same_month_last_year",
                "same_period_last_year"
              ]
            },
            "description": "Named historical window. Overrides history_days/start/end."
          },
          {
            "name": "history_start",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "history_end",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "station",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "site_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Compact weather snapshot as JSON"
          },
          "400": {
            "description": "Invalid parameters"
          },
          "404": {
            "description": "Location not found"
          },
          "401": {
            "description": "Missing or invalid bearer token when ACEWEATHER_WEBHOOK_TOKEN is configured"
          },
          "502": {
            "description": "Upstream provider unavailable"
          }
        }
      },
      "post": {
        "summary": "Post compact JSON snapshot request",
        "description": "Accepts a JSON body with query or lat/lon and returns the same compact AppSheet-friendly snapshot as the GET variant.",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Compact weather snapshot as JSON"
          },
          "400": {
            "description": "Invalid request body or parameters"
          },
          "404": {
            "description": "Location not found"
          },
          "401": {
            "description": "Missing or invalid bearer token when ACEWEATHER_WEBHOOK_TOKEN is configured"
          },
          "502": {
            "description": "Upstream provider unavailable"
          }
        }
      }
    },
    "/api/digest": {
      "get": {
        "summary": "Get canonical regional digest",
        "description": "Returns a bundled plain-text digest for a named regional set such as cropdynamics. The default brief mode is optimized for LLM use and includes the requested historical rainfall and temperature highs/lows plus next-7 forecast rainfall and temperature highs/lows per region.",
        "parameters": [
          {
            "name": "set",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "cropdynamics"
            }
          },
          {
            "name": "mode",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "brief",
              "enum": ["brief", "full"]
            },
            "description": "Use brief for the smaller LLM-oriented bundle or full for the full multi-section digest."
          },
          {
            "name": "history_days",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 366,
              "default": 7
            },
            "description": "Number of historical observation days to include, counting back from yesterday. For example, on 4 June 2026, history_days=29 covers 6 May through 3 June."
          },
          {
            "name": "format",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "brief",
              "enum": ["brief", "short"]
            },
            "description": "Use short for a fast historical-only regional summary table without forecast or daily rows."
          }
        ],
        "responses": {
          "200": {
            "description": "Plain-text bundled digest",
            "content": {
              "text/plain": {}
            }
          },
          "400": {
            "description": "Unknown digest set"
          },
          "502": {
            "description": "Upstream provider unavailable"
          }
        }
      }
    },
    "/api/cropdynamics": {
      "get": {
        "summary": "Get Crop Dynamics JSON summary",
        "description": "Returns a fast JSON summary for the seven Crop Dynamics locations. Defaults to the last 29 historical days ending yesterday and includes exact date range, executive summary, rankings, confidence metadata, and rain_mm/high_c/low_c per location. Add include=daily for daily rows.",
        "parameters": [
          {
            "name": "days",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 366,
              "default": 29
            },
            "description": "Historical days to include, counting back from yesterday. history_days is accepted as an alias."
          },
          {
            "name": "include",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": ["daily"]
            },
            "description": "Set include=daily to add per-location daily rows with date, rain_mm, high_c, and low_c."
          }
        ],
        "responses": {
          "200": {
            "description": "Crop Dynamics summary JSON",
            "content": {
              "application/json": {}
            }
          },
          "400": {
            "description": "Invalid days value"
          },
          "502": {
            "description": "Upstream provider unavailable"
          }
        }
      }
    },
    "/api/providers": {
      "get": {
        "summary": "Get provider availability",
        "responses": {
          "200": {
            "description": "Provider status as JSON"
          }
        }
      }
    },
    "/api/tropical": {
      "get": {
        "summary": "Active tropical systems (NHC + JTWC)",
        "description": "Aggregates active storms from NHC (Atlantic + East Pacific) and JTWC (West Pacific + Indian Ocean), normalized into a single storm list with category, winds, pressure, position, and advisory link. Either source may fail gracefully — check sources.{nhc,jtwc}.ok.",
        "responses": {
          "200": { "description": "Tropical aggregation JSON" },
          "502": { "description": "Both upstream feeds unavailable" }
        }
      }
    },
    "/api/onthisday": {
      "get": {
        "summary": "On-this-day historical climatology",
        "description": "Returns observed weather for the same calendar date across the last N years (Open-Meteo ERA5 archive). Default years=40. The summary block includes per-metric records (hottest/coldest/wettest/windiest year for the date).",
        "parameters": [
          { "name": "query", "in": "query", "required": false, "schema": { "type": "string", "minLength": 2 } },
          { "name": "lat", "in": "query", "required": false, "schema": { "type": "number", "minimum": -90, "maximum": 90 } },
          { "name": "lon", "in": "query", "required": false, "schema": { "type": "number", "minimum": -180, "maximum": 180 } },
          { "name": "timezone", "in": "query", "required": false, "schema": { "type": "string", "default": "auto" } },
          { "name": "label", "in": "query", "required": false, "schema": { "type": "string" } },
          { "name": "date", "in": "query", "required": false, "schema": { "type": "string", "format": "date" }, "description": "YYYY-MM-DD. If omitted, defaults to today's month/day." },
          { "name": "month", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1, "maximum": 12 } },
          { "name": "day", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1, "maximum": 31 } },
          { "name": "years", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1, "maximum": 80, "default": 40 } }
        ],
        "responses": {
          "200": { "description": "Historical climatology JSON" },
          "400": { "description": "Invalid parameters" },
          "404": { "description": "Location not found" },
          "502": { "description": "Upstream provider unavailable" }
        }
      }
    }
  }
}
