openapi: 3.1.0

info:
  title: Jackbox Game Picker API
  description: API for managing Jackbox Party Pack games, sessions, voting, and integrations.
  version: "1.0"

servers:
  - url: http://localhost:5000
    description: Local development (backend direct)
  - url: http://localhost:3000/api
    description: Docker Compose (via Vite/Nginx proxy)

tags:
  - name: Health
    description: Server health check
  - name: Auth
    description: Authentication and token verification
  - name: Games
    description: Game management, filtering, and pack metadata
  - name: Sessions
    description: Session lifecycle and game tracking
  - name: Picker
    description: Weighted random game selection
  - name: Stats
    description: Aggregate statistics
  - name: Votes
    description: Real-time popularity voting
  - name: Webhooks
    description: Webhook management for external integrations

components:
  responses:
    Unauthorized:
      description: No access token provided
      content:
        application/json:
          schema:
            type: object
            required: [error]
            properties:
              error: { type: string, example: "Access token required" }
    Forbidden:
      description: Invalid or expired token
      content:
        application/json:
          schema:
            type: object
            required: [error]
            properties:
              error: { type: string, example: "Invalid or expired token" }
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: >
        JWT token obtained from POST /api/auth/login. Pass as Authorization Bearer <token>.
        Tokens expire after 24 hours.

  schemas:
    Error:
      type: object
      required:
        - error
      properties:
        error:
          type: string

    Game:
      type: object
      properties:
        id:
          type: integer
        pack_name:
          type: string
        title:
          type: string
        min_players:
          type: integer
        max_players:
          type: integer
        length_minutes:
          type:
            - integer
            - "null"
        has_audience:
          type: integer
          enum: [0, 1]
        family_friendly:
          type: integer
          enum: [0, 1]
        game_type:
          type:
            - string
            - "null"
        secondary_type:
          type:
            - string
            - "null"
        play_count:
          type: integer
        popularity_score:
          type: integer
        upvotes:
          type: integer
        downvotes:
          type: integer
        enabled:
          type: integer
          enum: [0, 1]
        favor_bias:
          type: integer
          enum: [-1, 0, 1]
        created_at:
          type: string
          format: date-time

    Session:
      type: object
      properties:
        id:
          type: integer
        created_at:
          type: string
          format: date-time
        closed_at:
          type:
            - string
            - "null"
          format: date-time
        is_active:
          type: integer
          enum: [0, 1]
        notes:
          type:
            - string
            - "null"
        games_played:
          type: integer

    SessionGame:
      type: object
      properties:
        id:
          type: integer
        session_id:
          type: integer
        game_id:
          type: integer
        played_at:
          type: string
          format: date-time
        manually_added:
          type: integer
          enum: [0, 1]
        status:
          type: string
          enum: [playing, played, skipped]
        room_code:
          type:
            - string
            - "null"
        player_count:
          type:
            - integer
            - "null"
        player_count_check_status:
          type:
            - string
            - "null"
        pack_name:
          type: string
        title:
          type: string
        game_type:
          type:
            - string
            - "null"
        min_players:
          type: integer
        max_players:
          type: integer
        popularity_score:
          type: integer
        upvotes:
          type: integer
        downvotes:
          type: integer

    Pack:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        favor_bias:
          type: integer
          enum: [-1, 0, 1]
        created_at:
          type: string
          format: date-time

    PackMeta:
      type: object
      properties:
        name:
          type: string
        total_count:
          type: integer
        enabled_count:
          type: integer
        total_plays:
          type: integer

    Webhook:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        url:
          type: string
          format: uri
        events:
          type: array
          items:
            type: string
        enabled:
          type: boolean
        created_at:
          type: string
          format: date-time

    WebhookLog:
      type: object
      properties:
        id:
          type: integer
        webhook_id:
          type: integer
        event_type:
          type: string
        payload:
          type: object
        response_status:
          type:
            - integer
            - "null"
        error_message:
          type:
            - string
            - "null"
        created_at:
          type: string
          format: date-time

paths:
  /health:
    get:
      operationId: getHealth
      summary: Health check
      tags: [Health]
      responses:
        "200":
          description: API is running
          content:
            application/json:
              schema:
                type: object
                required: [status, message]
                properties:
                  status: { type: string, example: ok }
                  message: { type: string, example: "Jackbox Game Picker API is running" }

  /api/auth/login:
    post:
      operationId: loginWithAdminKey
      summary: Authenticate with admin key
      tags: [Auth]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [key]
              properties:
                key: { type: string }
      responses:
        "200":
          description: Authentication successful
          content:
            application/json:
              schema:
                type: object
                required: [token, message, expiresIn]
                properties:
                  token: { type: string }
                  message: { type: string, example: "Authentication successful" }
                  expiresIn: { type: string, example: "24h" }
        "400":
          description: Admin key is required
          content:
            application/json:
              schema:
                type: object
                required: [error]
                properties:
                  error: { type: string, example: "Admin key is required" }
        "401":
          description: Invalid admin key
          content:
            application/json:
              schema:
                type: object
                required: [error]
                properties:
                  error: { type: string, example: "Invalid admin key" }

  /api/auth/verify:
    post:
      operationId: verifyToken
      summary: Verify JWT token
      tags: [Auth]
      security: [{ bearerAuth: [] }]
      responses:
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "200":
          description: Token is valid
          content:
            application/json:
              schema:
                type: object
                required: [valid, user]
                properties:
                  valid: { type: boolean, example: true }
                  user:
                    type: object
                    properties:
                      role: { type: string, example: admin }
                      timestamp: { type: number }

  /api/games:
    get:
      operationId: listGames
      summary: List games with optional filters
      tags: [Games]
      parameters:
        - name: enabled
          in: query
          schema: { type: string, enum: ["true", "false"] }
        - name: playerCount
          in: query
          schema: { type: integer }
        - name: drawing
          in: query
          schema: { type: string, enum: ["only", "exclude"] }
        - name: length
          in: query
          schema: { type: string, enum: ["short", "medium", "long"] }
        - name: familyFriendly
          in: query
          schema: { type: string, enum: ["true", "false"] }
        - name: pack
          in: query
          schema: { type: string }
      responses:
        "200":
          description: Array of games ordered by pack_name, title
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Game" }
    post:
      operationId: createGame
      summary: Create a new game
      tags: [Games]
      security: [{ bearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [pack_name, title, min_players, max_players]
              properties:
                pack_name: { type: string }
                title: { type: string }
                min_players: { type: integer }
                max_players: { type: integer }
                length_minutes: { type: integer }
                has_audience: { type: boolean }
                family_friendly: { type: boolean }
                game_type: { type: string }
                secondary_type: { type: string }
      responses:
        "201":
          description: Game created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Game" }
        "400":
          description: Missing required fields
          content:
            application/json:
              schema:
                type: object
                required: [error]
                properties:
                  error: { type: string, example: "Missing required fields" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /api/games/packs:
    get:
      operationId: listPacks
      summary: List all packs
      tags: [Games]
      responses:
        "200":
          description: Array of packs
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Pack" }

  /api/games/meta/packs:
    get:
      operationId: listPackMeta
      summary: List pack metadata
      tags: [Games]
      responses:
        "200":
          description: Array of pack metadata
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/PackMeta" }

  /api/games/export/csv:
    get:
      operationId: exportGamesCsv
      summary: Export games as CSV
      tags: [Games]
      security: [{ bearerAuth: [] }]
      responses:
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "200":
          description: CSV file download
          headers:
            Content-Disposition:
              schema:
                type: string
                example: 'attachment; filename="jackbox-games.csv"'
          content:
            text/csv:
              schema:
                type: string
                description: CSV with columns Pack Name, Title, Min Players, Max Players, Length (minutes), Audience, Family Friendly, Game Type, Secondary Type

  /api/games/import/csv:
    post:
      operationId: importGamesCsv
      summary: Import games from CSV
      tags: [Games]
      security: [{ bearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [csvData]
              properties:
                csvData: { type: string }
                mode: { type: string, enum: ["append", "replace"], description: Optional; defaults to append if absent }
      responses:
        "200":
          description: Import successful
          content:
            application/json:
              schema:
                type: object
                required: [message, count, mode]
                properties:
                  message: { type: string }
                  count: { type: integer }
                  mode: { type: string }
        "400":
          description: CSV data required
          content:
            application/json:
              schema:
                type: object
                required: [error]
                properties:
                  error: { type: string, example: "CSV data required" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /api/games/packs/{name}/favor:
    patch:
      operationId: updatePackFavor
      summary: Update pack favor bias
      tags: [Games]
      security: [{ bearerAuth: [] }]
      parameters:
        - name: name
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [favor_bias]
              properties:
                favor_bias: { type: integer, enum: [-1, 0, 1] }
      responses:
        "200":
          description: Pack favor bias updated
          content:
            application/json:
              schema:
                type: object
                required: [message, favor_bias]
                properties:
                  message: { type: string, example: "Pack favor bias updated successfully" }
                  favor_bias: { type: integer }
        "400":
          description: Invalid favor_bias
          content:
            application/json:
              schema:
                type: object
                required: [error]
                properties:
                  error: { type: string, example: "favor_bias must be 1 (favor), -1 (disfavor), or 0 (neutral)" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /api/games/packs/{name}/toggle:
    patch:
      operationId: togglePack
      summary: Enable or disable a pack
      tags: [Games]
      security: [{ bearerAuth: [] }]
      parameters:
        - name: name
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [enabled]
              properties:
                enabled: { type: boolean }
      responses:
        "200":
          description: Pack enabled/disabled
          content:
            application/json:
              schema:
                type: object
                required: [message, gamesAffected]
                properties:
                  message: { type: string }
                  gamesAffected: { type: integer }
        "400":
          description: enabled status required
          content:
            application/json:
              schema:
                type: object
                required: [error]
                properties:
                  error: { type: string, example: "enabled status required" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /api/games/{id}:
    get:
      operationId: getGame
      summary: Get a game by ID
      tags: [Games]
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer }
      responses:
        "200":
          description: Game found
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Game" }
        "404":
          description: Game not found
          content:
            application/json:
              schema:
                type: object
                required: [error]
                properties:
                  error: { type: string, example: "Game not found" }
    put:
      operationId: updateGame
      summary: Update a game
      tags: [Games]
      security: [{ bearerAuth: [] }]
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                pack_name: { type: string }
                title: { type: string }
                min_players: { type: integer }
                max_players: { type: integer }
                length_minutes: { type: integer }
                has_audience: { type: boolean }
                family_friendly: { type: boolean }
                game_type: { type: string }
                secondary_type: { type: string }
                enabled: { type: boolean }
      responses:
        "200":
          description: Game updated
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Game" }
        "404":
          description: Game not found
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    delete:
      operationId: deleteGame
      summary: Delete a game
      tags: [Games]
      security: [{ bearerAuth: [] }]
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer }
      responses:
        "200":
          description: Game deleted
          content:
            application/json:
              schema:
                type: object
                required: [message]
                properties:
                  message: { type: string, example: "Game deleted successfully" }
        "404":
          description: Game not found
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /api/games/{id}/toggle:
    patch:
      operationId: toggleGame
      summary: Toggle game enabled status
      tags: [Games]
      security: [{ bearerAuth: [] }]
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer }
      responses:
        "200":
          description: Game updated
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Game" }
        "404":
          description: Game not found
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /api/games/{id}/favor:
    patch:
      operationId: updateGameFavor
      summary: Update game favor bias
      tags: [Games]
      security: [{ bearerAuth: [] }]
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [favor_bias]
              properties:
                favor_bias: { type: integer, enum: [-1, 0, 1] }
      responses:
        "200":
          description: Favor bias updated
          content:
            application/json:
              schema:
                type: object
                required: [message, favor_bias]
                properties:
                  message: { type: string, example: "Favor bias updated successfully" }
                  favor_bias: { type: integer }
        "400":
          description: Invalid favor_bias
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "404":
          description: Game not found
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /api/sessions:
    get:
      operationId: listSessions
      summary: List all sessions
      tags: [Sessions]
      responses:
        "200":
          description: Array of sessions with games_played
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Session" }
    post:
      operationId: createSession
      summary: Create a new session
      tags: [Sessions]
      security: [{ bearerAuth: [] }]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                notes: { type: string }
      responses:
        "201":
          description: Session created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Session" }
        "400":
          description: Active session already exists
          content:
            application/json:
              schema:
                type: object
                required: [error, activeSessionId]
                properties:
                  error: { type: string }
                  activeSessionId: { type: integer }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /api/sessions/active:
    get:
      operationId: getActiveSession
      summary: Get the active session
      tags: [Sessions]
      responses:
        "200":
          description: Active session (raw object) or null with message when no session
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/Session"
                  - type: object
                    required: [session, message]
                    properties:
                      session: { type: "null" }
                      message: { type: string, example: "No active session" }

  /api/sessions/{id}:
    get:
      operationId: getSession
      summary: Get a session by ID
      tags: [Sessions]
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer }
      responses:
        "200":
          description: Session found
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Session" }
        "404":
          description: Session not found
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
    delete:
      operationId: deleteSession
      summary: Delete a session
      tags: [Sessions]
      security: [{ bearerAuth: [] }]
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer }
      responses:
        "200":
          description: Session deleted
          content:
            application/json:
              schema:
                type: object
                required: [message, sessionId]
                properties:
                  message: { type: string, example: "Session deleted successfully" }
                  sessionId: { type: integer }
        "404":
          description: Session not found
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "400":
          description: Cannot delete active session
          content:
            application/json:
              schema:
                type: object
                required: [error]
                properties:
                  error: { type: string, example: "Cannot delete an active session. Please close it first." }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /api/sessions/{id}/close:
    post:
      operationId: closeSession
      summary: Close a session
      tags: [Sessions]
      security: [{ bearerAuth: [] }]
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                notes: { type: string }
      responses:
        "200":
          description: Session closed with games_played
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Session" }
        "404":
          description: Session not found
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "400":
          description: Session already closed
          content:
            application/json:
              schema:
                type: object
                required: [error]
                properties:
                  error: { type: string, example: "Session is already closed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /api/sessions/{id}/games:
    get:
      operationId: listSessionGames
      summary: List games in a session
      tags: [Sessions]
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer }
      responses:
        "200":
          description: Array of session games
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/SessionGame" }
        "404":
          description: Session not found
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
    post:
      operationId: addGameToSession
      summary: Add a game to a session
      tags: [Sessions]
      security: [{ bearerAuth: [] }]
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [game_id]
              properties:
                game_id: { type: integer }
                manually_added: { type: boolean }
                room_code: { type: string }
      responses:
        "201":
          description: Game added to session
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SessionGame" }
        "400":
          description: Closed session or missing game_id
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "404":
          description: Session or game not found
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /api/sessions/{id}/votes:
    get:
      operationId: getSessionVotes
      summary: Get per-game vote breakdown for a session
      tags: [Sessions]
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer }
          description: Session ID
      responses:
        "200":
          description: Per-game vote aggregates for the session
          content:
            application/json:
              schema:
                type: object
                required: [session_id, votes]
                properties:
                  session_id:
                    type: integer
                  votes:
                    type: array
                    items:
                      type: object
                      properties:
                        game_id: { type: integer }
                        title: { type: string }
                        pack_name: { type: string }
                        upvotes: { type: integer }
                        downvotes: { type: integer }
                        net_score: { type: integer }
                        total_votes: { type: integer }
        "404":
          description: Session not found
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

  /api/sessions/{id}/chat-import:
    post:
      operationId: importSessionChat
      summary: Import chat log for vote processing
      tags: [Sessions]
      security: [{ bearerAuth: [] }]
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [chatData]
              properties:
                chatData:
                  type: array
                  items:
                    type: object
                    required: [username, message, timestamp]
                    properties:
                      username: { type: string }
                      message: { type: string }
                      timestamp: { type: string }
      responses:
        "200":
          description: Chat imported and processed
          content:
            application/json:
              schema:
                type: object
                required: [message, messagesImported, duplicatesSkipped, votesProcessed, votesByGame, debug]
                properties:
                  message: { type: string }
                  messagesImported: { type: integer }
                  duplicatesSkipped: { type: integer }
                  votesProcessed: { type: integer }
                  votesByGame: { type: object }
                  debug: { type: object }
        "400":
          description: |
            Two distinct cases:
            - "chatData must be an array" — when chatData is missing or not an array
            - "No games played in this session to match votes against" — when session has no games
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "404":
          description: Session not found
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /api/sessions/{id}/export:
    get:
      operationId: exportSession
      summary: Export session
      tags: [Sessions]
      security: [{ bearerAuth: [] }]
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer }
        - name: format
          in: query
          schema: { type: string, enum: ["json", "txt"], default: "txt" }
      responses:
        "200":
          description: File download
          content:
            application/json:
              schema: { type: object }
            text/plain:
              schema: { type: string }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /api/sessions/{sessionId}/games/{sessionGameId}/status:
    patch:
      operationId: updateSessionGameStatus
      summary: Update session game status
      tags: [Sessions]
      security: [{ bearerAuth: [] }]
      parameters:
        - name: sessionId
          in: path
          required: true
          schema: { type: integer }
        - name: sessionGameId
          in: path
          required: true
          description: Session-game row ID (session_games.id), not games.id
          schema: { type: integer }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [status]
              properties:
                status: { type: string, enum: [playing, played, skipped] }
      responses:
        "200":
          description: Status updated
          content:
            application/json:
              schema:
                type: object
                required: [message, status]
                properties:
                  message: { type: string, example: "Status updated successfully" }
                  status: { type: string }
        "400":
          description: Invalid status
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "404":
          description: Not found
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /api/sessions/{sessionId}/games/{sessionGameId}:
    delete:
      operationId: removeGameFromSession
      summary: Remove game from session
      tags: [Sessions]
      security: [{ bearerAuth: [] }]
      parameters:
        - name: sessionId
          in: path
          required: true
          schema: { type: integer }
        - name: sessionGameId
          in: path
          required: true
          description: Session-game row ID (session_games.id), not games.id
          schema: { type: integer }
      responses:
        "200":
          description: Game removed
          content:
            application/json:
              schema:
                type: object
                required: [message]
                properties:
                  message: { type: string, example: "Game removed from session successfully" }
        "404":
          description: Not found
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /api/sessions/{sessionId}/games/{sessionGameId}/room-code:
    patch:
      operationId: updateSessionGameRoomCode
      summary: Update room code for session game
      tags: [Sessions]
      security: [{ bearerAuth: [] }]
      parameters:
        - name: sessionId
          in: path
          required: true
          schema: { type: integer }
        - name: sessionGameId
          in: path
          required: true
          description: Session-game row ID (session_games.id), not games.id
          schema: { type: integer }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [room_code]
              properties:
                room_code: { type: string, pattern: "^[A-Z0-9]{4}$" }
      responses:
        "200":
          description: Room code updated
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SessionGame" }
        "400":
          description: Missing or invalid room code format
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "404":
          description: Not found
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /api/sessions/{sessionId}/games/{sessionGameId}/start-player-check:
    post:
      operationId: startPlayerCheck
      summary: Start room monitor for player count
      tags: [Sessions]
      security: [{ bearerAuth: [] }]
      parameters:
        - name: sessionId
          in: path
          required: true
          schema: { type: integer }
        - name: sessionGameId
          in: path
          required: true
          description: Session-game row ID (session_games.id), not games.id
          schema: { type: integer }
      responses:
        "200":
          description: Room monitor started
          content:
            application/json:
              schema:
                type: object
                required: [message, status]
                properties:
                  message: { type: string, example: "Room monitor started" }
                  status: { type: string, example: monitoring }
        "400":
          description: Game does not have a room code
          content:
            application/json:
              schema:
                type: object
                required: [error]
                properties:
                  error: { type: string, example: "Game does not have a room code" }
        "404":
          description: Not found
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /api/sessions/{sessionId}/games/{sessionGameId}/stop-player-check:
    post:
      operationId: stopPlayerCheck
      summary: Stop room monitor
      tags: [Sessions]
      security: [{ bearerAuth: [] }]
      parameters:
        - name: sessionId
          in: path
          required: true
          schema: { type: integer }
        - name: sessionGameId
          in: path
          required: true
          description: Session-game row ID (session_games.id), not games.id
          schema: { type: integer }
      responses:
        "200":
          description: Monitor stopped
          content:
            application/json:
              schema:
                type: object
                required: [message, status]
                properties:
                  message: { type: string, example: "Room monitor and player count check stopped" }
                  status: { type: string, example: stopped }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /api/sessions/{sessionId}/games/{sessionGameId}/player-count:
    patch:
      operationId: updateSessionGamePlayerCount
      summary: Update player count for session game
      tags: [Sessions]
      security: [{ bearerAuth: [] }]
      parameters:
        - name: sessionId
          in: path
          required: true
          schema: { type: integer }
        - name: sessionGameId
          in: path
          required: true
          description: Session-game row ID (session_games.id), not games.id
          schema: { type: integer }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [player_count]
              properties:
                player_count: { type: integer, minimum: 0 }
      responses:
        "200":
          description: Player count updated
          content:
            application/json:
              schema:
                type: object
                required: [message, player_count]
                properties:
                  message: { type: string, example: "Player count updated successfully" }
                  player_count: { type: integer }
        "400":
          description: Missing or invalid player count
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "404":
          description: Not found
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /api/pick:
    post:
      operationId: pickRandomGame
      summary: Pick a random game with optional filters
      tags: [Picker]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                playerCount: { type: integer }
                drawing: { type: string }
                length: { type: string }
                familyFriendly: { type: boolean }
                sessionId: { type: integer }
                excludePlayed: { type: boolean }
      responses:
        "200":
          description: Random game picked
          content:
            application/json:
              schema:
                type: object
                required: [game, poolSize, totalEnabled]
                properties:
                  game: { $ref: "#/components/schemas/Game" }
                  poolSize: { type: integer }
                  totalEnabled: { type: integer }
        "404":
          description: No matching games
          content:
            application/json:
              schema:
                type: object
                required: [error]
                properties:
                  error: { type: string }
                  suggestion: { type: string }
                  recentlyPlayed: { type: array, items: { type: integer } }

  /api/stats:
    get:
      operationId: getStats
      summary: Get aggregate statistics
      tags: [Stats]
      responses:
        "200":
          description: Stats object
          content:
            application/json:
              schema:
                type: object
                required: [games, gamesEnabled, packs, sessions, activeSessions, totalGamesPlayed, mostPlayedGames, topRatedGames]
                properties:
                  games: { type: object, properties: { count: { type: integer } } }
                  gamesEnabled: { type: object, properties: { count: { type: integer } } }
                  packs: { type: object, properties: { count: { type: integer } } }
                  sessions: { type: object, properties: { count: { type: integer } } }
                  activeSessions: { type: object, properties: { count: { type: integer } } }
                  totalGamesPlayed: { type: object, properties: { count: { type: integer } } }
                  mostPlayedGames:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: integer }
                        title: { type: string }
                        pack_name: { type: string }
                        play_count: { type: integer }
                        popularity_score: { type: integer }
                        upvotes: { type: integer }
                        downvotes: { type: integer }
                  topRatedGames:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: integer }
                        title: { type: string }
                        pack_name: { type: string }
                        play_count: { type: integer }
                        popularity_score: { type: integer }
                        upvotes: { type: integer }
                        downvotes: { type: integer }

  /api/votes:
    get:
      operationId: listVotes
      summary: Paginated vote history with filtering
      tags: [Votes]
      parameters:
        - name: session_id
          in: query
          schema: { type: integer }
          description: Filter by session
        - name: game_id
          in: query
          schema: { type: integer }
          description: Filter by game
        - name: username
          in: query
          schema: { type: string }
          description: Filter by voter
        - name: vote_type
          in: query
          schema: { type: string, enum: [up, down] }
          description: Filter by direction
        - name: page
          in: query
          schema: { type: integer, default: 1 }
          description: Page number
        - name: limit
          in: query
          schema: { type: integer, default: 50, maximum: 100 }
          description: Results per page (max 100)
      responses:
        "200":
          description: Paginated vote records
          content:
            application/json:
              schema:
                type: object
                required: [votes, pagination]
                properties:
                  votes:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: integer }
                        session_id: { type: integer }
                        game_id: { type: integer }
                        game_title: { type: string }
                        pack_name: { type: string }
                        username: { type: string }
                        vote_type: { type: string, enum: [up, down] }
                        timestamp: { type: string, format: date-time }
                        created_at: { type: string, format: date-time }
                  pagination:
                    type: object
                    properties:
                      page: { type: integer }
                      limit: { type: integer }
                      total: { type: integer }
                      total_pages: { type: integer }
        "400":
          description: Invalid filter parameter
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

  /api/votes/live:
    post:
      operationId: recordLiveVote
      summary: Record a live vote (up/down)
      tags: [Votes]
      security: [{ bearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [username, vote, timestamp]
              properties:
                username: { type: string }
                vote: { type: string, enum: [up, down] }
                timestamp: { type: string, format: date-time, description: ISO 8601 }
      responses:
        "200":
          description: Vote recorded
          content:
            application/json:
              schema:
                type: object
                required: [success, message, session, game, vote]
                properties:
                  success: { type: boolean, example: true }
                  message: { type: string, example: "Vote recorded successfully" }
                  session:
                    type: object
                    properties:
                      id: { type: integer }
                      games_played: { type: integer }
                  game:
                    type: object
                    properties:
                      id: { type: integer }
                      title: { type: string }
                      upvotes: { type: integer }
                      downvotes: { type: integer }
                      popularity_score: { type: integer }
                  vote:
                    type: object
                    properties:
                      username: { type: string }
                      type: { type: string }
                      timestamp: { type: string }
        "400":
          description: Missing fields, invalid vote, or invalid timestamp
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "404":
          description: No active session, no games, or no matching game (vote timestamp doesn't match)
          content:
            application/json:
              schema:
                type: object
                required: [error]
                properties:
                  error: { type: string }
                  debug:
                    type: object
                    description: Present when vote timestamp doesn't match any game
                    properties:
                      voteTimestamp: { type: string }
                      sessionGames:
                        type: array
                        items:
                          type: object
                          properties:
                            title: { type: string }
                            played_at: { type: string }
        "409":
          description: Duplicate vote within 1 second
          content:
            application/json:
              schema:
                type: object
                required: [error]
                properties:
                  error: { type: string }
                  message: { type: string }
                  timeSinceLastVote: { type: number }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /api/webhooks:
    get:
      operationId: listWebhooks
      summary: List all webhooks
      tags: [Webhooks]
      security: [{ bearerAuth: [] }]
      responses:
        "200":
          description: Array of webhooks
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Webhook" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    post:
      operationId: createWebhook
      summary: Create a webhook
      tags: [Webhooks]
      security: [{ bearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, url, secret, events]
              properties:
                name: { type: string }
                url: { type: string, format: uri }
                secret: { type: string }
                events:
                  type: array
                  items: { type: string }
      responses:
        "201":
          description: Webhook created
          content:
            application/json:
              schema:
                allOf:
                  - { $ref: "#/components/schemas/Webhook" }
                  - type: object
                    properties:
                      message: { type: string, example: "Webhook created successfully" }
        "400":
          description: Invalid input
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /api/webhooks/{id}:
    get:
      operationId: getWebhook
      summary: Get a webhook by ID
      tags: [Webhooks]
      security: [{ bearerAuth: [] }]
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer }
      responses:
        "200":
          description: Webhook found
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Webhook" }
        "404":
          description: Webhook not found
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    patch:
      operationId: updateWebhook
      summary: Update a webhook
      tags: [Webhooks]
      security: [{ bearerAuth: [] }]
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
                url: { type: string, format: uri }
                secret: { type: string }
                events:
                  type: array
                  items: { type: string }
                enabled: { type: boolean }
      responses:
        "200":
          description: Webhook updated
          content:
            application/json:
              schema:
                allOf:
                  - { $ref: "#/components/schemas/Webhook" }
                  - type: object
                    properties:
                      message: { type: string, example: "Webhook updated successfully" }
        "400":
          description: No fields, invalid URL, or events not array
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "404":
          description: Webhook not found
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    delete:
      operationId: deleteWebhook
      summary: Delete a webhook
      tags: [Webhooks]
      security: [{ bearerAuth: [] }]
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer }
      responses:
        "200":
          description: Webhook deleted
          content:
            application/json:
              schema:
                type: object
                required: [message, webhookId]
                properties:
                  message: { type: string, example: "Webhook deleted successfully" }
                  webhookId: { type: integer }
        "404":
          description: Webhook not found
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

  /api/webhooks/test/{id}:
    post:
      operationId: testWebhook
      summary: Send test webhook
      tags: [Webhooks]
      security: [{ bearerAuth: [] }]
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer }
      responses:
        "200":
          description: Test webhook sent
          content:
            application/json:
              schema:
                type: object
                required: [message, note]
                properties:
                  message: { type: string, example: "Test webhook sent" }
                  note: { type: string, example: "Check webhook_logs table for delivery status" }
        "404":
          description: Webhook not found
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /api/webhooks/{id}/logs:
    get:
      operationId: listWebhookLogs
      summary: List webhook delivery logs
      tags: [Webhooks]
      security: [{ bearerAuth: [] }]
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer }
        - name: limit
          in: query
          schema: { type: integer, default: 50 }
      responses:
        "200":
          description: Array of webhook logs
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/WebhookLog" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
