{
  "openapi": "3.0.3",
  "info": {
    "title": "BGaaS",
    "description": "Board Games as a Service. Catalog API for BoardGames.Guru: owned catalog + Board Game Arena titles + Wikidata fallback.\n\nBase URL: `https://api.boardgames.guru`",
    "version": "0.3.0"
  },
  "servers": [
    {
      "url": "https://api.boardgames.guru",
      "description": "Production"
    },
    {
      "url": "http://127.0.0.1:3002",
      "description": "Local"
    }
  ],
  "tags": [
    { "name": "meta" },
    { "name": "catalog" },
    { "name": "pick" },
    { "name": "images" }
  ],
  "paths": {
    "/api/health": {
      "get": {
        "tags": ["meta"],
        "summary": "Health and catalog counts",
        "operationId": "getHealth",
        "responses": {
          "200": {
            "description": "Service is up",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/HealthResponse" }
              }
            }
          }
        }
      }
    },
    "/api/search": {
      "get": {
        "tags": ["catalog"],
        "summary": "Search games",
        "description": "Requires `q` with at least 2 characters. Prefer owned catalog matches, then BGA, then Wikidata.",
        "operationId": "searchGames",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "schema": { "type": "string", "minLength": 2 },
            "example": "wingspan"
          }
        ],
        "responses": {
          "200": {
            "description": "Search hits",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/SearchResponse" }
              }
            }
          },
          "502": { "description": "Upstream / catalog load failure" }
        }
      }
    },
    "/api/games/{id}": {
      "get": {
        "tags": ["catalog"],
        "summary": "Get one game",
        "description": "Lookup by owned id (`bgaas_…`), BGA catalog id, or Wikidata Q-id.",
        "operationId": "getGame",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" },
            "examples": {
              "owned": { "value": "bgaas_codenames" },
              "wikidata": { "value": "Q20977894" }
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Game found",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/GameResponse" }
              }
            }
          },
          "404": { "description": "Not found" },
          "502": { "description": "Lookup failed" }
        }
      }
    },
    "/api/pick": {
      "get": {
        "tags": ["pick"],
        "summary": "Pick games for tonight",
        "description": "Score and return catalog games matching optional filters. Multi-select vibes are AND filters on playFeel.",
        "operationId": "pickTonight",
        "parameters": [
          {
            "name": "players",
            "in": "query",
            "schema": { "type": "string", "example": "4" },
            "description": "`any`, a number like `4`, or at-least form `5+`."
          },
          {
            "name": "maxMinutes",
            "in": "query",
            "schema": { "type": "string", "example": "60" },
            "description": "`any` or a positive number."
          },
          {
            "name": "minMinutes",
            "in": "query",
            "schema": { "type": "string", "example": "any" }
          },
          {
            "name": "maxWeight",
            "in": "query",
            "schema": { "type": "string", "example": "2.5" },
            "description": "BGG-style weight ceiling, or `any`."
          },
          {
            "name": "genre",
            "in": "query",
            "schema": { "type": "string", "example": "party" }
          },
          {
            "name": "q",
            "in": "query",
            "schema": { "type": "string" },
            "description": "Optional name/text filter."
          },
          {
            "name": "vibes",
            "in": "query",
            "schema": {
              "type": "string",
              "example": "easy_teach,quick_setup"
            },
            "description": "Comma-separated: `easy_teach`, `quick_setup`, `low_downtime`, `small_table`."
          },
          {
            "name": "seed",
            "in": "query",
            "schema": { "type": "string", "default": "catalog-tonight" }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": { "type": "integer", "minimum": 1, "maximum": 20, "default": 6 }
          }
        ],
        "responses": {
          "200": {
            "description": "Ranked picks",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/PickResponse" }
              }
            }
          },
          "400": { "description": "Invalid filter" },
          "500": { "description": "Pick failed" }
        }
      }
    },
    "/api/placeholders/{id}": {
      "get": {
        "tags": ["images"],
        "summary": "Placeholder art for a game id",
        "operationId": "getPlaceholder",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": { "description": "Image bytes" },
          "404": { "description": "Not found" }
        }
      }
    },
    "/api/images/cge/{file}": {
      "get": {
        "tags": ["images"],
        "summary": "CGE cover image file",
        "operationId": "getCgeImage",
        "parameters": [
          {
            "name": "file",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": { "description": "Image bytes" },
          "404": { "description": "Not found" }
        }
      }
    },
    "/api/images/ai/{file}": {
      "get": {
        "tags": ["images"],
        "summary": "AI cover image file",
        "operationId": "getAiImage",
        "parameters": [
          {
            "name": "file",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": { "description": "Image bytes" },
          "404": { "description": "Not found" }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "PlayFeel": {
        "type": "object",
        "properties": {
          "setupTeardown": {
            "type": "string",
            "enum": ["quick", "moderate", "heavy"],
            "nullable": true
          },
          "tableFootprint": {
            "type": "string",
            "enum": ["tiny", "small", "medium", "large"],
            "nullable": true
          },
          "interaction": {
            "type": "string",
            "enum": ["solitaire", "low", "moderate", "high"],
            "nullable": true
          },
          "downtime": {
            "type": "string",
            "enum": ["low", "moderate", "high"],
            "nullable": true
          },
          "playerElimination": {
            "type": "string",
            "enum": ["none", "rare", "common"],
            "nullable": true
          },
          "luckSkill": {
            "type": "string",
            "enum": ["mostly_skill", "balanced", "mostly_luck"],
            "nullable": true
          },
          "teachability": {
            "type": "string",
            "enum": ["instant", "easy", "involved", "rules_heavy"],
            "nullable": true
          },
          "mood": {
            "type": "array",
            "nullable": true,
            "items": {
              "type": "string",
              "enum": [
                "social",
                "focused",
                "competitive",
                "casual",
                "tense",
                "cozy"
              ]
            }
          },
          "replayability": {
            "type": "string",
            "enum": [
              "steady",
              "high_setup_variety",
              "asymmetric",
              "campaign"
            ],
            "nullable": true
          },
          "confidence": {
            "type": "string",
            "enum": ["high", "medium", "low"],
            "nullable": true
          },
          "notes": { "type": "string", "nullable": true }
        }
      },
      "CatalogGame": {
        "type": "object",
        "required": ["id", "name", "source"],
        "properties": {
          "id": { "type": "string" },
          "name": { "type": "string" },
          "description": { "type": "string" },
          "shortDescription": { "type": "string" },
          "yearPublished": { "type": "integer" },
          "imageUrl": { "type": "string" },
          "thumbnailUrl": { "type": "string" },
          "source": {
            "type": "string",
            "enum": ["owned", "bga", "wikidata"]
          },
          "wikidataId": { "type": "string" },
          "tags": { "type": "array", "items": { "type": "string" } },
          "boxColor": { "type": "string" },
          "boardGameArenaId": { "type": "string" },
          "minPlayers": { "type": "integer" },
          "maxPlayers": { "type": "integer" },
          "playTimeMinutes": { "type": "integer" },
          "minAge": { "type": "integer" },
          "weight": { "type": "number" },
          "designer": { "type": "string" },
          "publisher": { "type": "string" },
          "playFeel": { "$ref": "#/components/schemas/PlayFeel" }
        }
      },
      "HealthResponse": {
        "type": "object",
        "properties": {
          "ok": { "type": "boolean" },
          "service": { "type": "string" },
          "name": { "type": "string" },
          "version": { "type": "string" },
          "catalog": { "type": "string" },
          "count": { "type": "integer", "nullable": true },
          "owned": { "type": "object" },
          "bga": { "type": "object" },
          "union": { "type": "object" }
        }
      },
      "SearchResponse": {
        "type": "object",
        "properties": {
          "results": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/CatalogGame" }
          },
          "sources": { "type": "array", "items": { "type": "string" } },
          "license": { "type": "string" }
        }
      },
      "GameResponse": {
        "type": "object",
        "properties": {
          "game": { "$ref": "#/components/schemas/CatalogGame" },
          "source": { "type": "string" },
          "license": { "type": "string" }
        }
      },
      "PickRow": {
        "type": "object",
        "properties": {
          "game": { "$ref": "#/components/schemas/CatalogGame" },
          "score": { "type": "number" },
          "reasons": { "type": "array", "items": { "type": "string" } }
        }
      },
      "PickResponse": {
        "type": "object",
        "properties": {
          "picks": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/PickRow" }
          },
          "count": { "type": "integer" }
        }
      }
    }
  }
}
