openapi: 3.0.3
info:
  title: Lorewizard API
  version: 1.0.0
  description: |
    JSON REST API for the Lorewizard worldbuilding app. All routes require `Authorization: Bearer` matching
    the Worker secret `API_BEARER_TOKEN` (see `docs/cloudflare-access-and-api-bearer.md`). The HTML admin UI
    (`/admin`) is separate and not described here.

servers:
  - url: https://{workerHost}
    description: Cloudflare Workers deployment (replace with your hostname).
    variables:
      workerHost:
        default: example.workers.dev

security:
  - bearerAuth: []

tags:
  - name: Health
  - name: Settings
  - name: Eras
  - name: Gods
  - name: Places
  - name: Characters
  - name: Loots
  - name: Evidences
  - name: Timelines

paths:
  /api/v1:
    get:
      tags: [Health]
      summary: API health
      operationId: getApiV1Health
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /api/v1/settings:
    get:
      tags: [Settings]
      summary: List settings
      operationId: listSettings
      parameters:
        - $ref: "#/components/parameters/QueryQ"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SettingListResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      tags: [Settings]
      summary: Create setting
      operationId: createSetting
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SettingPayload"
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SettingOneResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /api/v1/settings/{id}:
    parameters:
      - $ref: "#/components/parameters/PathId"
    get:
      tags: [Settings]
      summary: Get setting by id
      operationId: getSetting
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SettingOneResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    put:
      tags: [Settings]
      summary: Update setting
      operationId: updateSetting
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SettingPayload"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SettingOneResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    delete:
      tags: [Settings]
      summary: Delete setting
      operationId: deleteSetting
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeleteSuccess"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /api/v1/eras:
    get:
      tags: [Eras]
      summary: List eras
      operationId: listEras
      parameters:
        - $ref: "#/components/parameters/QueryQ"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EraListResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      tags: [Eras]
      summary: Create era
      operationId: createEra
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EraPayload"
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EraOneResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /api/v1/eras/{id}:
    parameters:
      - $ref: "#/components/parameters/PathId"
    get:
      tags: [Eras]
      summary: Get era by id
      operationId: getEra
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EraOneResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    put:
      tags: [Eras]
      summary: Update era
      operationId: updateEra
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EraPayload"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EraOneResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    delete:
      tags: [Eras]
      summary: Delete era
      operationId: deleteEra
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeleteSuccess"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /api/v1/gods:
    get:
      tags: [Gods]
      summary: List gods
      operationId: listGods
      parameters:
        - name: setting_id
          in: query
          schema:
            type: integer
            minimum: 1
        - name: category
          in: query
          schema:
            type: string
        - $ref: "#/components/parameters/QueryQ"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GodListResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      tags: [Gods]
      summary: Create god
      operationId: createGod
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/GodPayload"
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GodOneResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /api/v1/gods/{id}:
    parameters:
      - $ref: "#/components/parameters/PathId"
    get:
      tags: [Gods]
      summary: Get god by id
      operationId: getGod
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GodOneResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    put:
      tags: [Gods]
      summary: Update god
      operationId: updateGod
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/GodPayload"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GodOneResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    delete:
      tags: [Gods]
      summary: Delete god
      operationId: deleteGod
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeleteSuccess"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /api/v1/places:
    get:
      tags: [Places]
      summary: List places
      operationId: listPlaces
      parameters:
        - name: type
          in: query
          schema:
            $ref: "#/components/schemas/PlaceType"
        - name: status
          in: query
          schema:
            $ref: "#/components/schemas/PlaceStatus"
        - name: setting_id
          in: query
          schema:
            type: integer
            minimum: 1
        - $ref: "#/components/parameters/QueryQ"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PlaceListResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      tags: [Places]
      summary: Create place
      operationId: createPlace
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PlacePayload"
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PlaceOneResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /api/v1/places/{id}:
    parameters:
      - $ref: "#/components/parameters/PathId"
    get:
      tags: [Places]
      summary: Get place by id
      operationId: getPlace
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PlaceOneResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    put:
      tags: [Places]
      summary: Update place
      operationId: updatePlace
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PlacePayload"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PlaceOneResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    delete:
      tags: [Places]
      summary: Delete place
      operationId: deletePlace
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeleteSuccess"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /api/v1/characters:
    get:
      tags: [Characters]
      summary: List characters
      operationId: listCharacters
      parameters:
        - name: status
          in: query
          schema:
            $ref: "#/components/schemas/CharacterStatus"
        - name: disposition
          in: query
          schema:
            $ref: "#/components/schemas/CharacterDisposition"
        - name: setting_id
          in: query
          schema:
            type: integer
            minimum: 1
        - $ref: "#/components/parameters/QueryQ"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CharacterListResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      tags: [Characters]
      summary: Create character
      operationId: createCharacter
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CharacterPayload"
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CharacterOneResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /api/v1/characters/{id}:
    parameters:
      - $ref: "#/components/parameters/PathId"
    get:
      tags: [Characters]
      summary: Get character by id
      operationId: getCharacter
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CharacterOneResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    put:
      tags: [Characters]
      summary: Update character
      operationId: updateCharacter
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CharacterPayload"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CharacterOneResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    delete:
      tags: [Characters]
      summary: Delete character
      operationId: deleteCharacter
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeleteSuccess"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /api/v1/loots:
    get:
      tags: [Loots]
      summary: List loots
      operationId: listLoots
      parameters:
        - name: type
          in: query
          schema:
            $ref: "#/components/schemas/LootType"
        - name: rarity
          in: query
          schema:
            $ref: "#/components/schemas/LootRarity"
        - name: nature
          in: query
          schema:
            $ref: "#/components/schemas/LootNature"
        - $ref: "#/components/parameters/QueryQ"
        - name: setting_id
          in: query
          schema:
            type: integer
            minimum: 1
        - name: found_in_place_id
          in: query
          schema:
            type: integer
            minimum: 1
        - name: owner_character_id
          in: query
          schema:
            type: integer
            minimum: 1
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LootListResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      tags: [Loots]
      summary: Create loot
      operationId: createLoot
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LootPayloadRequest"
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LootOneResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /api/v1/loots/{id}:
    parameters:
      - $ref: "#/components/parameters/PathId"
    get:
      tags: [Loots]
      summary: Get loot by id
      operationId: getLoot
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LootOneResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    put:
      tags: [Loots]
      summary: Update loot
      operationId: updateLoot
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LootPayloadRequest"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LootOneResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    delete:
      tags: [Loots]
      summary: Delete loot
      operationId: deleteLoot
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeleteSuccess"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /api/v1/evidences:
    get:
      tags: [Evidences]
      summary: List evidences
      operationId: listEvidences
      parameters:
        - name: format
          in: query
          schema:
            $ref: "#/components/schemas/EvidenceFormat"
        - name: status
          in: query
          schema:
            $ref: "#/components/schemas/EvidenceStatus"
        - $ref: "#/components/parameters/QueryQ"
        - name: setting_id
          in: query
          schema:
            type: integer
            minimum: 1
        - name: credibility
          in: query
          description: Filter by credibility 1–5.
          schema:
            type: integer
            minimum: 1
            maximum: 5
        - name: found_in_place_id
          in: query
          schema:
            type: integer
            minimum: 1
        - name: found_by_character_id
          in: query
          schema:
            type: integer
            minimum: 1
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EvidenceListResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      tags: [Evidences]
      summary: Create evidence
      operationId: createEvidence
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EvidencePayloadRequest"
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EvidenceOneResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /api/v1/evidences/{id}:
    parameters:
      - $ref: "#/components/parameters/PathId"
    get:
      tags: [Evidences]
      summary: Get evidence by id
      operationId: getEvidence
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EvidenceOneResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    put:
      tags: [Evidences]
      summary: Update evidence
      operationId: updateEvidence
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EvidencePayloadRequest"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EvidenceOneResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    delete:
      tags: [Evidences]
      summary: Delete evidence
      operationId: deleteEvidence
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeleteSuccess"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /api/v1/timelines:
    get:
      tags: [Timelines]
      summary: List timelines
      operationId: listTimelines
      parameters:
        - $ref: "#/components/parameters/QueryQ"
        - name: setting_id
          in: query
          schema:
            type: integer
            minimum: 1
        - name: era_id
          in: query
          schema:
            type: integer
            minimum: 1
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TimelineListResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      tags: [Timelines]
      summary: Create timeline
      operationId: createTimeline
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TimelinePayload"
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TimelineOneResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /api/v1/timelines/{id}:
    parameters:
      - $ref: "#/components/parameters/PathId"
    get:
      tags: [Timelines]
      summary: Get timeline by id
      operationId: getTimeline
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TimelineOneResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    put:
      tags: [Timelines]
      summary: Update timeline
      operationId: updateTimeline
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TimelinePayload"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TimelineOneResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    delete:
      tags: [Timelines]
      summary: Delete timeline
      operationId: deleteTimeline
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeleteSuccess"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: Must match Worker secret `API_BEARER_TOKEN`.

  parameters:
    PathId:
      name: id
      in: path
      required: true
      description: Positive integer primary key.
      schema:
        type: integer
        minimum: 1
    QueryQ:
      name: q
      in: query
      required: false
      description: Search / filter string (semantics per resource).
      schema:
        type: string

  responses:
    Unauthorized:
      description: Missing or invalid Bearer token
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/UnauthorizedBody"
    BadRequest:
      description: Invalid id, path, or JSON payload
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorBody"
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorBody"

  schemas:
    UnauthorizedBody:
      type: object
      required: [error]
      properties:
        error:
          type: string
          example: Unauthorized
    ErrorBody:
      type: object
      required: [error]
      properties:
        error:
          type: string
    HealthResponse:
      type: object
      required: [status, version]
      properties:
        status:
          type: string
          example: ok
        version:
          type: string
          example: v1
    DeleteSuccess:
      type: object
      required: [success]
      properties:
        success:
          type: boolean
          example: true

    Setting:
      type: object
      required: [id, created_at, updated_at]
      properties:
        id:
          type: integer
        slug:
          type: string
          nullable: true
        title:
          type: string
          nullable: true
        description:
          type: string
          nullable: true
        created_at:
          type: string
        updated_at:
          type: string
    SettingPayload:
      type: object
      properties:
        slug:
          type: string
          nullable: true
        title:
          type: string
          nullable: true
        description:
          type: string
          nullable: true
    SettingListResponse:
      type: object
      required: [data]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/Setting"
    SettingOneResponse:
      type: object
      required: [data]
      properties:
        data:
          $ref: "#/components/schemas/Setting"

    Era:
      type: object
      required: [id, created_at, updated_at]
      properties:
        id:
          type: integer
        slug:
          type: string
          nullable: true
        title:
          type: string
          nullable: true
        description:
          type: string
          nullable: true
        created_at:
          type: string
        updated_at:
          type: string
    EraPayload:
      type: object
      properties:
        slug:
          type: string
          nullable: true
        title:
          type: string
          nullable: true
        description:
          type: string
          nullable: true
    EraListResponse:
      type: object
      required: [data]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/Era"
    EraOneResponse:
      type: object
      required: [data]
      properties:
        data:
          $ref: "#/components/schemas/Era"

    God:
      type: object
      required: [id, setting_id, created_at, updated_at]
      properties:
        id:
          type: integer
        setting_id:
          type: integer
        slug:
          type: string
          nullable: true
        title:
          type: string
          nullable: true
        category:
          type: string
          nullable: true
        domain:
          type: string
          nullable: true
        description:
          type: string
          nullable: true
        created_at:
          type: string
        updated_at:
          type: string
    GodPayload:
      type: object
      required: [setting_id]
      properties:
        setting_id:
          type: integer
          minimum: 1
        slug:
          type: string
          nullable: true
        title:
          type: string
          nullable: true
        category:
          type: string
          nullable: true
        domain:
          type: string
          nullable: true
        description:
          type: string
          nullable: true
    GodListResponse:
      type: object
      required: [data]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/God"
    GodOneResponse:
      type: object
      required: [data]
      properties:
        data:
          $ref: "#/components/schemas/God"

    PlaceType:
      type: string
      enum:
        - settlement
        - facility
        - geographic
        - anomaly
        - transit
        - ruin
    PlaceStatus:
      type: string
      enum:
        - known
        - rumored
        - restricted
        - destroyed
    Place:
      type: object
      required:
        - id
        - setting_id
        - name
        - type
        - status
        - points_of_interest
        - notable_npcs
        - sessions_appeared
        - tags
        - created_at
        - updated_at
      properties:
        id:
          type: integer
        setting_id:
          type: integer
        slug:
          type: string
          nullable: true
        name:
          type: string
        type:
          $ref: "#/components/schemas/PlaceType"
        status:
          $ref: "#/components/schemas/PlaceStatus"
        description:
          type: string
          nullable: true
        faction:
          type: string
          nullable: true
        hazard_level:
          type: integer
          nullable: true
          minimum: 1
          maximum: 5
        points_of_interest:
          type: string
        notable_npcs:
          type: string
        sessions_appeared:
          type: string
        tags:
          type: string
        created_at:
          type: string
        updated_at:
          type: string
    PlacePayload:
      type: object
      required: [setting_id, name, type]
      properties:
        setting_id:
          type: integer
          minimum: 1
        slug:
          type: string
          nullable: true
        name:
          type: string
        type:
          $ref: "#/components/schemas/PlaceType"
        status:
          $ref: "#/components/schemas/PlaceStatus"
        description:
          type: string
          nullable: true
        faction:
          type: string
          nullable: true
        hazard_level:
          type: integer
          nullable: true
          minimum: 1
          maximum: 5
    PlaceListResponse:
      type: object
      required: [data]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/Place"
    PlaceOneResponse:
      type: object
      required: [data]
      properties:
        data:
          $ref: "#/components/schemas/Place"

    CharacterNature:
      type: string
      enum: [biological, hybrid, synthetic]
    CharacterDisposition:
      type: string
      enum: [hostile, suspicious, neutral, friendly]
    CharacterStatus:
      type: string
      enum: [alive, dead, missing, unknown]
    Character:
      type: object
      required:
        - id
        - setting_id
        - slug
        - name
        - status
        - notable_abilities
        - bonds
        - tags
        - created_at
        - updated_at
      properties:
        id:
          type: integer
        setting_id:
          type: integer
        slug:
          type: string
        name:
          type: string
        nature:
          $ref: "#/components/schemas/CharacterNature"
          nullable: true
        role:
          type: string
          nullable: true
        disposition:
          $ref: "#/components/schemas/CharacterDisposition"
          nullable: true
        threat_level:
          type: integer
          nullable: true
          minimum: 1
          maximum: 5
        status:
          $ref: "#/components/schemas/CharacterStatus"
        description:
          type: string
          nullable: true
        faction:
          type: string
          nullable: true
        location_place_id:
          type: integer
          nullable: true
        first_seen:
          type: integer
          nullable: true
        secret:
          type: string
          nullable: true
        notable_abilities:
          type: string
        bonds:
          type: string
        tags:
          type: string
        created_at:
          type: string
        updated_at:
          type: string
    CharacterPayload:
      type: object
      required: [setting_id, slug, name]
      properties:
        setting_id:
          type: integer
          minimum: 1
        slug:
          type: string
        name:
          type: string
        nature:
          $ref: "#/components/schemas/CharacterNature"
          nullable: true
        role:
          type: string
          nullable: true
        disposition:
          $ref: "#/components/schemas/CharacterDisposition"
          nullable: true
        threat_level:
          type: integer
          nullable: true
          minimum: 1
          maximum: 5
        status:
          $ref: "#/components/schemas/CharacterStatus"
        description:
          type: string
          nullable: true
        faction:
          type: string
          nullable: true
        location_place_id:
          type: integer
          nullable: true
        first_seen:
          type: integer
          nullable: true
        secret:
          type: string
          nullable: true
    CharacterListResponse:
      type: object
      required: [data]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/Character"
    CharacterOneResponse:
      type: object
      required: [data]
      properties:
        data:
          $ref: "#/components/schemas/Character"

    LootType:
      type: string
      enum:
        - ordnance
        - protection
        - utility
        - consumable
        - intel
        - asset
        - relic
    LootRarity:
      type: string
      enum:
        - common
        - uncommon
        - rare
        - very_rare
        - legendary
        - unique
    LootNature:
      type: string
      enum:
        - physical
        - technological
        - arcane
        - eldritch
        - hybrid
    Loot:
      type: object
      required:
        - id
        - setting_id
        - slug
        - name
        - type
        - rarity
        - nature
        - description
        - properties
        - tags
        - created_at
        - updated_at
      properties:
        id:
          type: integer
        setting_id:
          type: integer
        slug:
          type: string
        name:
          type: string
        type:
          $ref: "#/components/schemas/LootType"
        rarity:
          $ref: "#/components/schemas/LootRarity"
        nature:
          $ref: "#/components/schemas/LootNature"
        description:
          type: string
        properties:
          type: string
          description: JSON string (array of strings) stored in D1.
        tags:
          type: string
          description: JSON string (array of strings) stored in D1.
        found_in_place_id:
          type: integer
          nullable: true
        found_during:
          type: integer
          nullable: true
        owner_character_id:
          type: integer
          nullable: true
        requirement:
          type: string
          nullable: true
        cost_of_use:
          type: string
          nullable: true
        created_at:
          type: string
        updated_at:
          type: string
    LootPayloadRequest:
      type: object
      required:
        - setting_id
        - slug
        - name
        - type
        - rarity
        - nature
        - description
        - properties
        - tags
      properties:
        setting_id:
          type: integer
          minimum: 1
        slug:
          type: string
        name:
          type: string
        type:
          $ref: "#/components/schemas/LootType"
        rarity:
          $ref: "#/components/schemas/LootRarity"
        nature:
          $ref: "#/components/schemas/LootNature"
        description:
          type: string
        properties:
          description: JSON array of strings, or a stringified JSON array.
          oneOf:
            - type: string
            - type: array
              items:
                type: string
        tags:
          description: JSON array of strings, or a stringified JSON array.
          oneOf:
            - type: string
            - type: array
              items:
                type: string
        found_in_place_id:
          type: integer
          nullable: true
        found_during:
          type: integer
          nullable: true
        owner_character_id:
          type: integer
          nullable: true
        requirement:
          type: string
          nullable: true
        cost_of_use:
          type: string
          nullable: true
    LootListResponse:
      type: object
      required: [data]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/Loot"
    LootOneResponse:
      type: object
      required: [data]
      properties:
        data:
          $ref: "#/components/schemas/Loot"

    EvidenceFormat:
      type: string
      enum: [physical, digital, sensory, esoteric]
    EvidenceStatus:
      type: string
      enum: [unresolved, deciphered, obfuscated]
    EvidenceLinksTo:
      type: object
      required: [places, npcs, items]
      properties:
        places:
          type: array
          items:
            type: string
        npcs:
          type: array
          items:
            type: string
        items:
          type: array
          items:
            type: string
    Evidence:
      type: object
      required:
        - id
        - setting_id
        - slug
        - name
        - format
        - status
        - credibility
        - description
        - discovery_method
        - session_revealed
        - links_to
        - tags
        - created_at
        - updated_at
      properties:
        id:
          type: integer
        setting_id:
          type: integer
        slug:
          type: string
        name:
          type: string
        format:
          $ref: "#/components/schemas/EvidenceFormat"
        status:
          $ref: "#/components/schemas/EvidenceStatus"
        credibility:
          type: integer
          minimum: 1
          maximum: 5
        description:
          type: string
        discovery_method:
          type: string
        session_revealed:
          type: integer
          minimum: 0
        links_to:
          type: string
          description: 'JSON string with object shape: places, npcs, items (each string arrays).'
        found_in_place_id:
          type: integer
          nullable: true
        found_by_character_id:
          type: integer
          nullable: true
        tags:
          type: string
          description: JSON string (array of strings) stored in D1.
        created_at:
          type: string
        updated_at:
          type: string
    EvidencePayloadRequest:
      type: object
      required:
        - setting_id
        - slug
        - name
        - format
        - status
        - credibility
        - description
        - discovery_method
        - session_revealed
        - links_to
        - tags
      properties:
        setting_id:
          type: integer
          minimum: 1
        slug:
          type: string
        name:
          type: string
        format:
          $ref: "#/components/schemas/EvidenceFormat"
        status:
          $ref: "#/components/schemas/EvidenceStatus"
        credibility:
          type: integer
          minimum: 1
          maximum: 5
        description:
          type: string
        discovery_method:
          type: string
        session_revealed:
          type: integer
          minimum: 0
        links_to:
          description: Object with places/npcs/items string arrays, or a JSON string / value accepted by the server normalizer.
          oneOf:
            - $ref: "#/components/schemas/EvidenceLinksTo"
            - type: string
        tags:
          description: JSON array of strings, or a stringified JSON array.
          oneOf:
            - type: string
            - type: array
              items:
                type: string
        found_in_place_id:
          type: integer
          nullable: true
        found_by_character_id:
          type: integer
          nullable: true
    EvidenceListResponse:
      type: object
      required: [data]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/Evidence"
    EvidenceOneResponse:
      type: object
      required: [data]
      properties:
        data:
          $ref: "#/components/schemas/Evidence"

    Timeline:
      type: object
      required: [id, setting_id, era_id, created_at, updated_at]
      properties:
        id:
          type: integer
        setting_id:
          type: integer
        era_id:
          type: integer
        slug:
          type: string
          nullable: true
        title:
          type: string
          nullable: true
        ref:
          type: integer
          nullable: true
        description:
          type: string
          nullable: true
        created_at:
          type: string
        updated_at:
          type: string
    TimelinePayload:
      type: object
      required: [setting_id, era_id]
      properties:
        setting_id:
          type: integer
          minimum: 1
        era_id:
          type: integer
          minimum: 1
        slug:
          type: string
          nullable: true
        title:
          type: string
          nullable: true
        ref:
          type: integer
          nullable: true
        description:
          type: string
          nullable: true
    TimelineListResponse:
      type: object
      required: [data]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/Timeline"
    TimelineOneResponse:
      type: object
      required: [data]
      properties:
        data:
          $ref: "#/components/schemas/Timeline"
