openapi: "3.0.3"
info:
  title: Lauschfreunde Nexus API
  description: |
    Backend API for the Lauschfreunde interactive audio adventure platform.
    Provides public game endpoints, authentication, and admin CRUD operations
    for managing boxes, characters, adventures, cards, file uploads, and TTS.
  version: "1.0.0"

servers:
  - url: https://cddevapps.com/api/nexus/lauschfreunde
    description: Production

tags:
  - name: Public
    description: Public game endpoints (no auth required)
  - name: Auth
    description: Authentication
  - name: Admin - Boxes
    description: Box CRUD (requires Bearer token)
  - name: Admin - Characters
    description: Character CRUD (requires Bearer token)
  - name: Admin - Adventures
    description: Adventure CRUD (requires Bearer token)
  - name: Admin - Cards
    description: Card CRUD (requires Bearer token)
  - name: Admin - Solution Placements
    description: Manage solution placements for adventures
  - name: Admin - Special Sounds
    description: Manage special sounds for adventures
  - name: Admin - Uploads
    description: File upload endpoints (audio, image, PDF)
  - name: Admin - TTS
    description: ElevenLabs TTS proxy endpoints

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

  schemas:
    # ── Envelope ──────────────────────────────────────────
    SuccessResponse:
      type: object
      properties:
        data:
          description: Response payload (varies per endpoint)
      required:
        - data

    ErrorResponse:
      type: object
      properties:
        error:
          type: string
          example: "Unauthorized"
      required:
        - error

    # ── Domain Objects ────────────────────────────────────
    Box:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
          example: "Abenteuer im Hoerwald"
        name_en:
          type: string
          nullable: true
          example: "Adventures in Hoerwald"
        slug:
          type: string
          example: "abenteuer-im-hoerwald"
        description:
          type: string
          nullable: true
          example: "Ein magischer Wald voller Abenteuer"
        cover_image_url:
          type: string
          nullable: true
          example: "https://cddevapps.com/api/nexus/lauschfreunde/files/images/abenteuer-im-hoerwald/cover.jpg"
        primary_color:
          type: string
          nullable: true
          example: "#7C3AED"
        accent_color:
          type: string
          nullable: true
          example: "#F59E0B"
        characters:
          type: array
          items:
            $ref: "#/components/schemas/Character"
        adventures:
          type: array
          items:
            $ref: "#/components/schemas/Adventure"

    BoxSummary:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        name_en:
          type: string
          nullable: true
        slug:
          type: string
        description:
          type: string
          nullable: true
        cover_image_url:
          type: string
          nullable: true
        primary_color:
          type: string
          nullable: true
        accent_color:
          type: string
          nullable: true

    Character:
      type: object
      properties:
        id:
          type: string
          format: uuid
        box_id:
          type: string
          format: uuid
        name:
          type: string
          example: "Eule Elara"
        slug:
          type: string
          example: "eule-elara"
        emoji:
          type: string
          example: "\U0001F989"
        color:
          type: string
          example: "#6B3FA0"
        sort_order:
          type: integer
          example: 0
        avatar_url:
          type: string
          nullable: true
        elevenlabs_voice_id:
          type: string
          nullable: true
          description: ElevenLabs voice ID for TTS
          example: "pNInz6obpgDQGcFmaJgB"
        voice_settings:
          type: object
          nullable: true
          description: ElevenLabs voice settings overrides
          properties:
            stability:
              type: number
              example: 0.5
            similarity_boost:
              type: number
              example: 0.75
            style:
              type: number
              example: 0.0
            use_speaker_boost:
              type: boolean
              example: true

    Adventure:
      type: object
      properties:
        id:
          type: string
          format: uuid
        box_id:
          type: string
          format: uuid
        title:
          type: string
          example: "Geburtstagsparty"
        slug:
          type: string
          example: "geburtstagsparty"
        sort_order:
          type: integer
          example: 1
        description:
          type: string
          nullable: true
        primary_color:
          type: string
          nullable: true
          description: Adventure-level color override
          example: "#7C3AED"
        accent_color:
          type: string
          nullable: true
          description: Adventure-level accent color override
          example: "#F59E0B"
        background_image_url:
          type: string
          nullable: true
          description: Background image for the adventure
        intro_audio_url:
          type: string
          nullable: true
          description: URL to the intro narration audio
        intro_script_text:
          type: string
          nullable: true
          description: Script text for intro TTS generation
        finale_audio_url:
          type: string
          nullable: true
          description: URL to the finale narration audio
        finale_script_text:
          type: string
          nullable: true
          description: Script text for finale TTS generation
        narrator_voice_id:
          type: string
          nullable: true
          description: ElevenLabs voice ID for intro/finale narration
        solution_image_url:
          type: string
          nullable: true
          description: Solution image for print materials
        solution_app_image_url:
          type: string
          nullable: true
          description: Solution image optimised for in-app display
        solution_audio_url:
          type: string
          nullable: true
          description: Audio played on the solution reveal page
        solution_placements:
          type: array
          nullable: true
          items:
            $ref: "#/components/schemas/SolutionPlacement"
        asset_pdf_url:
          type: string
          nullable: true
          description: URL to the game material PDF
        certificate_easy_pdf_url:
          type: string
          nullable: true
          description: Certificate PDF for easy difficulty
        certificate_hard_pdf_url:
          type: string
          nullable: true
          description: Certificate PDF for hard difficulty
        cards:
          type: array
          items:
            $ref: "#/components/schemas/Card"

    Card:
      type: object
      properties:
        id:
          type: string
          format: uuid
        adventure_id:
          type: string
          format: uuid
        character_id:
          type: string
          format: uuid
        slug:
          type: string
          example: "eule-elara-1"
        scene_title:
          type: string
          nullable: true
          example: "Der fruehe Morgen"
        strand_order:
          type: integer
          description: Order within the character strand (1-3)
          example: 1
        global_order:
          type: integer
          description: Global timeline order across all cards
          example: 1
        is_milestone:
          type: boolean
          description: Whether this card is a milestone card
          example: false
        hint_text:
          type: string
          nullable: true
          description: Hint shown when card is assigned to wrong character
        script_text:
          type: string
          nullable: true
          description: TTS script text for this card
        audio_url:
          type: string
          nullable: true
          description: URL to the card audio file
        scene_image_url:
          type: string
          nullable: true
          description: Scene illustration image
        qr_payload:
          type: string
          description: QR code payload string (auto-generated)
          example: "LF:abenteuer-im-hoerwald:geburtstagsparty:eule-elara-1"

    SolutionPlacement:
      type: object
      properties:
        character_id:
          type: string
          format: uuid
        slot_index:
          type: integer
          description: Quadrant index (0-3)
          example: 0

    SpecialSound:
      type: object
      properties:
        event:
          type: string
          description: Event identifier
          example: "correct_assign"
        audio_url:
          type: string
          description: URL to the sound effect audio file

    UploadResult:
      type: object
      properties:
        url:
          type: string
          description: Public URL of the uploaded file
          example: "https://cddevapps.com/api/nexus/lauschfreunde/files/audio/abenteuer-im-hoerwald/geburtstagsparty/cards/eule-elara-1.mp3"

    TTSGenerateAndSaveResult:
      type: object
      properties:
        audio_url:
          type: string
          description: Public URL of the saved audio file

    TTSUsage:
      type: object
      properties:
        character_count:
          type: integer
          description: Characters used this billing period
        character_limit:
          type: integer
          description: Character limit for this billing period

    CrossoverBonus:
      type: object
      properties:
        message:
          type: string
          example: "Ein Gast aus einer anderen Welt!"
        bonus_audio_url:
          type: string
          nullable: true

    # ── Request Bodies ────────────────────────────────────
    BoxCreate:
      type: object
      required:
        - name
        - slug
      properties:
        name:
          type: string
          example: "Abenteuer im Hoerwald"
        slug:
          type: string
          example: "abenteuer-im-hoerwald"
        description:
          type: string
          nullable: true

    BoxUpdate:
      type: object
      properties:
        name:
          type: string
        name_en:
          type: string
          nullable: true
        description:
          type: string
          nullable: true
        primary_color:
          type: string
          nullable: true
        accent_color:
          type: string
          nullable: true

    CharacterCreate:
      type: object
      required:
        - name
        - slug
        - emoji
        - color
      properties:
        name:
          type: string
          example: "Eule Elara"
        slug:
          type: string
          example: "eule-elara"
        emoji:
          type: string
          example: "\U0001F989"
        color:
          type: string
          example: "#6B3FA0"
        sort_order:
          type: integer
          example: 0

    CharacterUpdate:
      type: object
      properties:
        name:
          type: string
        slug:
          type: string
        emoji:
          type: string
        color:
          type: string
        sort_order:
          type: integer
        avatar_url:
          type: string
          nullable: true
        elevenlabs_voice_id:
          type: string
          nullable: true
        voice_settings:
          type: object
          nullable: true
          properties:
            stability:
              type: number
            similarity_boost:
              type: number
            style:
              type: number
            use_speaker_boost:
              type: boolean

    AdventureCreate:
      type: object
      required:
        - title
        - slug
      properties:
        title:
          type: string
          example: "Geburtstagsparty"
        slug:
          type: string
          example: "geburtstagsparty"
        sort_order:
          type: integer
          example: 1

    AdventureUpdate:
      type: object
      properties:
        title:
          type: string
        slug:
          type: string
        sort_order:
          type: integer
        description:
          type: string
          nullable: true
        primary_color:
          type: string
          nullable: true
        accent_color:
          type: string
          nullable: true
        background_image_url:
          type: string
          nullable: true
        intro_audio_url:
          type: string
          nullable: true
        intro_script_text:
          type: string
          nullable: true
        finale_audio_url:
          type: string
          nullable: true
        finale_script_text:
          type: string
          nullable: true
        narrator_voice_id:
          type: string
          nullable: true
        solution_image_url:
          type: string
          nullable: true
        solution_app_image_url:
          type: string
          nullable: true
        solution_audio_url:
          type: string
          nullable: true
        asset_pdf_url:
          type: string
          nullable: true
        certificate_easy_pdf_url:
          type: string
          nullable: true
        certificate_hard_pdf_url:
          type: string
          nullable: true

    CardCreate:
      type: object
      required:
        - slug
        - character_id
        - strand_order
        - global_order
      properties:
        slug:
          type: string
          example: "eule-elara-1"
        scene_title:
          type: string
          example: "Der fruehe Morgen"
        character_id:
          type: string
          format: uuid
        strand_order:
          type: integer
          example: 1
        global_order:
          type: integer
          example: 1
        is_milestone:
          type: boolean
          example: false
        hint_text:
          type: string
          nullable: true
        script_text:
          type: string
          nullable: true

    CardUpdate:
      type: object
      properties:
        slug:
          type: string
        scene_title:
          type: string
          nullable: true
        character_id:
          type: string
          format: uuid
        strand_order:
          type: integer
        global_order:
          type: integer
        is_milestone:
          type: boolean
        hint_text:
          type: string
          nullable: true
        script_text:
          type: string
          nullable: true
        audio_url:
          type: string
          nullable: true
        scene_image_url:
          type: string
          nullable: true

    SolutionPlacementsUpdate:
      type: object
      required:
        - placements
      properties:
        placements:
          type: array
          items:
            $ref: "#/components/schemas/SolutionPlacement"

    SpecialSoundsUpdate:
      type: object
      required:
        - sounds
      properties:
        sounds:
          type: array
          items:
            $ref: "#/components/schemas/SpecialSound"

    TTSGenerateRequest:
      type: object
      required:
        - text
        - voice_id
      properties:
        text:
          type: string
          description: Text to synthesise
          example: "Hallo, ich bin Eule Elara!"
        voice_id:
          type: string
          description: ElevenLabs voice ID
          example: "pNInz6obpgDQGcFmaJgB"
        voice_settings:
          type: object
          properties:
            stability:
              type: number
            similarity_boost:
              type: number
            style:
              type: number
            use_speaker_boost:
              type: boolean

    TTSGenerateAndSaveRequest:
      type: object
      required:
        - text
        - voice_id
        - save_path
      properties:
        text:
          type: string
          description: Text to synthesise
        voice_id:
          type: string
          description: ElevenLabs voice ID
        voice_settings:
          type: object
          properties:
            stability:
              type: number
            similarity_boost:
              type: number
            style:
              type: number
            use_speaker_boost:
              type: boolean
        save_path:
          type: string
          description: Storage path for the generated audio file
          example: "abenteuer-im-hoerwald/geburtstagsparty/cards/eule-elara-1.mp3"
        card_id:
          type: string
          format: uuid
          nullable: true
          description: Optional card ID to auto-update with the generated audio URL

    TTSSoundEffectRequest:
      type: object
      required:
        - prompt
      properties:
        prompt:
          type: string
          description: Descriptive prompt for the sound effect
          example: "magical sparkle chime"
        duration_seconds:
          type: number
          description: Desired duration in seconds
          example: 3.0

    LoginRequest:
      type: object
      required:
        - email
        - password
      properties:
        email:
          type: string
          format: email
          example: "admin@lauschfreunde.de"
        password:
          type: string
          format: password

    LoginResponse:
      type: object
      properties:
        token:
          type: string
          description: JWT bearer token

paths:
  # ══════════════════════════════════════════════════════
  #  PUBLIC ENDPOINTS
  # ══════════════════════════════════════════════════════

  /boxes:
    get:
      tags: [Public]
      summary: List all boxes
      description: Returns a summary list of all published boxes (without nested adventures/cards).
      operationId: listBoxes
      responses:
        "200":
          description: List of boxes
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/BoxSummary"

  /boxes/{slug}:
    get:
      tags: [Public]
      summary: Get a box by slug
      description: |
        Returns a single box with its full data including characters, adventures,
        cards, and solution placements. Used by the game client to load all
        data for a box.
      operationId: getBox
      parameters:
        - name: slug
          in: path
          required: true
          description: Box slug or ID
          schema:
            type: string
            example: "abenteuer-im-hoerwald"
      responses:
        "200":
          description: Full box data
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Box"
        "404":
          description: Box not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /cards/resolve:
    get:
      tags: [Public]
      summary: Resolve a QR code payload
      description: |
        Resolves a scanned QR payload string to the corresponding card.
        Used by the game client when a player scans a QR code.
      operationId: resolveQR
      parameters:
        - name: qr
          in: query
          required: true
          description: QR code payload string
          schema:
            type: string
            example: "LF:abenteuer-im-hoerwald:geburtstagsparty:eule-elara-1"
      responses:
        "200":
          description: Resolved card data
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Card"
        "404":
          description: Card not found for this QR payload
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /crossover-bonus:
    get:
      tags: [Public]
      summary: Get crossover bonus
      description: |
        Returns bonus content when a card from one box is scanned while
        playing an adventure in a different box.
      operationId: getCrossoverBonus
      parameters:
        - name: source_box
          in: query
          required: true
          description: Slug of the box the scanned card belongs to
          schema:
            type: string
        - name: target_box
          in: query
          required: true
          description: Slug of the box currently being played
          schema:
            type: string
        - name: adventure
          in: query
          required: true
          description: Slug of the adventure currently being played
          schema:
            type: string
      responses:
        "200":
          description: Crossover bonus content
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/CrossoverBonus"
        "404":
          description: No crossover bonus available
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  # ══════════════════════════════════════════════════════
  #  AUTH
  # ══════════════════════════════════════════════════════

  /auth/login:
    post:
      tags: [Auth]
      summary: Admin login
      description: Authenticates an admin user and returns a JWT bearer token.
      operationId: login
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LoginRequest"
      responses:
        "200":
          description: Login successful
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/LoginResponse"
        "401":
          description: Invalid credentials
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  # ══════════════════════════════════════════════════════
  #  ADMIN: BOXES
  # ══════════════════════════════════════════════════════

  /admin/boxes:
    post:
      tags: [Admin - Boxes]
      summary: Create a box
      operationId: createBox
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/BoxCreate"
      responses:
        "201":
          description: Box created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Box"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /admin/boxes/{id}:
    put:
      tags: [Admin - Boxes]
      summary: Update a box
      operationId: updateBox
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/BoxUpdate"
      responses:
        "200":
          description: Box updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Box"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Box not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

    delete:
      tags: [Admin - Boxes]
      summary: Delete a box
      description: Deletes a box and all associated adventures, cards, and files.
      operationId: deleteBox
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Box deleted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessResponse"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Box not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  # ══════════════════════════════════════════════════════
  #  ADMIN: CHARACTERS
  # ══════════════════════════════════════════════════════

  /admin/boxes/{boxId}/characters:
    post:
      tags: [Admin - Characters]
      summary: Create a character
      description: Creates a new character within a box (max 4 per box).
      operationId: createCharacter
      security:
        - BearerAuth: []
      parameters:
        - name: boxId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CharacterCreate"
      responses:
        "201":
          description: Character created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Character"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /admin/characters/{id}:
    put:
      tags: [Admin - Characters]
      summary: Update a character
      operationId: updateCharacter
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CharacterUpdate"
      responses:
        "200":
          description: Character updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Character"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Character not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

    delete:
      tags: [Admin - Characters]
      summary: Delete a character
      operationId: deleteCharacter
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Character deleted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessResponse"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Character not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  # ══════════════════════════════════════════════════════
  #  ADMIN: ADVENTURES
  # ══════════════════════════════════════════════════════

  /admin/boxes/{boxId}/adventures:
    post:
      tags: [Admin - Adventures]
      summary: Create an adventure
      description: Creates a new adventure within a box.
      operationId: createAdventure
      security:
        - BearerAuth: []
      parameters:
        - name: boxId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AdventureCreate"
      responses:
        "201":
          description: Adventure created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Adventure"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /admin/adventures/{id}:
    put:
      tags: [Admin - Adventures]
      summary: Update an adventure
      description: |
        Updates adventure fields. Supports all adventure properties including
        audio URLs, script texts, solution fields, PDF URLs, and color overrides.
      operationId: updateAdventure
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AdventureUpdate"
      responses:
        "200":
          description: Adventure updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Adventure"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Adventure not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

    delete:
      tags: [Admin - Adventures]
      summary: Delete an adventure
      description: Deletes an adventure and all its cards and associated files.
      operationId: deleteAdventure
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Adventure deleted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessResponse"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Adventure not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  # ══════════════════════════════════════════════════════
  #  ADMIN: CARDS
  # ══════════════════════════════════════════════════════

  /admin/adventures/{advId}/cards:
    post:
      tags: [Admin - Cards]
      summary: Create a card
      description: Creates a new card within an adventure (max 3 per character).
      operationId: createCard
      security:
        - BearerAuth: []
      parameters:
        - name: advId
          in: path
          required: true
          description: Adventure ID
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CardCreate"
      responses:
        "201":
          description: Card created (includes auto-generated qr_payload)
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Card"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /admin/cards/{id}:
    put:
      tags: [Admin - Cards]
      summary: Update a card
      operationId: updateCard
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CardUpdate"
      responses:
        "200":
          description: Card updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Card"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Card not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

    delete:
      tags: [Admin - Cards]
      summary: Delete a card
      operationId: deleteCard
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Card deleted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessResponse"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Card not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  # ══════════════════════════════════════════════════════
  #  ADMIN: SOLUTION PLACEMENTS
  # ══════════════════════════════════════════════════════

  /admin/adventures/{advId}/solution-placements:
    put:
      tags: [Admin - Solution Placements]
      summary: Set solution placements
      description: |
        Replaces all solution placements for an adventure. Each placement maps
        a character to a quadrant slot (0-3) on the solution board.
      operationId: updateSolutionPlacements
      security:
        - BearerAuth: []
      parameters:
        - name: advId
          in: path
          required: true
          description: Adventure ID
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SolutionPlacementsUpdate"
      responses:
        "200":
          description: Placements updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessResponse"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  # ══════════════════════════════════════════════════════
  #  ADMIN: SPECIAL SOUNDS
  # ══════════════════════════════════════════════════════

  /admin/adventures/{advId}/special-sounds:
    put:
      tags: [Admin - Special Sounds]
      summary: Set special sounds
      description: Replaces all special event sounds for an adventure.
      operationId: updateSpecialSounds
      security:
        - BearerAuth: []
      parameters:
        - name: advId
          in: path
          required: true
          description: Adventure ID
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SpecialSoundsUpdate"
      responses:
        "200":
          description: Sounds updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessResponse"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  # ══════════════════════════════════════════════════════
  #  ADMIN: FILE UPLOADS
  # ══════════════════════════════════════════════════════

  /admin/upload/audio:
    post:
      tags: [Admin - Uploads]
      summary: Upload an audio file
      description: Uploads an audio file (MP3) to the server. Returns the public URL.
      operationId: uploadAudio
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - file
                - path
              properties:
                file:
                  type: string
                  format: binary
                  description: Audio file (MP3)
                path:
                  type: string
                  description: Storage sub-path
                  example: "abenteuer-im-hoerwald/geburtstagsparty/cards/eule-elara-1.mp3"
      responses:
        "200":
          description: Upload successful
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/UploadResult"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /admin/upload/image:
    post:
      tags: [Admin - Uploads]
      summary: Upload an image file
      description: Uploads an image file (JPEG, PNG, WebP) to the server. Returns the public URL.
      operationId: uploadImage
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - file
                - path
              properties:
                file:
                  type: string
                  format: binary
                  description: Image file (JPEG, PNG, WebP)
                path:
                  type: string
                  description: Storage sub-path
                  example: "abenteuer-im-hoerwald/geburtstagsparty/bg.jpg"
      responses:
        "200":
          description: Upload successful
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/UploadResult"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /admin/upload/pdf:
    post:
      tags: [Admin - Uploads]
      summary: Upload a PDF file
      description: |
        Uploads a PDF file to the server. Max file size 10 MB.
        Only application/pdf content type is accepted.
      operationId: uploadPdf
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - file
                - path
              properties:
                file:
                  type: string
                  format: binary
                  description: PDF file
                path:
                  type: string
                  description: Storage sub-path
                  example: "abenteuer-im-hoerwald/geburtstagsparty/spielmaterial.pdf"
      responses:
        "200":
          description: Upload successful
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/UploadResult"
        "400":
          description: Invalid file type
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: "Only PDF files are allowed"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "413":
          description: File too large (exceeds 10 MB or Nginx client_max_body_size)

  # ══════════════════════════════════════════════════════
  #  ADMIN: TTS (ElevenLabs Proxy)
  # ══════════════════════════════════════════════════════

  /admin/tts/generate:
    post:
      tags: [Admin - TTS]
      summary: Generate TTS audio preview
      description: |
        Generates speech audio from text using ElevenLabs. Returns raw audio
        binary (audio/mpeg) for preview playback. Does not save the file.
      operationId: ttsGenerate
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TTSGenerateRequest"
      responses:
        "200":
          description: Generated audio stream
          content:
            audio/mpeg:
              schema:
                type: string
                format: binary
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          description: TTS generation failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /admin/tts/generate-and-save:
    post:
      tags: [Admin - TTS]
      summary: Generate TTS audio and save to storage
      description: |
        Generates speech audio from text using ElevenLabs, saves it to the
        specified path on the server, and optionally updates the linked card
        with the new audio URL.
      operationId: ttsGenerateAndSave
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TTSGenerateAndSaveRequest"
      responses:
        "200":
          description: Audio generated and saved
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/TTSGenerateAndSaveResult"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          description: TTS generation failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /admin/tts/sound-effect:
    post:
      tags: [Admin - TTS]
      summary: Generate a sound effect
      description: |
        Generates a sound effect from a text prompt using ElevenLabs.
        Returns raw audio binary (audio/mpeg) for preview playback.
      operationId: ttsSoundEffect
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TTSSoundEffectRequest"
      responses:
        "200":
          description: Generated sound effect audio stream
          content:
            audio/mpeg:
              schema:
                type: string
                format: binary
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          description: Sound effect generation failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /admin/tts/usage:
    get:
      tags: [Admin - TTS]
      summary: Get TTS usage statistics
      description: Returns the current ElevenLabs character usage and limits.
      operationId: ttsGetUsage
      security:
        - BearerAuth: []
      responses:
        "200":
          description: Usage data
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/TTSUsage"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
