{
  "openapi": "3.1.0",
  "info": {
    "title": "Windwave API",
    "version": "1.0.0",
    "summary": "Vento medido nas praias e câmeras ao vivo do Windwave.",
    "description": "Fluxo: troque a chave (ww_live_… ou ww_test_…) por uma sessão em POST /session, use o token de sessão (wws_…) nos demais pedidos e renove antes de vencer. Cada chave aceita uma sessão por vez. Guia completo: https://windwave.com.br/developers",
    "contact": { "name": "Windwave", "url": "https://windwave.com.br/developers" }
  },
  "servers": [{ "url": "https://windwave.com.br/api/v1" }],
  "security": [{ "session": [] }],
  "tags": [
    { "name": "Sessão", "description": "Abrir, conferir, renovar e encerrar a sessão da chave." },
    { "name": "Estações", "description": "Vento medido pelas estações Windwave." },
    { "name": "Câmeras", "description": "Câmeras e vídeo ao vivo (HLS)." },
    { "name": "Conta", "description": "Uso e situação." }
  ],
  "paths": {
    "/status": {
      "get": {
        "tags": ["Conta"], "summary": "Situação da API (sem autenticação)", "security": [],
        "responses": { "200": { "description": "No ar", "content": { "application/json": { "schema": { "type": "object", "properties": { "status": { "type": "string", "example": "ok" }, "version": { "type": "string" }, "time": { "type": "string", "format": "date-time" } } } } } } }
      }
    },
    "/session": {
      "post": {
        "tags": ["Sessão"], "summary": "Abrir sessão com a chave", "security": [{ "apiKey": [] }],
        "description": "Envie a chave no cabeçalho Authorization. Se a chave já tem sessão ativa em outra rede, responde 409 key_in_use. Da mesma rede, a sessão antiga é substituída.",
        "responses": {
          "201": { "description": "Sessão aberta", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Session" } } } },
          "401": { "$ref": "#/components/responses/Problem" }, "403": { "$ref": "#/components/responses/Problem" }, "409": { "$ref": "#/components/responses/Problem" }
        }
      },
      "get": {
        "tags": ["Sessão"], "summary": "Conferir a sessão atual (não conta na cota)",
        "responses": { "200": { "description": "Sessão válida", "content": { "application/json": { "schema": { "type": "object", "properties": { "session_id": { "type": "string", "format": "uuid" }, "client": { "type": "string" }, "env": { "type": "string", "enum": ["live", "test"] }, "expires_at": { "type": "string", "format": "date-time" }, "scopes": { "type": "array", "items": { "type": "string" } } } } } } }, "401": { "$ref": "#/components/responses/Problem" } }
      },
      "delete": {
        "tags": ["Sessão"], "summary": "Encerrar a sessão (libera a chave na hora)",
        "responses": { "204": { "description": "Encerrada" }, "401": { "$ref": "#/components/responses/Problem" } }
      }
    },
    "/session/refresh": {
      "post": {
        "tags": ["Sessão"], "summary": "Renovar a sessão",
        "description": "Devolve um token novo com mais 15 minutos. O token anterior deixa de valer. Faça a cada ~10 minutos.",
        "responses": { "200": { "description": "Renovada", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Session" } } } }, "401": { "$ref": "#/components/responses/Problem" } }
      }
    },
    "/usage": {
      "get": {
        "tags": ["Conta"], "summary": "Uso da chave (30 dias)",
        "responses": { "200": { "description": "Uso", "content": { "application/json": { "schema": { "type": "object", "properties": { "client": { "type": "string" }, "month": { "type": "object", "properties": { "used": { "type": "integer" }, "quota": { "type": "integer" } } }, "per_minute": { "type": "object", "properties": { "limit": { "type": "integer" }, "used": { "type": "integer" } } }, "days": { "type": "array", "items": { "type": "object", "properties": { "day": { "type": "string", "format": "date" }, "requests": { "type": "integer" } } } } } } } } } }
      }
    },
    "/stations": {
      "get": {
        "tags": ["Estações"], "summary": "Estações liberadas para a chave", "x-scope": "stations:read",
        "responses": { "200": { "description": "Lista", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/Station" } } } } } } }, "403": { "$ref": "#/components/responses/Problem" } }
      }
    },
    "/stations/{id}/now": {
      "get": {
        "tags": ["Estações"], "summary": "Leitura mais recente", "x-scope": "stations:read",
        "parameters": [{ "$ref": "#/components/parameters/StationId" }],
        "responses": { "200": { "description": "Leitura", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Reading" } } } }, "404": { "$ref": "#/components/responses/Problem" } }
      }
    },
    "/stations/{id}/history": {
      "get": {
        "tags": ["Estações"], "summary": "Histórico agregado", "x-scope": "history:read",
        "parameters": [
          { "$ref": "#/components/parameters/StationId" },
          { "name": "from", "in": "query", "schema": { "type": "string", "format": "date-time" }, "description": "Início (ISO 8601). Padrão: 24 h antes de to." },
          { "name": "to", "in": "query", "schema": { "type": "string", "format": "date-time" }, "description": "Fim (ISO 8601). Padrão: agora." },
          { "name": "interval", "in": "query", "schema": { "type": "string", "enum": ["10m", "1h", "1d"], "default": "1h" }, "description": "Agrupamento. Período máximo: 7 dias (10m), 31 dias (1h), 366 dias (1d)." }
        ],
        "responses": { "200": { "description": "Série", "content": { "application/json": { "schema": { "type": "object", "properties": { "station_id": { "type": "string" }, "interval": { "type": "string" }, "from": { "type": "string", "format": "date-time" }, "to": { "type": "string", "format": "date-time" }, "data": { "type": "array", "items": { "$ref": "#/components/schemas/Bucket" } } } } } } }, "400": { "$ref": "#/components/responses/Problem" } }
      }
    },
    "/cameras": {
      "get": {
        "tags": ["Câmeras"], "summary": "Câmeras liberadas para a chave", "x-scope": "cameras:read",
        "responses": { "200": { "description": "Lista", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/Camera" } } } } } } } }
      }
    },
    "/cameras/{id}/stream": {
      "post": {
        "tags": ["Câmeras"], "summary": "Endereço de vídeo ao vivo (HLS)", "x-scope": "cameras:live",
        "description": "Devolve um endereço .m3u8 válido por 10 minutos. Peça outro antes de vencer. O limite de telas conta aparelhos (IPs) diferentes assistindo ao mesmo tempo.",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }],
        "responses": { "201": { "description": "Endereço", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Stream" } } } }, "429": { "$ref": "#/components/responses/Problem" } }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": { "type": "http", "scheme": "bearer", "bearerFormat": "ww_live_… / ww_test_…", "description": "A chave do cliente. Usada só em POST /session." },
      "session": { "type": "http", "scheme": "bearer", "bearerFormat": "wws_…", "description": "Token de sessão (15 min)." }
    },
    "parameters": {
      "StationId": { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "UUID da estação (ou demo-station com chave de teste)." }
    },
    "responses": {
      "Problem": { "description": "Erro (RFC 9457)", "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } } }
    },
    "schemas": {
      "Session": { "type": "object", "properties": { "token": { "type": "string", "example": "wws_…" }, "token_type": { "type": "string", "example": "Bearer" }, "session_id": { "type": "string", "format": "uuid" }, "expires_at": { "type": "string", "format": "date-time" }, "expires_in": { "type": "integer", "example": 900 }, "env": { "type": "string", "enum": ["live", "test"] } } },
      "Station": { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "provider": { "type": "string" }, "spot": { "type": ["object", "null"], "properties": { "name": { "type": "string" }, "uf": { "type": "string" }, "lat": { "type": "number" }, "lon": { "type": "number" } } }, "live": { "type": "boolean", "description": "Mandou leitura nos últimos 10 min" }, "last_reading_at": { "type": ["string", "null"], "format": "date-time" } } },
      "Reading": { "type": "object", "properties": { "station_id": { "type": "string" }, "at": { "type": ["string", "null"], "format": "date-time" }, "age_sec": { "type": "integer" }, "wind_kt": { "type": "number" }, "gust_kt": { "type": "number" }, "dir_deg": { "type": "integer", "description": "De onde o vento vem, 0–359" }, "temp_c": { "type": ["number", "null"] }, "units": { "type": "object" } } },
      "Bucket": { "type": "object", "properties": { "t": { "type": "string", "format": "date-time" }, "wind_avg_kt": { "type": "number" }, "wind_max_kt": { "type": "number" }, "gust_max_kt": { "type": "number" }, "dir_deg": { "type": ["integer", "null"], "description": "Média vetorial" }, "temp_c": { "type": ["number", "null"] }, "samples": { "type": "integer" } } },
      "Camera": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "slug": { "type": "string" }, "name": { "type": "string" }, "spot": { "type": ["object", "null"] }, "online": { "type": "boolean" }, "last_seen": { "type": ["string", "null"], "format": "date-time" } } },
      "Stream": { "type": "object", "properties": { "camera_id": { "type": "string", "format": "uuid" }, "hls_url": { "type": "string", "format": "uri" }, "expires_at": { "type": "string", "format": "date-time" }, "expires_in": { "type": "integer", "example": 600 }, "max_viewers": { "type": "integer" } } },
      "Problem": { "type": "object", "properties": { "type": { "type": "string", "format": "uri" }, "title": { "type": "string" }, "status": { "type": "integer" }, "code": { "type": "string", "example": "key_in_use" }, "hint": { "type": "string" }, "detail": { "type": "string" } } }
    }
  }
}
