openapi: 3.0.3
info:
  title: NowChess Board API
  description: |
    REST API for the NowChess application. Designed to feel familiar to users
    of the [lichess API](https://lichess.org/api).

    ## Authentication
    Most endpoints require a Bearer token:
    ```
    Authorization: Bearer <token>
    ```
    Authentication is reserved for future implementation — endpoints are currently
    open unless noted otherwise.

    ## Move notation
    Moves are expressed in **UCI notation**: `{from}{to}[promotion]`
    - Normal move: `e2e4`
    - Capture: `d5e6`
    - Promotion: `e7e8q` (q=queen, r=rook, b=bishop, n=knight)
    - Castling: `e1g1` (kingside white), `e1c1` (queenside white)

    ## Streaming
    Endpoints that support streaming return **NDJSON** (newline-delimited JSON).
    Request them with:
    ```
    Accept: application/x-ndjson
    ```
    Each line of the response is a complete JSON object. Empty lines are
    keep-alive heartbeats.

    ## Rate limiting
    Requests that exceed the rate limit receive `429 Too Many Requests`.
    Honour the `Retry-After` response header and wait before retrying.
  version: 1.0.0
  contact:
    name: NowChess
  license:
    name: MIT

servers:
  - url: http://localhost:8080
    description: Local development server

tags:
  - name: game
    description: Create and manage chess games
  - name: move
    description: Make moves and navigate game history
  - name: draw
    description: Draw offers and claims
  - name: import
    description: Load a game from FEN or PGN
  - name: export
    description: Export a game as FEN or PGN

paths:

  # ---------------------------------------------------------------------------
  # Game lifecycle
  # ---------------------------------------------------------------------------

  /api/board/game:
    post:
      operationId: createGame
      tags: [game]
      summary: Create a new game
      description: |
        Creates a new chess game starting from the initial position.
        Returns the full game state including the generated `gameId`.
      security:
        - bearerAuth: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateGameRequest'
      responses:
        '201':
          description: Game created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GameFull'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  /api/board/game/{gameId}:
    get:
      operationId: getGame
      tags: [game]
      summary: Get game state
      description: Returns the full current state of a game.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/gameId'
      responses:
        '200':
          description: Current game state
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GameFull'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  /api/board/game/{gameId}/stream:
    get:
      operationId: streamGame
      tags: [game]
      summary: Stream game events
      description: |
        Opens a persistent NDJSON stream for a game. The first object sent is
        a `gameFull` event containing the complete game state. Subsequent
        objects are `gameState` events sent whenever the game changes (move
        made, draw offered, game over, etc.).

        Empty lines are heartbeats to keep the connection alive.

        Connect with:
        ```
        Accept: application/x-ndjson
        ```
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/gameId'
      responses:
        '200':
          description: NDJSON event stream
          content:
            application/x-ndjson:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/GameFullEvent'
                  - $ref: '#/components/schemas/GameStateEvent'
                  - $ref: '#/components/schemas/ErrorEvent'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  /api/board/game/{gameId}/resign:
    post:
      operationId: resignGame
      tags: [game]
      summary: Resign the game
      description: The active player resigns. The game ends immediately.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/gameId'
      responses:
        '200':
          description: Resignation accepted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OkResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  # ---------------------------------------------------------------------------
  # Move-making
  # ---------------------------------------------------------------------------

  /api/board/game/{gameId}/move/{uci}:
    post:
      operationId: makeMove
      tags: [move]
      summary: Make a move
      description: |
        Submit a move in UCI notation. The move must be legal for the side
        currently to move.

        For promotion moves include the target piece as the fifth character:
        `e7e8q`, `a2a1r`, etc. Promotion moves without the fifth character
        are rejected with `400 INVALID_MOVE`.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/gameId'
        - name: uci
          in: path
          required: true
          description: Move in UCI notation (e.g. `e2e4`, `e7e8q`)
          schema:
            type: string
            pattern: '^[a-h][1-8][a-h][1-8][qrbn]?$'
            example: e2e4
      responses:
        '200':
          description: Move applied — returns updated game state
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GameState'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  /api/board/game/{gameId}/moves:
    get:
      operationId: getLegalMoves
      tags: [move]
      summary: Get legal moves
      description: |
        Returns all legal moves for the side currently to move.
        Optionally filter to moves originating from a single square.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/gameId'
        - name: square
          in: query
          required: false
          description: Filter to moves from this square (e.g. `e2`)
          schema:
            type: string
            pattern: '^[a-h][1-8]$'
            example: e2
      responses:
        '200':
          description: List of legal moves
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LegalMovesResponse'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  /api/board/game/{gameId}/undo:
    post:
      operationId: undoMove
      tags: [move]
      summary: Undo the last move
      description: Reverts the most recent move. Returns the updated game state.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/gameId'
      responses:
        '200':
          description: Move undone
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GameState'
        '400':
          description: No moves to undo
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  /api/board/game/{gameId}/redo:
    post:
      operationId: redoMove
      tags: [move]
      summary: Redo a previously undone move
      description: Re-applies the next move in the undo stack. Returns the updated game state.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/gameId'
      responses:
        '200':
          description: Move redone
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GameState'
        '400':
          description: No moves to redo
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  # ---------------------------------------------------------------------------
  # Draw handling
  # ---------------------------------------------------------------------------

  /api/board/game/{gameId}/draw/{action}:
    post:
      operationId: drawAction
      tags: [draw]
      summary: Offer, accept, decline, or claim a draw
      description: |
        Perform a draw-related action:

        | Action    | Description |
        |-----------|-------------|
        | `offer`   | Offer a draw to the opponent |
        | `accept`  | Accept the opponent's draw offer |
        | `decline` | Decline the opponent's draw offer |
        | `claim`   | Claim a draw under the fifty-move rule (only valid when `status` is `fiftyMoveAvailable`) |
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/gameId'
        - name: action
          in: path
          required: true
          schema:
            type: string
            enum: [offer, accept, decline, claim]
      responses:
        '200':
          description: Action accepted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OkResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  # ---------------------------------------------------------------------------
  # Import
  # ---------------------------------------------------------------------------

  /api/board/game/import/fen:
    post:
      operationId: importFen
      tags: [import]
      summary: Load a position from FEN
      description: |
        Creates a new game from a FEN string. The game starts at the position
        described by the FEN; move history prior to that position is not
        available.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ImportFenRequest'
      responses:
        '201':
          description: Game created from FEN
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GameFull'
        '400':
          $ref: '#/components/responses/BadRequest'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  /api/board/game/import/pgn:
    post:
      operationId: importPgn
      tags: [import]
      summary: Load a game from PGN
      description: |
        Creates a new game by replaying all moves in a PGN string. The game
        starts at the position after the final move in the PGN; undo is
        available for every replayed move.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ImportPgnRequest'
      responses:
        '201':
          description: Game created from PGN
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GameFull'
        '400':
          $ref: '#/components/responses/BadRequest'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  # ---------------------------------------------------------------------------
  # Export
  # ---------------------------------------------------------------------------

  /api/board/game/{gameId}/export/fen:
    get:
      operationId: exportFen
      tags: [export]
      summary: Export current position as FEN
      description: Returns the FEN string representing the current board position.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/gameId'
      responses:
        '200':
          description: FEN string
          content:
            text/plain:
              schema:
                type: string
                example: rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  /api/board/game/{gameId}/export/pgn:
    get:
      operationId: exportPgn
      tags: [export]
      summary: Export game as PGN
      description: Returns the full PGN for the game including headers and move text.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/gameId'
      responses:
        '200':
          description: PGN text
          content:
            application/x-chess-pgn:
              schema:
                type: string
                example: |
                  [Event "NowChess game"]
                  [White "Player1"]
                  [Black "Player2"]
                  [Result "*"]

                  1. e4 e5 2. Nf3 *
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'

# =============================================================================
# Components
# =============================================================================

components:

  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: 'Personal access token — `Authorization: Bearer <token>`'

  parameters:
    gameId:
      name: gameId
      in: path
      required: true
      description: 8-character alphanumeric game ID (e.g. `Qa7FJNk2`)
      schema:
        type: string
        pattern: '^[A-Za-z0-9]{8}$'
        example: Qa7FJNk2

  responses:
    BadRequest:
      description: Invalid input
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiError'
    Unauthorized:
      description: Missing or invalid authentication token
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiError'
    NotFound:
      description: Game not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiError'
    TooManyRequests:
      description: Rate limit exceeded — see `Retry-After` header
      headers:
        Retry-After:
          description: Seconds to wait before retrying
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiError'

  schemas:

    # -------------------------------------------------------------------------
    # Requests
    # -------------------------------------------------------------------------

    CreateGameRequest:
      type: object
      description: Parameters for creating a new game. All fields are optional.
      properties:
        white:
          $ref: '#/components/schemas/PlayerInfo'
        black:
          $ref: '#/components/schemas/PlayerInfo'

    ImportFenRequest:
      type: object
      required: [fen]
      properties:
        fen:
          type: string
          description: Complete FEN string (6 fields)
          example: rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1
        white:
          $ref: '#/components/schemas/PlayerInfo'
        black:
          $ref: '#/components/schemas/PlayerInfo'

    ImportPgnRequest:
      type: object
      required: [pgn]
      properties:
        pgn:
          type: string
          description: PGN text (headers and move list)
          example: "1. e4 e5 2. Nf3 Nc6 *"

    # -------------------------------------------------------------------------
    # Game state
    # -------------------------------------------------------------------------

    GameFull:
      type: object
      description: Complete game information including players and current state.
      required: [gameId, white, black, state]
      properties:
        gameId:
          type: string
          description: Unique 8-character game identifier
          example: Qa7FJNk2
        white:
          $ref: '#/components/schemas/PlayerInfo'
        black:
          $ref: '#/components/schemas/PlayerInfo'
        state:
          $ref: '#/components/schemas/GameState'

    GameState:
      type: object
      description: |
        The current game state. Included in `GameFull` and returned by move
        endpoints and stream events.
      required: [fen, pgn, turn, status, moves, undoAvailable, redoAvailable]
      properties:
        fen:
          type: string
          description: FEN string for the current position
          example: rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1
        pgn:
          type: string
          description: PGN move text for the full game so far
          example: "1. e4"
        turn:
          type: string
          enum: [white, black]
          description: The side to move
        status:
          $ref: '#/components/schemas/GameStatus'
        winner:
          type: string
          enum: [white, black]
          description: Set when `status` is `checkmate` or `resign`
          nullable: true
        moves:
          type: array
          description: All moves played so far, in UCI notation
          items:
            type: string
          example: [e2e4, e7e5, g1f3]
        undoAvailable:
          type: boolean
          description: Whether `POST /undo` is currently valid
        redoAvailable:
          type: boolean
          description: Whether `POST /redo` is currently valid

    GameStatus:
      type: string
      description: |
        Current game status:

        | Value | Meaning |
        |-------|---------|
        | `started` | Game in progress, no special condition |
        | `check` | Side to move is in check |
        | `checkmate` | Side to move is checkmated — game over |
        | `stalemate` | Side to move has no legal moves, not in check — game over (draw) |
        | `resign` | A player resigned — game over |
        | `draw` | Draw agreed or claimed — game over |
        | `drawOffered` | Waiting for the opponent to accept or decline a draw offer |
        | `fiftyMoveAvailable` | Fifty-move rule threshold reached; active player may claim draw |
        | `insufficientMaterial` | Neither side has enough pieces to deliver checkmate — game over (draw) |
      enum:
        - started
        - check
        - checkmate
        - stalemate
        - resign
        - draw
        - drawOffered
        - fiftyMoveAvailable
        - insufficientMaterial

    # -------------------------------------------------------------------------
    # Moves
    # -------------------------------------------------------------------------

    LegalMovesResponse:
      type: object
      required: [moves]
      properties:
        moves:
          type: array
          items:
            $ref: '#/components/schemas/LegalMove'

    LegalMove:
      type: object
      required: [from, to, uci, moveType]
      properties:
        from:
          type: string
          description: Origin square in algebraic notation
          example: e2
        to:
          type: string
          description: Destination square in algebraic notation
          example: e4
        uci:
          type: string
          description: Full move in UCI notation
          example: e2e4
        moveType:
          $ref: '#/components/schemas/MoveType'
        promotion:
          type: string
          enum: [queen, rook, bishop, knight]
          description: Target piece for promotion moves
          nullable: true

    MoveType:
      type: string
      description: Classification of the move
      enum:
        - normal
        - capture
        - castleKingside
        - castleQueenside
        - enPassant
        - promotion

    # -------------------------------------------------------------------------
    # Streaming events
    # -------------------------------------------------------------------------

    GameFullEvent:
      type: object
      description: |
        First event on a game stream. Contains the complete game snapshot.
      required: [type, game]
      properties:
        type:
          type: string
          enum: [gameFull]
        game:
          $ref: '#/components/schemas/GameFull'

    GameStateEvent:
      type: object
      description: |
        Emitted on a game stream whenever the game state changes (move played,
        draw offered, game over, etc.).
      required: [type, state]
      properties:
        type:
          type: string
          enum: [gameState]
        state:
          $ref: '#/components/schemas/GameState'

    ErrorEvent:
      type: object
      description: Emitted on a game stream when an error occurs.
      required: [type, error]
      properties:
        type:
          type: string
          enum: [error]
        error:
          $ref: '#/components/schemas/ApiError'

    # -------------------------------------------------------------------------
    # Shared types
    # -------------------------------------------------------------------------

    PlayerInfo:
      type: object
      required: [id, displayName]
      properties:
        id:
          type: string
          description: Unique player identifier
          example: player1
        displayName:
          type: string
          description: Human-readable display name
          example: Alice

    OkResponse:
      type: object
      required: [ok]
      properties:
        ok:
          type: boolean
          enum: [true]

    ApiError:
      type: object
      required: [code, message]
      properties:
        code:
          type: string
          description: Machine-readable error code
          example: INVALID_MOVE
        message:
          type: string
          description: Human-readable error description
          example: e2e5 is not a legal move
        field:
          type: string
          description: Request field that caused the error, if applicable
          example: uci
          nullable: true
