openapi: 3.0.3
info:
  title: Pornhwa Database API
  description: |
    Public REST API for accessing the Pornhwa Database.

    ## Authentication
    All operations except `GET /health` require an API key in the request header.
    CORS preflight (`OPTIONS`) requests do not require authentication.
    - **Header**: `X-API-Key: pwdb_your_key_here`

    ## Rate Limits
    - **100 requests per minute by default** per API key; individual keys may have a custom limit
    - User-owned mutation operations also enforce 30 successful writes per minute and 500 per hour, per endpoint

    Rate limit headers are included after a valid API key has been accepted and on API-key rate-limit responses:
    - `X-RateLimit-Limit`: Maximum requests allowed per minute
    - `X-RateLimit-Remaining`: Requests remaining in current window
    - `X-RateLimit-Reset`: Unix timestamp when the limit resets

    ## Caching
    The API supports HTTP caching to reduce bandwidth and improve performance.

    ### Cache-Control Headers
    Responses include `Cache-Control` headers indicating cacheability:
    - **Single resources** (e.g., `/pornhwa/{slug}`): `public, max-age=60, s-maxage=300`
    - **Ratings**: `public, max-age=30, s-maxage=60`
    - **API info, random, private tracking and health**: `private, no-store` or `no-store`

    ### ETag Support
    The pornhwa detail, pornhwa characters, pornhwa ratings, creator detail and public-list detail endpoints return an `ETag` header. Use conditional requests to avoid re-downloading unchanged data:
    - Send `If-None-Match: <etag>` header with subsequent requests
    - If the resource hasn't changed, the API returns `304 Not Modified` with no body
    - This saves response bandwidth; the request still authenticates and counts against the API-key limit

    ### Last-Modified Support
    Those ETag-enabled endpoints also return a `Last-Modified` header:
    - Send `If-Modified-Since: <date>` header with subsequent requests
    - If the resource hasn't changed since that date, the API returns `304 Not Modified`

    ### Example Conditional Request
    ```
    GET /api/v1/pornhwa/example-slug
    If-None-Match: W/"abc123"

    Response: 304 Not Modified (if unchanged)
    ```

    ## Pagination

    ### Link Headers (RFC 5988)
    Paginated endpoints return a `Link` header with navigation URLs:
    ```
    Link: </api/v1/pornhwa?page=1&limit=20>; rel="first",
          </api/v1/pornhwa?page=1&limit=20>; rel="prev",
          </api/v1/pornhwa?page=3&limit=20>; rel="next",
          </api/v1/pornhwa?page=10&limit=20>; rel="last"
    ```

    ### Pagination URLs in Response Body
    The pagination object includes navigation URLs for convenience:
    ```json
    {
      "pagination": {
        "page": 2,
        "limit": 20,
        "total": 200,
        "totalPages": 10,
        "hasMore": true,
        "nextUrl": "/api/v1/pornhwa?page=3&limit=20",
        "prevUrl": "/api/v1/pornhwa?page=1&limit=20",
        "firstUrl": "/api/v1/pornhwa?page=1&limit=20",
        "lastUrl": "/api/v1/pornhwa?page=10&limit=20"
      }
    }
    ```

    ## Advanced Query Features

    ### Sparse Fieldsets
    Request only the fields you need using the `fields` parameter:
    ```
    GET /api/v1/pornhwa?fields=id,title,slug,coverImage
    ```
    This reduces response size and improves performance.

    ### Reference Expansion
    Include related resources using the `includes[]` parameter:
    ```
    GET /api/v1/pornhwa/example-slug?includes[]=characters&includes[]=chapters
    ```
    By default, the detail endpoint returns basic data without characters or chapters.

    ### Deep Object Ordering
    Sort by multiple fields using the `order[field]` syntax:
    ```
    GET /api/v1/pornhwa?order[releaseYear]=desc&order[title]=asc
    ```

    ### Filter Operators
    Use comparison operators for numeric filtering:
    ```
    GET /api/v1/pornhwa?releaseYear[gte]=2020&releaseYear[lte]=2024
    GET /api/v1/pornhwa?averageRating[gte]=4.0&chapterCount[gte]=50
    ```
    Supported operators: `gte` (>=), `lte` (<=), `gt` (>), `lt` (<)

    Fields that support filter operators: `releaseYear`, `releaseMonth`, `endYear`, `endMonth`, `averageRating`, `totalRatings`, `chapterCount`

    ### Batch Requests
    Fetch multiple items in a single request:
    ```
    GET /api/v1/pornhwa/batch?slugs=slug1,slug2,slug3
    GET /api/v1/pornhwa/batch?ids=1,2,3
    ```
    Maximum 50 items per request. Returns null for not-found items.

    ## Limits & Constraints

    ### Pagination Limits
    | Parameter | Default | Maximum | Description |
    |-----------|---------|---------|-------------|
    | `limit` | 20 | 100 | Items per page (most endpoints) |
    | `limit` | 50 | 100 | Items per page (chapters endpoint) |
    | `page` | 1 | 1000 | Page number |

    ### Other Limits
    | Constraint | Value | Description |
    |------------|-------|-------------|
    | Batch size | 50 | Maximum items in `/pornhwa/batch` |
    | Search query | 200 chars | Maximum search string length |
    | Random count | 10 | Maximum items from `/discover/random` |
    | Similar limit | 50 | Maximum items from `/pornhwa/{slug}/similar` |

    ### Response Size Guidelines
    - List endpoints return paginated results to keep responses manageable
    - Use `fields` parameter to reduce response size when you only need specific data
    - Nested resources (characters for a pornhwa) return all items without pagination
    - Discovery endpoints support pagination for browsing beyond the default results

    ## Getting an API Key
    1. Create an account on Pornhwa Database
    2. Go to your profile settings
    3. Navigate to the API Keys section
    4. Create a new API key

    **Important**: Your API key is shown only once when created. Store it securely!

    ## Series identifiers
    Routes written as `/pornhwa/{slug}` accept a current slug, a retired slug, or the numeric series ID.
    A non-canonical reference returns `Content-Location` with the current canonical API path.
  version: 1.0.0
  contact:
    name: Pornhwa Database
  license:
    name: Pornhwa Database API Terms
    url: https://pornhwadb.com/docs/guides/terms

servers:
  - url: /api/v1
    description: Production API

security:
  - ApiKeyHeader: []

components:
  securitySchemes:
    ApiKeyHeader:
      type: apiKey
      in: header
      name: X-API-Key
      description: API key passed in the X-API-Key header

  headers:
    ETag:
      description: Entity tag for cache validation
      schema:
        type: string
        example: W/"1a2b3c4d"
    Last-Modified:
      description: Last modification date of the resource
      schema:
        type: string
        example: "Sun, 01 Jan 2025 12:00:00 GMT"
    Cache-Control:
      description: Caching directives
      schema:
        type: string
        example: "public, max-age=60, s-maxage=300"
    Link:
      description: RFC 5988 pagination links
      schema:
        type: string
        example: '</api/v1/pornhwa?page=1&limit=20>; rel="first", </api/v1/pornhwa?page=2&limit=20>; rel="next", </api/v1/pornhwa?page=10&limit=20>; rel="last"'
    Content-Location:
      description: Canonical API path when a retired slug or numeric ID resolved the series
      schema:
        type: string
        example: "/api/v1/pornhwa/current-slug"
    Retry-After:
      description: Seconds to wait before retrying a rate-limited request
      schema:
        type: integer
        minimum: 1

  parameters:
    If-None-Match:
      name: If-None-Match
      in: header
      description: ETag value from a previous response for conditional request
      schema:
        type: string
        example: W/"1a2b3c4d"
    If-Modified-Since:
      name: If-Modified-Since
      in: header
      description: Date from Last-Modified header of a previous response
      schema:
        type: string
        example: "Sun, 01 Jan 2025 12:00:00 GMT"

  responses:
    NotModified:
      description: Resource has not been modified since the last request
      headers:
        ETag:
          $ref: '#/components/headers/ETag'
        Last-Modified:
          $ref: '#/components/headers/Last-Modified'
        Cache-Control:
          $ref: '#/components/headers/Cache-Control'
    Unauthorized:
      description: Authentication required or invalid API key
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: "API key required. Provide it via the X-API-Key header."
            code: "MISSING_API_KEY"
            requestId: "550e8400-e29b-41d4-a716-446655440000"
    RateLimited:
      description: Rate limit exceeded
      headers:
        Retry-After:
          $ref: '#/components/headers/Retry-After'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: "Rate limit exceeded"
            code: "RATE_LIMITED"
            requestId: "550e8400-e29b-41d4-a716-446655440000"
            retryAfter: 45
    InternalError:
      description: Internal server error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: "Internal server error"
            code: "INTERNAL_ERROR"
            requestId: "550e8400-e29b-41d4-a716-446655440000"
    ValidationError:
      description: Invalid request parameters
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: "Invalid request parameters"
            code: "VALIDATION_ERROR"
            requestId: "550e8400-e29b-41d4-a716-446655440000"
    Forbidden:
      description: The API key owner account is suspended or lacks permission for this resource
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: "The API key owner account is suspended"
            code: "ACCOUNT_SUSPENDED"
            requestId: "550e8400-e29b-41d4-a716-446655440000"

  schemas:
    Creator:
      type: object
      description: Normalized creator data with aliases
      required: [id, canonicalName, aliases, role]
      properties:
        id:
          type: integer
          description: Creator ID
        canonicalName:
          type: string
          description: Primary/canonical name of the creator
        aliases:
          type: array
          description: Alternative names for this creator
          items:
            type: string
        role:
          type: string
          enum: [artist, author]
          description: Role of the creator for this work

    Pornhwa:
      type: object
      properties:
        id:
          type: integer
          description: Unique identifier
        title:
          type: string
          description: Primary title
        slug:
          type: string
          description: URL-friendly identifier
        coverImage:
          type: string
          description: Cover image URL
        status:
          type: string
          enum: [On Going, Completed, Hiatus]
          description: Publication status
        orientation:
          type: string
          nullable: true
          enum: [yaoi, yuri]
          description: Content orientation. `null` means non-yaoi/yuri.
        description:
          type: string
          description: Synopsis/description
        artists:
          type: array
          items:
            type: string
          description: List of artists (legacy string array for backward compatibility)
        authors:
          type: array
          items:
            type: string
          description: List of authors (legacy string array for backward compatibility)
        creators:
          type: array
          description: Normalized creator data with aliases
          items:
            $ref: '#/components/schemas/Creator'
        releaseYear:
          type: integer
          nullable: true
          description: Year of first release
        releaseMonth:
          type: integer
          nullable: true
          minimum: 1
          maximum: 12
          description: Month of first release (1-12)
        endYear:
          type: integer
          nullable: true
          description: Year series ended (for completed series)
        endMonth:
          type: integer
          nullable: true
          minimum: 1
          maximum: 12
          description: Month series ended (1-12, for completed series)
        genreTags:
          type: array
          items:
            type: string
          description: Genre tags
        averageRating:
          type: number
          nullable: true
          description: Average user rating (0.5-5.0 scale)
        totalRatings:
          type: integer
          description: Total number of ratings
        chapterCount:
          type: integer
          description: Number of scene list entries in the database (not the actual chapter count - use totalChapters for that)
        totalChapters:
          type: integer
          nullable: true
          description: The highest chapter number. For completed series, this may be manually set to the final chapter count. For ongoing/hiatus series, this is calculated from the highest chapter number in the scene list (e.g., chapters 1,3,4,10-15 would return 15).
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
          nullable: true

    PornhwaDetail:
      allOf:
        - $ref: '#/components/schemas/Pornhwa'
        - type: object
          properties:
            dataStatus:
              type: string
              enum: [complete, processing]
            alternativeTitles:
              type: array
              items:
                type: string
            externalLinks:
              type: array
              items:
                type: object
                properties:
                  siteName:
                    type: string
                  url:
                    type: string
            characters:
              type: array
              items:
                $ref: '#/components/schemas/Character'
            chapterCount:
              type: integer
              description: Number of scene list entries in the database
            totalChapters:
              type: integer
              nullable: true
              description: The highest chapter number. For completed series, this may be manually set. For ongoing/hiatus series, calculated from scene list.
            creators:
              type: array
              description: Normalized creator data with aliases
              items:
                $ref: '#/components/schemas/Creator'
            chaptersPagination:
              type: object
              nullable: true
              description: Pagination info for chapters (only present when includes[]=chapters is used)
              properties:
                total:
                  type: integer
                  description: Total number of chapters
                included:
                  type: integer
                  description: Number of chapters included in this response
                note:
                  type: string
                  description: Usage note for paginated access

    Character:
      type: object
      required: [id, name, image, role, alternativeNames]
      properties:
        id:
          type: integer
        name:
          type: string
        image:
          type: string
          nullable: true
        role:
          type: string
          enum: [Main, Supporting]
          nullable: true
        tags:
          type: array
          items:
            type: string
        alternativeNames:
          type: array
          items:
            type: string
            maxLength: 255
          maxItems: 10
          description: Alternative names for the character (e.g., Korean name, romanized variants)
        description:
          type: string
          nullable: true
          maxLength: 1000
          description: Brief character description (max 1000 characters)

    Chapter:
      type: object
      required: [id, chapterStart, chapterEnd, description, tags, characters]
      properties:
        id:
          type: integer
        chapterStart:
          type: integer
          description: Starting chapter number
        chapterEnd:
          type: integer
          nullable: true
          description: Ending chapter number (for ranges)
        description:
          type: string
          nullable: true
        tags:
          type: array
          items:
            type: string
        characters:
          type: array
          items:
            $ref: '#/components/schemas/Character'

    RatingAggregate:
      type: object
      required: [averageRating, totalRatings, ratingDistribution]
      properties:
        averageRating:
          type: number
          nullable: true
          description: Average rating (0.5-5.0 scale)
        totalRatings:
          type: integer
          description: Total number of ratings
        ratingDistribution:
          type: object
          additionalProperties:
            type: integer
          description: Count of ratings by score

    Review:
      type: object
      required: [id, reviewText, isEdited, userDisplayName, userRating, createdAt, updatedAt]
      properties:
        id:
          type: integer
        reviewText:
          type: string
        isEdited:
          type: boolean
        userDisplayName:
          type: string
        userRating:
          type: number
          nullable: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    CharacterRating:
      type: object
      required: [id, characterId, rating, userDisplayName, avatarImage, createdAt, updatedAt]
      properties:
        id:
          type: integer
          description: Unique rating ID
        characterId:
          type: integer
          description: ID of the rated character
        rating:
          type: number
          description: Score given by the user (0.5-5.0 scale)
        userDisplayName:
          type: string
          nullable: true
          description: Display name of the user who left the rating
        avatarImage:
          type: string
          nullable: true
          description: Avatar image URL of the user who left the rating
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    Pagination:
      type: object
      required: [page, limit, total, totalPages, hasMore, nextUrl, prevUrl, firstUrl, lastUrl]
      properties:
        page:
          type: integer
          description: Current page number
        limit:
          type: integer
          description: Items per page
        total:
          type: integer
          description: Total number of items
        totalPages:
          type: integer
          description: Total number of pages
        hasMore:
          type: boolean
          description: Whether there are more pages
        nextUrl:
          type: string
          nullable: true
          description: URL for the next page (null if on last page)
        prevUrl:
          type: string
          nullable: true
          description: URL for the previous page (null if on first page)
        firstUrl:
          type: string
          description: URL for the first page
        lastUrl:
          type: string
          description: URL for the last page

    ListSummary:
      type: object
      description: Summary of a public list for browse views.
      required: [slug, title, description, isPublic, likeCount, itemCount, coverImages, username, avatarImage, createdAt, updatedAt]
      properties:
        slug:
          type: string
          description: URL-friendly identifier of the list
        title:
          type: string
        description:
          type: string
          nullable: true
        isPublic:
          type: boolean
        likeCount:
          type: integer
          description: Number of likes the list has received
        itemCount:
          type: integer
          description: Number of items in the list
        coverImages:
          type: array
          description: Up to four cover images of the list's first items
          items:
            type: string
        username:
          type: string
          nullable: true
          description: Username of the list owner
        avatarImage:
          type: string
          nullable: true
          description: Avatar image URL of the list owner
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    ListCollaborator:
      type: object
      required: [username, avatarImage, addedAt]
      properties:
        username:
          type: string
        avatarImage:
          type: string
          nullable: true
        addedAt:
          type: string
          format: date-time

    ListItem:
      type: object
      required: [id, pornhwaId, position, addedAt, title, slug, coverImage, status, notes, addedByUsername, chapters]
      properties:
        id:
          type: integer
        pornhwaId:
          type: integer
        position:
          type: integer
          description: Ordering position of the item within the list
        addedAt:
          type: string
          format: date-time
        title:
          type: string
          nullable: true
        slug:
          type: string
          nullable: true
        coverImage:
          type: string
          nullable: true
        status:
          type: string
          nullable: true
        notes:
          type: string
          nullable: true
        addedByUsername:
          type: string
          nullable: true
        chapters:
          type: array
          items:
            type: object
            properties:
              id:
                type: integer
              chapterId:
                type: integer
              chapterStart:
                type: number
              chapterEnd:
                type: number
                nullable: true
              description:
                type: string
                nullable: true
              addedAt:
                type: string
                format: date-time
              tags:
                type: array
                items:
                  type: string
              characters:
                type: array
                items:
                  type: object

    ListDetail:
      type: object
      description: A public list including its items and collaborators.
      required: [slug, title, description, isPublic, likeCount, itemCount, username, avatarImage, isCollaborative, createdAt, updatedAt, items, collaborators]
      properties:
        slug:
          type: string
        title:
          type: string
        description:
          type: string
          nullable: true
        isPublic:
          type: boolean
        likeCount:
          type: integer
        itemCount:
          type: integer
        username:
          type: string
          nullable: true
        avatarImage:
          type: string
          nullable: true
        isCollaborative:
          type: boolean
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        items:
          type: array
          items:
            $ref: '#/components/schemas/ListItem'
        collaborators:
          type: array
          items:
            $ref: '#/components/schemas/ListCollaborator'

    ActivityFeedItem:
      type: object
      description: |
        A single event in the global activity feed. The shape of `target` and
        `metadata` varies by `eventType`.
      required: [id, eventType, occurredAt, actor, target, metadata]
      properties:
        id:
          type: string
          description: Stable identifier for the event (e.g. "reviews:8801")
        eventType:
          type: string
          enum:
            - review_posted
            - rating_set
            - character_rating_set
            - tracking_status_changed
            - list_created
            - list_item_added
        occurredAt:
          type: string
          format: date-time
        actor:
          type: object
          properties:
            username:
              type: string
            avatarImage:
              type: string
              nullable: true
        target:
          type: object
          description: The subject of the event (pornhwa, character, or list).
        metadata:
          type: object
          description: Event-type-specific details.

    ApiStats:
      type: object
      required: [totalPornhwa, totalCharacters, totalChapters, totalReviews, totalRatings, totalTags]
      properties:
        totalPornhwa:
          type: integer
          description: Total number of pornhwa in the database
        totalCharacters:
          type: integer
          description: Total number of characters
        totalChapters:
          type: integer
          description: Total number of chapters
        totalReviews:
          type: integer
          description: Total number of reviews
        totalRatings:
          type: integer
          description: Total number of ratings
        totalTags:
          type: object
          properties:
            genre:
              type: integer
              description: Number of genre tags
            character:
              type: integer
              description: Number of character tags
            chapter:
              type: integer
              description: Number of chapter tags

    Error:
      type: object
      required: [error, code, requestId]
      properties:
        error:
          type: string
          description: Human-readable error message
        code:
          type: string
          description: Machine-readable error code
        requestId:
          type: string
          description: Unique request identifier for debugging
        details:
          description: Additional error details; validation errors use an array of issue objects
          oneOf:
            - type: object
              additionalProperties: true
            - type: array
              items:
                type: object
                additionalProperties: true
        retryAfter:
          type: integer
          minimum: 1
          description: Seconds to wait before retrying a rate-limited request

    TrackingStatus:
      type: string
      description: |
        Reading status for a tracked series.
        - `reading` — currently reading
        - `completed` — finished
        - `dropped` — stopped reading
        - `plan_to_read` — queued for later (supports priority 1–5)
        - `on_hold` — paused
        - `re_reading` — reading again after completing
      enum:
        - reading
        - completed
        - dropped
        - plan_to_read
        - on_hold
        - re_reading

    TrackingEntryPornhwa:
      type: object
      description: Abbreviated series data embedded in a tracking entry
      required: [id, title, slug, coverImage, status, totalChapters, genreTags]
      properties:
        id:
          type: integer
        title:
          type: string
        slug:
          type: string
        coverImage:
          type: string
        status:
          type: string
          enum: [On Going, Completed, Hiatus]
        totalChapters:
          type: integer
          nullable: true
          description: Highest known chapter number
        genreTags:
          type: array
          items:
            type: string

    TrackingEntry:
      type: object
      description: A user's tracking record for a single series
      required: [id, pornhwa, status, currentChapter, startedAt, finishedAt, notes, timesCompleted, priority, isPrivate, hasReview, createdAt, updatedAt]
      properties:
        id:
          type: integer
          description: Tracking entry ID
        pornhwa:
          $ref: '#/components/schemas/TrackingEntryPornhwa'
        status:
          $ref: '#/components/schemas/TrackingStatus'
        currentChapter:
          type: integer
          nullable: true
          description: Last chapter read
        startedAt:
          type: string
          nullable: true
          description: Date started reading (YYYY-MM-DD)
          example: "2025-01-10"
        finishedAt:
          type: string
          nullable: true
          description: Date finished reading (YYYY-MM-DD)
          example: "2025-03-22"
        notes:
          type: string
          nullable: true
          description: Personal notes (max 2000 characters)
        timesCompleted:
          type: integer
          minimum: 0
          description: Number of times this series has been completed
        priority:
          type: integer
          nullable: true
          minimum: 1
          maximum: 5
          description: "Priority (plan_to_read only): 1=Low, 2=Medium-Low, 3=Medium, 4=Medium-High, 5=High"
        isPrivate:
          type: boolean
          description: When true, this entry is hidden from your public profile
        hasReview:
          type: boolean
          description: Whether you have written a review for this series
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    TrackingUpsertInput:
      type: object
      required:
        - status
      description: Request body for creating or updating a tracking entry
      properties:
        status:
          $ref: '#/components/schemas/TrackingStatus'
        currentChapter:
          type: integer
          nullable: true
          minimum: 0
          maximum: 10000
          description: >-
            Last chapter read. Not bounded by the series' totalChapters — our
            chapter count can lag the source, so progress ahead of it is valid.
        startedAt:
          type: string
          nullable: true
          description: YYYY-MM-DD
          example: "2025-01-10"
        finishedAt:
          type: string
          nullable: true
          description: YYYY-MM-DD
          example: "2025-03-22"
        notes:
          type: string
          nullable: true
          maxLength: 2000
        timesCompleted:
          type: integer
          minimum: 0
        priority:
          type: integer
          nullable: true
          minimum: 1
          maximum: 5
          description: "1=Low, 2=Medium-Low, 3=Medium, 4=Medium-High, 5=High"
        isPrivate:
          type: boolean
          description: Hide this entry from your public profile

    CharacterReview:
      type: object
      description: A review of a character
      required: [id, characterId, reviewText, isEdited, userDisplayName, userRating, avatarImage, createdAt, updatedAt]
      properties:
        id:
          type: integer
        characterId:
          type: integer
        reviewText:
          type: string
        isEdited:
          type: boolean
        userDisplayName:
          type: string
          nullable: true
        userRating:
          type: number
          format: float
          nullable: true
        avatarImage:
          type: string
          nullable: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    PublicUserProfile:
      type: object
      description: Public profile information for a user
      required: [uid, username, avatarImage, createdAt, trackingPublic, bio, malUsername, anilistUsername, mangabakaUsername, followerCount, followingCount, isEditor]
      properties:
        uid:
          type: string
          description: Canonical user identifier
        username:
          type: string
        avatarImage:
          type: string
          nullable: true
        createdAt:
          type: string
          format: date-time
        trackingPublic:
          type: boolean
          description: Whether the user has made their tracking list public
        bio:
          type: string
          nullable: true
        malUsername:
          type: string
          nullable: true
        anilistUsername:
          type: string
          nullable: true
        mangabakaUsername:
          type: string
          nullable: true
        followerCount:
          type: integer
        followingCount:
          type: integer
        isEditor:
          type: boolean

    UserContributionStats:
      type: object
      description: Aggregate counts of a user's contributions
      required: [seriesAdded, editsApproved, reviewsWritten, ratingsGiven, galleryUploads, publicLists, characterRatingsGiven, characterReviewsWritten]
      properties:
        seriesAdded:
          type: integer
        editsApproved:
          type: integer
        reviewsWritten:
          type: integer
        ratingsGiven:
          type: integer
        galleryUploads:
          type: integer
        publicLists:
          type: integer
        characterRatingsGiven:
          type: integer
        characterReviewsWritten:
          type: integer

    UserRating:
      type: object
      description: A user's rating of a pornhwa
      required: [pornhwaId, pornhwaTitle, pornhwaSlug, pornhwaCover, rating, createdAt]
      properties:
        pornhwaId:
          type: integer
        pornhwaTitle:
          type: string
        pornhwaSlug:
          type: string
        pornhwaCover:
          type: string
          nullable: true
        rating:
          type: number
          format: float
        createdAt:
          type: string
          format: date-time

    UserReview:
      type: object
      description: A user's review of a pornhwa
      required: [id, pornhwaId, pornhwaTitle, pornhwaSlug, pornhwaCover, reviewText, rating, isEdited, createdAt, updatedAt]
      properties:
        id:
          type: integer
        pornhwaId:
          type: integer
        pornhwaTitle:
          type: string
        pornhwaSlug:
          type: string
        pornhwaCover:
          type: string
          nullable: true
        reviewText:
          type: string
        rating:
          type: number
          format: float
          nullable: true
          description: The user's rating for the same pornhwa, if any
        isEdited:
          type: boolean
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    UserCharacterRating:
      type: object
      description: A user's rating of a character
      required: [characterId, characterName, characterImage, pornhwaId, pornhwaTitle, pornhwaSlug, rating, createdAt]
      properties:
        characterId:
          type: integer
        characterName:
          type: string
        characterImage:
          type: string
          nullable: true
        pornhwaId:
          type: integer
        pornhwaTitle:
          type: string
        pornhwaSlug:
          type: string
        rating:
          type: number
          format: float
        createdAt:
          type: string
          format: date-time

    UserCharacterReview:
      type: object
      description: A user's review of a character
      required: [characterId, characterName, characterImage, pornhwaId, pornhwaTitle, pornhwaSlug, rating, reviewText, isEdited, createdAt]
      properties:
        characterId:
          type: integer
        characterName:
          type: string
        characterImage:
          type: string
          nullable: true
        pornhwaId:
          type: integer
        pornhwaTitle:
          type: string
        pornhwaSlug:
          type: string
        rating:
          type: number
          format: float
          description: The user's rating for the same character, or 0 if none
        reviewText:
          type: string
        isEdited:
          type: boolean
        createdAt:
          type: string
          format: date-time

    UserTrackingEntry:
      type: object
      description: A public tracking entry from a user's list
      required: [pornhwaId, title, slug, coverImage, pornhwaStatus, trackingStatus, averageRating, currentChapter, totalChapters, startedAt, finishedAt, timesCompleted, priority, createdAt, updatedAt]
      properties:
        pornhwaId:
          type: integer
        title:
          type: string
        slug:
          type: string
        coverImage:
          type: string
          nullable: true
        pornhwaStatus:
          type: string
        trackingStatus:
          $ref: '#/components/schemas/TrackingStatus'
        averageRating:
          type: number
          format: float
          nullable: true
        currentChapter:
          type: integer
          nullable: true
        totalChapters:
          type: integer
          nullable: true
        startedAt:
          type: string
          format: date-time
          nullable: true
        finishedAt:
          type: string
          format: date-time
          nullable: true
        timesCompleted:
          type: integer
        priority:
          type: integer
          nullable: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    UserList:
      type: object
      description: A public list owned by, or collaborated on by, the user
      required: [id, user_id, slug, title, description, is_public, like_count, item_count, cover_images, username, avatar_image, created_at, updated_at]
      properties:
        id:
          type: integer
        user_id:
          type: string
        slug:
          type: string
        title:
          type: string
        description:
          type: string
          nullable: true
        is_public:
          type: boolean
        like_count:
          type: integer
        item_count:
          type: integer
        cover_images:
          type: array
          items:
            type: string
        username:
          type: string
          nullable: true
        avatar_image:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    UserContribution:
      type: object
      description: A single contribution (series addition or approved edit)
      required: [id, type, pornhwaId, pornhwaTitle, pornhwaSlug, pornhwaCover, createdAt]
      properties:
        id:
          type: integer
        type:
          type: string
          enum: [series, edit]
        pornhwaId:
          type: integer
        pornhwaTitle:
          type: string
        pornhwaSlug:
          type: string
        pornhwaCover:
          type: string
          nullable: true
        createdAt:
          type: string
          format: date-time

    UserFollowUser:
      type: object
      description: A user in a follower/following list
      required: [uid, username, avatarImage, bio, followedAt]
      properties:
        uid:
          type: string
        username:
          type: string
        avatarImage:
          type: string
          nullable: true
        bio:
          type: string
          nullable: true
        followedAt:
          type: string
          format: date-time

    UserEditorActivityItem:
      type: object
      description: A series a user wants to edit, is working on, or has completed
      required: [pornhwaId, slug, title, coverImage, status, updatedAt]
      properties:
        pornhwaId:
          type: integer
        slug:
          type: string
        title:
          type: string
        coverImage:
          type: string
          nullable: true
        status:
          type: string
          enum: [want_to_edit, working_on, done]
        updatedAt:
          type: string

paths:
  /:
    get:
      operationId: getApiInfo
      summary: Get API information
      description: Get basic information about the API including version and rate limits.
      tags:
        - Info
      x-examples:
        request:
          description: "Get API information"
          url: "/api/v1/"
          headers:
            X-API-Key: "pwdb_your_api_key_here"
        response:
          status: 200
          headers:
            Content-Type: "application/json"
            Cache-Control: "private, no-store"
            X-Request-ID: "req_t2f9d5g1b8o6"
          body:
            requestId: "req_t2f9d5g1b8o6"
            name: "Pornhwa Database API"
            version: "1.0.0"
            documentation: "/docs"
            rateLimit:
              requests: 100
              window: "1 minute"
      responses:
        '200':
          description: Successful response
          headers:
            Cache-Control:
              $ref: '#/components/headers/Cache-Control'
          content:
            application/json:
              schema:
                type: object
                required: [name, version, documentation, rateLimit, requestId]
                properties:
                  name:
                    type: string
                    description: API name
                    example: "Pornhwa Database API"
                  version:
                    type: string
                    description: API version
                    example: "1.0.0"
                  documentation:
                    type: string
                    description: Documentation URL
                    example: "/docs"
                  rateLimit:
                    type: object
                    required: [requests, window]
                    description: Effective limit for the authenticated API key
                    properties:
                      requests:
                        type: integer
                        description: Maximum requests per window
                        example: 100
                      window:
                        type: string
                        description: Rate limit window duration
                        example: "1 minute"
                  requestId:
                    type: string
                    description: Unique request identifier
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /pornhwa:
    get:
      operationId: listPornhwa
      summary: List pornhwa
      description: Get a paginated list of pornhwa with optional filtering and search
      tags:
        - Pornhwa
      x-examples:
        request:
          description: "List completed pornhwa with pagination"
          url: "/api/v1/pornhwa?page=1&limit=2&status=Completed"
          headers:
            X-API-Key: "pwdb_your_api_key_here"
        response:
          status: 200
          headers:
            Content-Type: "application/json"
            X-RateLimit-Remaining: "99"
            X-Request-ID: "req_k7m2p9x4n1b8"
            Link: '</api/v1/pornhwa?page=1&limit=2&status=Completed>; rel="first", </api/v1/pornhwa?page=2&limit=2&status=Completed>; rel="next"'
          body:
            requestId: "req_k7m2p9x4n1b8"
            data:
              - id: 1
                title: "Secret Class"
                slug: "secret-class"
                status: "Completed"
                orientation: null
                coverImage: "https://cdn.pornhwadb.com/covers/secret-class.jpg"
                releaseYear: 2019
                releaseMonth: 3
                endYear: 2023
                endMonth: 8
                genreTags: ["Romance", "Drama", "School Life"]
                artists: ["Wang Kang Cheol"]
                authors: ["Minachan"]
                creators:
                  - id: 10
                    canonicalName: "Wang Kang Cheol"
                    aliases: ["WKC", "Wang KC"]
                    role: "artist"
                  - id: 20
                    canonicalName: "Minachan"
                    aliases: []
                    role: "author"
              - id: 2
                title: "Perfect Half"
                slug: "perfect-half"
                status: "Completed"
                orientation: "yuri"
                coverImage: "https://cdn.pornhwadb.com/covers/perfect-half.jpg"
                releaseYear: 2018
                releaseMonth: 6
                endYear: 2022
                endMonth: 12
                genreTags: ["Romance", "Fantasy"]
                artists: ["Luv P"]
                authors: ["Luv P"]
                creators:
                  - id: 30
                    canonicalName: "Luv P"
                    aliases: ["LuvP"]
                    role: "artist"
                  - id: 30
                    canonicalName: "Luv P"
                    aliases: ["LuvP"]
                    role: "author"
            pagination:
              page: 1
              limit: 2
              total: 847
              totalPages: 424
              hasMore: true
              nextUrl: "/api/v1/pornhwa?page=2&limit=2&status=Completed"
              prevUrl: null
              firstUrl: "/api/v1/pornhwa?page=1&limit=2&status=Completed"
              lastUrl: "/api/v1/pornhwa?page=424&limit=2&status=Completed"
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            default: 1
            minimum: 1
            maximum: 1000
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            minimum: 1
            maximum: 100
        - name: search
          in: query
          description: Search by title or alternative titles
          schema:
            type: string
            maxLength: 200
        - name: status
          in: query
          schema:
            type: string
            enum: [On Going, Completed, Hiatus]
        - name: orientation
          in: query
          description: Filter by orientation
          schema:
            type: string
            enum: [yaoi, yuri]
        - name: dataStatus
          in: query
          description: Filter by data completeness status
          schema:
            type: string
            enum: [complete, processing]
        - name: tags
          in: query
          description: Comma-separated list of genre tags to filter by
          schema:
            type: string
        - name: artist
          in: query
          description: Filter by artist name (exact match)
          schema:
            type: string
            maxLength: 200
        - name: author
          in: query
          description: Filter by author name (exact match)
          schema:
            type: string
            maxLength: 200
        - name: creator
          in: query
          description: Filter by creator name (matches either artist or author)
          schema:
            type: string
            maxLength: 200
        - name: minRating
          in: query
          description: Minimum average rating (0-5)
          schema:
            type: number
            minimum: 0
            maximum: 5
        - name: maxRating
          in: query
          description: Maximum average rating (0-5)
          schema:
            type: number
            minimum: 0
            maximum: 5
        - name: minRatings
          in: query
          description: Minimum number of ratings required
          schema:
            type: integer
            minimum: 0
        - name: sort
          in: query
          schema:
            type: string
            enum: [title, release_year, created_at, updated_at, average_rating, total_ratings, chapter_count]
            default: title
        - name: order
          in: query
          schema:
            type: string
            enum: [asc, desc]
            default: asc
        - name: fields
          in: query
          description: |
            Comma-separated list of fields to include in response (sparse fieldsets).
            Available fields: id, title, slug, coverImage, status, orientation, description, artists, authors, creators, releaseYear, releaseMonth, endYear, endMonth, genreTags, averageRating, totalRatings, chapterCount, totalChapters, createdAt, updatedAt
          schema:
            type: string
            example: "id,title,slug,coverImage"
        - name: order[field]
          in: query
          description: |
            Deep object ordering. Use order[fieldName]=asc|desc for multi-field sorting.
            Available fields: title, releaseYear, releaseMonth, endYear, endMonth, createdAt, updatedAt, averageRating, totalRatings, chapterCount.
            In Try It Out, enter comma-separated field assignments such as `releaseYear=desc,title=asc`.
            Example: order[releaseYear]=desc&order[title]=asc
          schema:
            type: string
            enum: [asc, desc]
        - name: releaseYear[gte]
          in: query
          description: Filter by release year greater than or equal to value
          schema: { type: integer }
        - name: releaseYear[lte]
          in: query
          description: Filter by release year less than or equal to value
          schema: { type: integer }
        - name: releaseYear[gt]
          in: query
          description: Filter by release year greater than value
          schema: { type: integer }
        - name: releaseYear[lt]
          in: query
          description: Filter by release year less than value
          schema: { type: integer }
        - name: endYear[gte]
          in: query
          description: Filter by end year greater than or equal to value
          schema: { type: integer }
        - name: endYear[lte]
          in: query
          description: Filter by end year less than or equal to value
          schema: { type: integer }
        - name: endYear[gt]
          in: query
          description: Filter by end year greater than value
          schema: { type: integer }
        - name: endYear[lt]
          in: query
          description: Filter by end year less than value
          schema: { type: integer }
        - name: releaseMonth[gte]
          in: query
          description: Filter by release month greater than or equal to value (1-12)
          schema: { type: integer, minimum: 1, maximum: 12 }
        - name: releaseMonth[lte]
          in: query
          description: Filter by release month less than or equal to value (1-12)
          schema: { type: integer, minimum: 1, maximum: 12 }
        - name: releaseMonth[gt]
          in: query
          description: Filter by release month greater than value (1-12)
          schema: { type: integer, minimum: 1, maximum: 12 }
        - name: releaseMonth[lt]
          in: query
          description: Filter by release month less than value (1-12)
          schema: { type: integer, minimum: 1, maximum: 12 }
        - name: endMonth[gte]
          in: query
          description: Filter by end month greater than or equal to value (1-12)
          schema: { type: integer, minimum: 1, maximum: 12 }
        - name: endMonth[lte]
          in: query
          description: Filter by end month less than or equal to value (1-12)
          schema: { type: integer, minimum: 1, maximum: 12 }
        - name: endMonth[gt]
          in: query
          description: Filter by end month greater than value (1-12)
          schema: { type: integer, minimum: 1, maximum: 12 }
        - name: endMonth[lt]
          in: query
          description: Filter by end month less than value (1-12)
          schema: { type: integer, minimum: 1, maximum: 12 }
        - name: averageRating[gte]
          in: query
          description: Filter by average rating greater than or equal to value
          schema: { type: number, minimum: 0, maximum: 5 }
        - name: averageRating[lte]
          in: query
          description: Filter by average rating less than or equal to value
          schema: { type: number, minimum: 0, maximum: 5 }
        - name: averageRating[gt]
          in: query
          description: Filter by average rating greater than value
          schema: { type: number, minimum: 0, maximum: 5 }
        - name: averageRating[lt]
          in: query
          description: Filter by average rating less than value
          schema: { type: number, minimum: 0, maximum: 5 }
        - name: totalRatings[gte]
          in: query
          description: Filter by total ratings count greater than or equal to value
          schema: { type: integer, minimum: 0 }
        - name: totalRatings[lte]
          in: query
          description: Filter by total ratings count less than or equal to value
          schema: { type: integer, minimum: 0 }
        - name: totalRatings[gt]
          in: query
          description: Filter by total ratings count greater than value
          schema: { type: integer, minimum: 0 }
        - name: totalRatings[lt]
          in: query
          description: Filter by total ratings count less than value
          schema: { type: integer, minimum: 0 }
        - name: chapterCount[gte]
          in: query
          description: Filter by chapter count greater than or equal to value
          schema: { type: integer, minimum: 0 }
        - name: chapterCount[lte]
          in: query
          description: Filter by chapter count less than or equal to value
          schema: { type: integer, minimum: 0 }
        - name: chapterCount[gt]
          in: query
          description: Filter by chapter count greater than value
          schema: { type: integer, minimum: 0 }
        - name: chapterCount[lt]
          in: query
          description: Filter by chapter count less than value
          schema: { type: integer, minimum: 0 }
      responses:
        '200':
          description: Successful response
          headers:
            Link:
              $ref: '#/components/headers/Link'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Pornhwa'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '400':
          description: Invalid parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /pornhwa/{slug}:
    get:
      operationId: getPornhwaBySlug
      summary: Get pornhwa details
      description: |
        Get full details for a single pornhwa by current slug, retired slug, or numeric ID.
        Requests using a retired slug or numeric ID return `Content-Location` with the current canonical API path.

        This endpoint supports conditional requests using ETag and Last-Modified headers
        for efficient caching.

        By default, characters and chapters are NOT included. Use the `includes[]` parameter
        to request them.
      tags:
        - Pornhwa
      x-examples:
        request:
          description: "Get full details for a pornhwa with characters included"
          url: "/api/v1/pornhwa/secret-class?includes[]=characters"
          headers:
            X-API-Key: "pwdb_your_api_key_here"
        response:
          status: 200
          headers:
            Content-Type: "application/json"
            ETag: 'W/"a1b2c3d4e5"'
            Last-Modified: "Sun, 05 Jan 2025 12:00:00 GMT"
            Cache-Control: "public, max-age=60, s-maxage=300"
            X-Request-ID: "req_j3h8w5r2q6v1"
          body:
            requestId: "req_j3h8w5r2q6v1"
            data:
              id: 1
              title: "Secret Class"
              slug: "secret-class"
              status: "Completed"
              dataStatus: "complete"
              coverImage: "https://cdn.pornhwadb.com/covers/secret-class.jpg"
              description: "Dae Ho, an orphan, was taken in by a family friend and has been living with them for 20 years. Now an adult, he realizes he never learned about relationships between men and women..."
              releaseYear: 2019
              releaseMonth: 3
              endYear: 2023
              endMonth: 8
              genreTags: ["Romance", "Drama", "School Life", "Mature"]
              artists: ["Wang Kang Cheol"]
              authors: ["Minachan"]
              creators:
                - id: 10
                  canonicalName: "Wang Kang Cheol"
                  aliases: ["WKC", "Wang KC"]
                  role: "artist"
                - id: 20
                  canonicalName: "Minachan"
                  aliases: []
                  role: "author"
              alternativeTitles: ["Secret Lessons", "Bimilsueop"]
              externalLinks:
                - siteName: "Toptoon"
                  url: "https://toptoon.com/secret-class"
              chapterCount: 178
              totalChapters: 178
              characters:
                - id: 101
                  name: "Dae Ho"
                  image: "https://cdn.pornhwadb.com/characters/dae-ho.jpg"
                  role: "Main"
                  tags: ["Male Lead", "Orphan"]
                  alternativeNames: ["대호"]
                - id: 102
                  name: "Mia"
                  image: "https://cdn.pornhwadb.com/characters/mia.jpg"
                  role: "Main"
                  tags: ["Female Lead", "Student"]
                  alternativeNames: ["미아"]
              createdAt: "2023-01-15T10:30:00Z"
              updatedAt: "2025-01-05T08:00:00Z"
      parameters:
        - name: slug
          in: path
          required: true
          description: Current slug, retired slug, or numeric series ID
          schema:
            type: string
        - $ref: '#/components/parameters/If-None-Match'
        - $ref: '#/components/parameters/If-Modified-Since'
        - name: fields
          in: query
          description: |
            Comma-separated list of fields to include in response (sparse fieldsets).
            Available fields: id, title, slug, coverImage, status, orientation, dataStatus, description, artists, authors, creators, releaseYear, releaseMonth, endYear, endMonth, genreTags, alternativeTitles, externalLinks, chapterCount, totalChapters, createdAt, updatedAt
          schema:
            type: string
            example: "id,title,slug,coverImage,status"
        - name: includes[]
          in: query
          description: |
            Related resources to include. By default, characters and chapters are NOT included.
            Use this parameter to request them.
          schema:
            type: array
            items:
              type: string
              enum: [characters, chapters]
          style: form
          explode: true
      responses:
        '200':
          description: Successful response
          headers:
            ETag:
              $ref: '#/components/headers/ETag'
            Last-Modified:
              $ref: '#/components/headers/Last-Modified'
            Cache-Control:
              $ref: '#/components/headers/Cache-Control'
            Content-Location:
              $ref: '#/components/headers/Content-Location'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/PornhwaDetail'
                  requestId:
                    type: string
                    description: Unique request identifier
        '304':
          $ref: '#/components/responses/NotModified'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Pornhwa not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /pornhwa/batch:
    get:
      operationId: batchGetPornhwa
      summary: Batch fetch pornhwa
      description: |
        Fetch multiple pornhwa in a single request by slugs or IDs.
        Maximum 50 items per request. Returns items in the same order as requested,
        with null for not-found items.
      tags:
        - Pornhwa
      x-examples:
        request:
          description: "Fetch multiple pornhwa by slugs"
          url: "/api/v1/pornhwa/batch?slugs=secret-class,perfect-half,not-found-slug"
          headers:
            X-API-Key: "pwdb_your_api_key_here"
        response:
          status: 200
          headers:
            Content-Type: "application/json"
            X-Request-ID: "req_b4t9y1c6m3f7"
          body:
            requestId: "req_b4t9y1c6m3f7"
            data:
              - id: 1
                title: "Secret Class"
                slug: "secret-class"
                status: "Completed"
                orientation: null
                coverImage: "https://cdn.pornhwadb.com/covers/secret-class.jpg"
                releaseYear: 2019
                genreTags: ["Romance", "Drama"]
              - id: 2
                title: "Perfect Half"
                slug: "perfect-half"
                status: "Completed"
                orientation: "yuri"
                coverImage: "https://cdn.pornhwadb.com/covers/perfect-half.jpg"
                releaseYear: 2018
                genreTags: ["Romance", "Fantasy"]
              - null
      parameters:
        - name: slugs
          in: query
          description: Comma-separated list of slugs to fetch (mutually exclusive with ids)
          schema:
            type: string
            example: "slug1,slug2,slug3"
        - name: ids
          in: query
          description: Comma-separated list of IDs to fetch (mutually exclusive with slugs)
          schema:
            type: string
            example: "1,2,3"
        - name: fields
          in: query
          description: Comma-separated list of fields to include in response (sparse fieldsets)
          schema:
            type: string
            example: "id,title,slug,coverImage"
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      nullable: true
                      allOf:
                        - $ref: '#/components/schemas/Pornhwa'
                    description: Array of pornhwa objects or null for not-found items
                  requestId:
                    type: string
        '400':
          description: Invalid parameters (missing slugs/ids, both provided, or exceeds limit)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /pornhwa/{slug}/chapters:
    get:
      operationId: getPornhwaChapters
      summary: Get chapters
      description: |
        Get chapters for a pornhwa with pagination.

        Use `all=true` to fetch all chapters in a single request (useful for smaller series).
        Default limit is 50 chapters per page.
      tags:
        - Pornhwa
      x-examples:
        request:
          description: "Get chapters for a pornhwa"
          url: "/api/v1/pornhwa/secret-class/chapters?page=1&limit=5"
          headers:
            X-API-Key: "pwdb_your_api_key_here"
        response:
          status: 200
          headers:
            Content-Type: "application/json"
            X-Request-ID: "req_c5n2m8p4k1x9"
            Link: '</api/v1/pornhwa/secret-class/chapters?page=1&limit=5>; rel="first", </api/v1/pornhwa/secret-class/chapters?page=2&limit=5>; rel="next"'
          body:
            requestId: "req_c5n2m8p4k1x9"
            data:
              - id: 1001
                chapterStart: 1
                chapterEnd: null
                description: "Introduction - Dae Ho's new life begins"
                tags: ["Introduction", "Setup"]
                characters:
                  - id: 101
                    name: "Dae Ho"
                    image: "https://cdn.pornhwadb.com/characters/dae-ho.jpg"
                    role: "Main"
                    alternativeNames: ["대호"]
              - id: 1002
                chapterStart: 2
                chapterEnd: 3
                description: "First lessons with Mia"
                tags: ["Romance", "School"]
                characters:
                  - id: 101
                    name: "Dae Ho"
                    alternativeNames: ["대호"]
                  - id: 102
                    name: "Mia"
                    alternativeNames: ["미아"]
              - id: 1003
                chapterStart: 4
                chapterEnd: null
                description: null
                tags: []
                characters: []
            pagination:
              page: 1
              limit: 5
              total: 178
              totalPages: 36
              hasMore: true
              nextUrl: "/api/v1/pornhwa/secret-class/chapters?page=2&limit=5"
              prevUrl: null
              firstUrl: "/api/v1/pornhwa/secret-class/chapters?page=1&limit=5"
              lastUrl: "/api/v1/pornhwa/secret-class/chapters?page=36&limit=5"
      parameters:
        - name: slug
          in: path
          required: true
          description: Current slug, retired slug, or numeric series ID
          schema:
            type: string
        - name: page
          in: query
          schema:
            type: integer
            default: 1
            minimum: 1
            maximum: 1000
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
            minimum: 1
            maximum: 100
        - name: all
          in: query
          description: Set to "true" to fetch all chapters without pagination
          schema:
            type: string
            enum: ["true", "false"]
      responses:
        '200':
          description: Successful response
          headers:
            Link:
              $ref: '#/components/headers/Link'
            Content-Location:
              $ref: '#/components/headers/Content-Location'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Chapter'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
                  requestId:
                    type: string
                    description: Unique request identifier
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Pornhwa not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /pornhwa/{slug}/characters:
    get:
      operationId: getPornhwaCharacters
      summary: Get characters
      description: |
        Get all characters for a pornhwa.

        This endpoint supports conditional requests using ETag and Last-Modified headers
        for efficient caching.
      tags:
        - Pornhwa
      x-examples:
        request:
          description: "Get all characters for a pornhwa"
          url: "/api/v1/pornhwa/secret-class/characters"
          headers:
            X-API-Key: "pwdb_your_api_key_here"
        response:
          status: 200
          headers:
            Content-Type: "application/json"
            ETag: 'W/"char-a1b2c3"'
            Last-Modified: "Sun, 05 Jan 2025 10:00:00 GMT"
            Cache-Control: "public, max-age=60, s-maxage=300"
            X-Request-ID: "req_d6p3n9q5l2y0"
          body:
            requestId: "req_d6p3n9q5l2y0"
            data:
              - id: 101
                name: "Dae Ho"
                image: "https://cdn.pornhwadb.com/characters/dae-ho.jpg"
                role: "Main"
                tags: ["Male Lead", "Orphan", "Student"]
                alternativeNames: ["대호"]
              - id: 102
                name: "Mia"
                image: "https://cdn.pornhwadb.com/characters/mia.jpg"
                role: "Main"
                tags: ["Female Lead", "Student", "Younger Sister"]
                alternativeNames: ["미아"]
              - id: 103
                name: "Sua"
                image: "https://cdn.pornhwadb.com/characters/sua.jpg"
                role: "Supporting"
                tags: ["Mother Figure", "Housewife"]
                alternativeNames: []
              - id: 104
                name: "Yohan"
                image: null
                role: "Supporting"
                tags: ["Father Figure"]
                alternativeNames: []
      parameters:
        - name: slug
          in: path
          required: true
          description: Current slug, retired slug, or numeric series ID
          schema:
            type: string
        - $ref: '#/components/parameters/If-None-Match'
        - $ref: '#/components/parameters/If-Modified-Since'
      responses:
        '200':
          description: Successful response
          headers:
            ETag:
              $ref: '#/components/headers/ETag'
            Last-Modified:
              $ref: '#/components/headers/Last-Modified'
            Cache-Control:
              $ref: '#/components/headers/Cache-Control'
            Content-Location:
              $ref: '#/components/headers/Content-Location'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Character'
        '304':
          $ref: '#/components/responses/NotModified'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Pornhwa not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /pornhwa/{slug}/ratings:
    get:
      operationId: getPornhwaRatings
      summary: Get rating aggregate
      description: |
        Get rating statistics for a pornhwa.

        This endpoint supports conditional requests using ETag and Last-Modified headers
        for efficient caching.
      tags:
        - Pornhwa
      x-examples:
        request:
          description: "Get rating statistics for a pornhwa"
          url: "/api/v1/pornhwa/secret-class/ratings"
          headers:
            X-API-Key: "pwdb_your_api_key_here"
        response:
          status: 200
          headers:
            Content-Type: "application/json"
            ETag: 'W/"rating-x1y2z3"'
            Last-Modified: "Sun, 05 Jan 2025 11:30:00 GMT"
            Cache-Control: "public, max-age=30, s-maxage=60"
            X-Request-ID: "req_e7q4o0r6m3z1"
          body:
            requestId: "req_e7q4o0r6m3z1"
            data:
              averageRating: 4.35
              totalRatings: 2847
              ratingDistribution:
                "0.5": 12
                "1": 28
                "1.5": 15
                "2": 45
                "2.5": 67
                "3": 189
                "3.5": 312
                "4": 587
                "4.5": 892
                "5": 700
      parameters:
        - name: slug
          in: path
          required: true
          description: Current slug, retired slug, or numeric series ID
          schema:
            type: string
        - $ref: '#/components/parameters/If-None-Match'
        - $ref: '#/components/parameters/If-Modified-Since'
      responses:
        '200':
          description: Successful response
          headers:
            ETag:
              $ref: '#/components/headers/ETag'
            Last-Modified:
              $ref: '#/components/headers/Last-Modified'
            Cache-Control:
              $ref: '#/components/headers/Cache-Control'
            Content-Location:
              $ref: '#/components/headers/Content-Location'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/RatingAggregate'
        '304':
          $ref: '#/components/responses/NotModified'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Pornhwa not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /pornhwa/{slug}/reviews:
    get:
      operationId: getPornhwaReviews
      summary: Get reviews
      description: Get reviews for a pornhwa with pagination
      tags:
        - Pornhwa
      x-examples:
        request:
          description: "Get reviews for a pornhwa"
          url: "/api/v1/pornhwa/secret-class/reviews?page=1&limit=3"
          headers:
            X-API-Key: "pwdb_your_api_key_here"
        response:
          status: 200
          headers:
            Content-Type: "application/json"
            X-Request-ID: "req_f8r5p1s7n4a2"
            Link: '</api/v1/pornhwa/secret-class/reviews?page=1&limit=3>; rel="first", </api/v1/pornhwa/secret-class/reviews?page=2&limit=3>; rel="next"'
          body:
            requestId: "req_f8r5p1s7n4a2"
            data:
              - id: 5001
                reviewText: "One of the best adult manhwa I've read. Great character development and an engaging story that keeps you hooked throughout all 178 chapters."
                isEdited: false
                userDisplayName: "ManhwaFan123"
                userRating: 4.5
                createdAt: "2024-12-15T14:30:00Z"
                updatedAt: "2024-12-15T14:30:00Z"
              - id: 5002
                reviewText: "Started strong but the ending felt rushed. Still worth reading for the art alone."
                isEdited: true
                userDisplayName: "ArtLover"
                userRating: 3.5
                createdAt: "2024-11-20T09:15:00Z"
                updatedAt: "2024-11-22T11:00:00Z"
              - id: 5003
                reviewText: "Classic pornhwa that set the standard for the genre. Highly recommended!"
                isEdited: false
                userDisplayName: "GenreExpert"
                userRating: 5
                createdAt: "2024-10-05T18:45:00Z"
                updatedAt: "2024-10-05T18:45:00Z"
            pagination:
              page: 1
              limit: 3
              total: 156
              totalPages: 52
              hasMore: true
              nextUrl: "/api/v1/pornhwa/secret-class/reviews?page=2&limit=3"
              prevUrl: null
              firstUrl: "/api/v1/pornhwa/secret-class/reviews?page=1&limit=3"
              lastUrl: "/api/v1/pornhwa/secret-class/reviews?page=52&limit=3"
      parameters:
        - name: slug
          in: path
          required: true
          description: Current slug, retired slug, or numeric series ID
          schema:
            type: string
        - name: page
          in: query
          schema:
            type: integer
            default: 1
            minimum: 1
            maximum: 1000
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            minimum: 1
            maximum: 100
        - name: sort
          in: query
          description: Sort reviews by field
          schema:
            type: string
            enum: [date, rating]
            default: date
        - name: order
          in: query
          description: Sort order
          schema:
            type: string
            enum: [asc, desc]
            default: desc
      responses:
        '200':
          description: Successful response
          headers:
            Link:
              $ref: '#/components/headers/Link'
            Content-Location:
              $ref: '#/components/headers/Content-Location'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Review'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Pornhwa not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /tags:
    get:
      operationId: listTags
      summary: List all tags
      description: Get all tags grouped by type (genre, character, chapter)
      tags:
        - Tags
      x-examples:
        request:
          description: "Get all tags grouped by type"
          url: "/api/v1/tags"
          headers:
            X-API-Key: "pwdb_your_api_key_here"
        response:
          status: 200
          headers:
            Content-Type: "application/json"
            X-Request-ID: "req_g9s6q2t8o5b3"
          body:
            requestId: "req_g9s6q2t8o5b3"
            data:
              genre:
                - "Romance"
                - "Drama"
                - "Fantasy"
                - "School Life"
                - "Mature"
                - "Action"
                - "Comedy"
                - "Slice of Life"
                - "Supernatural"
                - "Harem"
              character:
                - "Male Lead"
                - "Female Lead"
                - "Student"
                - "Teacher"
                - "Office Worker"
                - "Housewife"
                - "MILF"
                - "Younger Sister"
                - "Older Sister"
                - "Childhood Friend"
              chapter:
                - "Introduction"
                - "Climax"
                - "Ending"
                - "Flashback"
                - "Time Skip"
                - "First Meeting"
                - "Confession"
                - "Beach Episode"
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      genre:
                        type: array
                        items:
                          type: string
                      character:
                        type: array
                        items:
                          type: string
                      chapter:
                        type: array
                        items:
                          type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /tags/{type}:
    get:
      operationId: listTagsByType
      summary: List tags by type
      description: Get tags of a specific type
      tags:
        - Tags
      x-examples:
        request:
          description: "Get all genre tags"
          url: "/api/v1/tags/genre"
          headers:
            X-API-Key: "pwdb_your_api_key_here"
        response:
          status: 200
          headers:
            Content-Type: "application/json"
            X-Request-ID: "req_h0t7r3u9p6c4"
          body:
            requestId: "req_h0t7r3u9p6c4"
            data:
              - "Romance"
              - "Drama"
              - "Fantasy"
              - "School Life"
              - "Mature"
              - "Action"
              - "Comedy"
              - "Slice of Life"
              - "Supernatural"
              - "Harem"
              - "Mystery"
              - "Thriller"
              - "Historical"
              - "Sci-Fi"
      parameters:
        - name: type
          in: path
          required: true
          schema:
            type: string
            enum: [genre, character, chapter]
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: string
        '400':
          description: Invalid tag type
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /tags/available:
    get:
      operationId: getAvailableTags
      summary: Get available tags for filtering (UI Helper)
      description: |
        **UI Helper Endpoint** - Designed for building dynamic filter interfaces.

        Returns tags that are available for selection based on current filters and selected tags.
        This endpoint helps prevent users from selecting tag combinations that would return zero results.

        **Tag Mode Behavior:**
        - `any` (OR mode): Returns all tags of the specified type, excluding already selected tags. Any combination is valid since selecting more tags expands results.
        - `all` (AND mode): Returns only tags that exist on items matching ALL currently selected tags. This ensures users can only select valid tag combinations that will return results.

        **Use Case Example:**
        When building a filter UI, call this endpoint after each tag selection to update the available options.
        This creates a "smart filter" experience where impossible combinations are automatically hidden.
      tags:
        - Tags
      x-examples:
        request:
          description: "Get available character tags in AND mode with 'Asian' already selected"
          url: "/api/v1/tags/available?type=character&selectedTags=Asian&tagMode=all"
          headers:
            X-API-Key: "pwdb_your_api_key_here"
        response:
          status: 200
          headers:
            Content-Type: "application/json"
            X-Request-ID: "req_abc123"
          body:
            requestId: "req_abc123"
            data:
              - "Big Boobs"
              - "Black Hair"
              - "Student"
              - "Office Worker"
      parameters:
        - name: type
          in: query
          required: true
          description: Type of tags to fetch
          schema:
            type: string
            enum: [character, genre]
        - name: selectedTags
          in: query
          required: false
          description: Comma-separated list of already selected tags to exclude from results. In AND mode, only returns tags that co-exist with all selected tags.
          schema:
            type: string
          example: "Asian,Big Boobs"
        - name: tagMode
          in: query
          required: false
          description: |
            Filter mode for determining available tags:
            - `any`: Returns all tags except selected ones (OR logic)
            - `all`: Returns only tags that exist on items with ALL selected tags (AND logic)
          schema:
            type: string
            enum: [any, all]
            default: any
        - name: status
          in: query
          required: false
          description: Filter by pornhwa status
          schema:
            type: string
            enum: [On Going, Completed, Hiatus]
        - name: search
          in: query
          required: false
          description: Filter by search term (matches title, artist, author)
          schema:
            type: string
        - name: creator
          in: query
          required: false
          description: Filter by creator name (artist or author)
          schema:
            type: string
        - name: genreTags
          in: query
          required: false
          description: Comma-separated genre tags to filter by (used when type=character)
          schema:
            type: string
        - name: characterTags
          in: query
          required: false
          description: Comma-separated character tags to filter by (used when type=genre)
          schema:
            type: string
      responses:
        '200':
          description: Available tags for the specified type and filters
          content:
            application/json:
              schema:
                type: object
                properties:
                  requestId:
                    type: string
                    description: Unique request identifier
                  data:
                    type: array
                    items:
                      type: string
                    description: Array of available tag names
              example:
                requestId: "req_abc123"
                data:
                  - "Big Boobs"
                  - "Black Hair"
                  - "Student"
        '400':
          description: Invalid or missing type parameter
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /search:
    get:
      operationId: searchGlobal
      summary: Global search
      description: |
        Search across pornhwa and characters with pagination.

        **Pagination Behavior:**
        When `type=all` (default), the response includes separate pagination objects for each result type.
        The `page` and `limit` parameters apply independently to both pornhwa and character results.

        To paginate through results of a specific type, use `type=pornhwa` or `type=character`
        with the desired page number.

        **Example workflow for paginating all results:**
        1. Initial request: `?q=secret&type=all` - Returns first page of both types
        2. More pornhwa: `?q=secret&type=pornhwa&page=2` - Get page 2 of pornhwa only
        3. More characters: `?q=secret&type=character&page=2` - Get page 2 of characters only
      tags:
        - Search
      x-examples:
        request:
          description: "Search for 'secret' across all content types"
          url: "/api/v1/search?q=secret&type=all&limit=3"
          headers:
            X-API-Key: "pwdb_your_api_key_here"
        response:
          status: 200
          headers:
            Content-Type: "application/json"
            X-Request-ID: "req_i1u8s4v0q7d5"
          body:
            requestId: "req_i1u8s4v0q7d5"
            data:
              pornhwa:
                - id: 1
                  title: "Secret Class"
                  slug: "secret-class"
                  coverImage: "https://cdn.pornhwadb.com/covers/secret-class.jpg"
                  status: "Completed"
                  orientation: "yaoi"
                - id: 45
                  title: "My Secret Roommate"
                  slug: "my-secret-roommate"
                  coverImage: "https://cdn.pornhwadb.com/covers/my-secret-roommate.jpg"
                  status: "On Going"
                  orientation: null
                - id: 78
                  title: "Secret Garden"
                  slug: "secret-garden"
                  coverImage: "https://cdn.pornhwadb.com/covers/secret-garden.jpg"
                  status: "Completed"
                  orientation: "yuri"
              characters:
                - id: 201
                  name: "Secretary Kim"
                  image: "https://cdn.pornhwadb.com/characters/secretary-kim.jpg"
                  pornhwaId: 23
                  pornhwaTitle: "Office Affairs"
                  pornhwaSlug: "office-affairs"
                - id: 202
                  name: "Secret Agent Yuna"
                  image: "https://cdn.pornhwadb.com/characters/secret-agent-yuna.jpg"
                  pornhwaId: 89
                  pornhwaTitle: "Undercover Love"
                  pornhwaSlug: "undercover-love"
            pagination:
              pornhwa:
                page: 1
                limit: 3
                total: 12
                totalPages: 4
                hasMore: true
                nextUrl: "/api/v1/search?q=secret&type=pornhwa&page=2&limit=3"
              characters:
                page: 1
                limit: 3
                total: 8
                totalPages: 3
                hasMore: true
                nextUrl: "/api/v1/search?q=secret&type=character&page=2&limit=3"
      parameters:
        - name: q
          in: query
          required: true
          description: Search query
          schema:
            type: string
            minLength: 1
            maxLength: 200
        - name: type
          in: query
          description: Filter results by type
          schema:
            type: string
            enum: [pornhwa, character, all]
            default: all
        - name: page
          in: query
          description: Page number
          schema:
            type: integer
            default: 1
            minimum: 1
            maximum: 100
        - name: limit
          in: query
          description: Results per page (per type when type=all)
          schema:
            type: integer
            default: 20
            minimum: 1
            maximum: 100
      responses:
        '200':
          description: Successful response
          headers:
            Link:
              $ref: '#/components/headers/Link'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      pornhwa:
                        type: array
                        items:
                          type: object
                          properties:
                            id:
                              type: integer
                            title:
                              type: string
                            slug:
                              type: string
                            coverImage:
                              type: string
                            status:
                              type: string
                            orientation:
                              type: string
                              nullable: true
                              enum: [yaoi, yuri]
                      characters:
                        type: array
                        items:
                          type: object
                          properties:
                            id:
                              type: integer
                            name:
                              type: string
                            image:
                              type: string
                              nullable: true
                            pornhwaId:
                              type: integer
                            pornhwaTitle:
                              type: string
                            pornhwaSlug:
                              type: string
                  pagination:
                    type: object
                    properties:
                      pornhwa:
                        $ref: '#/components/schemas/Pagination'
                      characters:
                        $ref: '#/components/schemas/Pagination'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /discover/trending:
    get:
      operationId: getTrending
      summary: Get trending pornhwa
      description: Get pornhwa trending based on recent activity with pagination
      tags:
        - Discover
      x-examples:
        request:
          description: "Get top 5 trending pornhwa"
          url: "/api/v1/discover/trending?limit=5"
          headers:
            X-API-Key: "pwdb_your_api_key_here"
        response:
          status: 200
          headers:
            Content-Type: "application/json"
            X-Request-ID: "req_j2v9t5w1r8e6"
          body:
            requestId: "req_j2v9t5w1r8e6"
            data:
              - id: 156
                title: "Touch to Unlock"
                slug: "touch-to-unlock"
                status: "On Going"
                coverImage: "https://cdn.pornhwadb.com/covers/touch-to-unlock.jpg"
                releaseYear: 2023
                genreTags: ["Romance", "Fantasy", "Supernatural"]
                averageRating: 4.6
                totalRatings: 1523
                recentViews: 45892
              - id: 89
                title: "Undercover Love"
                slug: "undercover-love"
                status: "On Going"
                coverImage: "https://cdn.pornhwadb.com/covers/undercover-love.jpg"
                releaseYear: 2024
                genreTags: ["Action", "Romance", "Thriller"]
                averageRating: 4.4
                totalRatings: 892
                recentViews: 38456
              - id: 1
                title: "Secret Class"
                slug: "secret-class"
                status: "Completed"
                coverImage: "https://cdn.pornhwadb.com/covers/secret-class.jpg"
                releaseYear: 2019
                genreTags: ["Romance", "Drama", "School Life"]
                averageRating: 4.35
                totalRatings: 2847
                recentViews: 32145
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            default: 1
            minimum: 1
            maximum: 1000
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            minimum: 1
            maximum: 100
        - name: status
          in: query
          description: Filter by publication status
          schema:
            type: string
            enum: [On Going, Completed, Hiatus]
        - name: tags
          in: query
          description: Comma-separated list of genre tags to filter by
          schema:
            type: string
      responses:
        '200':
          description: Successful response
          headers:
            Link:
              $ref: '#/components/headers/Link'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      allOf:
                        - $ref: '#/components/schemas/Pornhwa'
                        - type: object
                          properties:
                            averageRating:
                              type: number
                              nullable: true
                            totalRatings:
                              type: integer
                            recentViews:
                              type: integer
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /discover/top-rated:
    get:
      operationId: getTopRated
      summary: Get top-rated pornhwa
      description: Get pornhwa with highest ratings with pagination
      tags:
        - Discover
      x-examples:
        request:
          description: "Get top 5 rated pornhwa with at least 10 ratings"
          url: "/api/v1/discover/top-rated?limit=5&minRatings=10"
          headers:
            X-API-Key: "pwdb_your_api_key_here"
        response:
          status: 200
          headers:
            Content-Type: "application/json"
            X-Request-ID: "req_k3w0u6x2s9f7"
          body:
            requestId: "req_k3w0u6x2s9f7"
            data:
              - id: 234
                title: "Queen Bee"
                slug: "queen-bee"
                status: "Completed"
                coverImage: "https://cdn.pornhwadb.com/covers/queen-bee.jpg"
                releaseYear: 2020
                genreTags: ["Romance", "Drama", "Mature"]
                averageRating: 4.78
                totalRatings: 3421
              - id: 156
                title: "Touch to Unlock"
                slug: "touch-to-unlock"
                status: "On Going"
                coverImage: "https://cdn.pornhwadb.com/covers/touch-to-unlock.jpg"
                releaseYear: 2023
                genreTags: ["Romance", "Fantasy"]
                averageRating: 4.65
                totalRatings: 1523
              - id: 2
                title: "Perfect Half"
                slug: "perfect-half"
                status: "Completed"
                coverImage: "https://cdn.pornhwadb.com/covers/perfect-half.jpg"
                releaseYear: 2018
                genreTags: ["Romance", "Fantasy"]
                averageRating: 4.52
                totalRatings: 2156
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            default: 1
            minimum: 1
            maximum: 1000
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            minimum: 1
            maximum: 100
        - name: minRatings
          in: query
          description: Minimum number of ratings required
          schema:
            type: integer
            default: 5
        - name: status
          in: query
          description: Filter by publication status
          schema:
            type: string
            enum: [On Going, Completed, Hiatus]
        - name: tags
          in: query
          description: Comma-separated list of genre tags to filter by
          schema:
            type: string
      responses:
        '200':
          description: Successful response
          headers:
            Link:
              $ref: '#/components/headers/Link'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      allOf:
                        - $ref: '#/components/schemas/Pornhwa'
                        - type: object
                          properties:
                            averageRating:
                              type: number
                            totalRatings:
                              type: integer
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /discover/random:
    get:
      operationId: getRandom
      summary: Get random pornhwa
      description: Get random pornhwa for discovery
      tags:
        - Discover
      x-examples:
        request:
          description: "Get 3 random pornhwa"
          url: "/api/v1/discover/random?count=3"
          headers:
            X-API-Key: "pwdb_your_api_key_here"
        response:
          status: 200
          headers:
            Content-Type: "application/json"
            Cache-Control: "no-store"
            X-Request-ID: "req_l4x1v7y3t0g8"
          body:
            requestId: "req_l4x1v7y3t0g8"
            data:
              - id: 342
                title: "Boarding Diary"
                slug: "boarding-diary"
                status: "Completed"
                coverImage: "https://cdn.pornhwadb.com/covers/boarding-diary.jpg"
                releaseYear: 2021
                genreTags: ["Romance", "Drama", "Slice of Life"]
                averageRating: 4.2
                totalRatings: 1876
              - id: 67
                title: "Sweet Guy"
                slug: "sweet-guy"
                status: "Completed"
                coverImage: "https://cdn.pornhwadb.com/covers/sweet-guy.jpg"
                releaseYear: 2014
                genreTags: ["Romance", "Supernatural", "Comedy"]
                averageRating: 4.1
                totalRatings: 2543
              - id: 189
                title: "Stepmother Friends"
                slug: "stepmother-friends"
                status: "On Going"
                coverImage: "https://cdn.pornhwadb.com/covers/stepmother-friends.jpg"
                releaseYear: 2022
                genreTags: ["Romance", "Drama", "Mature"]
                averageRating: 4.3
                totalRatings: 1234
      parameters:
        - name: count
          in: query
          description: Number of random results to return
          schema:
            type: integer
            default: 1
            minimum: 1
            maximum: 10
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      allOf:
                        - $ref: '#/components/schemas/Pornhwa'
                        - type: object
                          properties:
                            averageRating:
                              type: number
                              nullable: true
                            totalRatings:
                              type: integer
                  requestId:
                    type: string
                    description: Unique request identifier
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /discover/recently-updated:
    get:
      operationId: getRecentlyUpdated
      summary: Get recently updated pornhwa
      description: Get pornhwa ordered by most recently updated with pagination
      tags:
        - Discover
      x-examples:
        request:
          description: "Get 5 most recently updated pornhwa"
          url: "/api/v1/discover/recently-updated?limit=5"
          headers:
            X-API-Key: "pwdb_your_api_key_here"
        response:
          status: 200
          headers:
            Content-Type: "application/json"
            X-Request-ID: "req_m5y2w8z4u1h9"
          body:
            requestId: "req_m5y2w8z4u1h9"
            data:
              - id: 156
                title: "Touch to Unlock"
                slug: "touch-to-unlock"
                status: "On Going"
                coverImage: "https://cdn.pornhwadb.com/covers/touch-to-unlock.jpg"
                releaseYear: 2023
                genreTags: ["Romance", "Fantasy"]
                averageRating: 4.6
                totalRatings: 1523
                updatedAt: "2025-01-05T08:30:00Z"
              - id: 89
                title: "Undercover Love"
                slug: "undercover-love"
                status: "On Going"
                coverImage: "https://cdn.pornhwadb.com/covers/undercover-love.jpg"
                releaseYear: 2024
                genreTags: ["Action", "Romance"]
                averageRating: 4.4
                totalRatings: 892
                updatedAt: "2025-01-05T06:15:00Z"
              - id: 189
                title: "Stepmother Friends"
                slug: "stepmother-friends"
                status: "On Going"
                coverImage: "https://cdn.pornhwadb.com/covers/stepmother-friends.jpg"
                releaseYear: 2022
                genreTags: ["Romance", "Drama"]
                averageRating: 4.3
                totalRatings: 1234
                updatedAt: "2025-01-04T22:00:00Z"
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            default: 1
            minimum: 1
            maximum: 1000
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            minimum: 1
            maximum: 100
        - name: status
          in: query
          description: Filter by publication status
          schema:
            type: string
            enum: [On Going, Completed, Hiatus]
      responses:
        '200':
          description: Successful response
          headers:
            Link:
              $ref: '#/components/headers/Link'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      allOf:
                        - $ref: '#/components/schemas/Pornhwa'
                        - type: object
                          properties:
                            averageRating:
                              type: number
                              nullable: true
                            totalRatings:
                              type: integer
                            updatedAt:
                              type: string
                              format: date-time
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /discover/new:
    get:
      operationId: getNewReleases
      summary: Get new releases
      description: Get newly added pornhwa ordered by creation date with pagination
      tags:
        - Discover
      x-examples:
        request:
          description: "Get 5 newest additions to the database"
          url: "/api/v1/discover/new?limit=5"
          headers:
            X-API-Key: "pwdb_your_api_key_here"
        response:
          status: 200
          headers:
            Content-Type: "application/json"
            X-Request-ID: "req_n6z3x9a5v2i0"
          body:
            requestId: "req_n6z3x9a5v2i0"
            data:
              - id: 456
                title: "New Beginning"
                slug: "new-beginning"
                status: "On Going"
                coverImage: "https://cdn.pornhwadb.com/covers/new-beginning.jpg"
                releaseYear: 2025
                genreTags: ["Romance", "Drama", "Fantasy"]
                averageRating: null
                totalRatings: 0
                createdAt: "2025-01-05T10:00:00Z"
              - id: 455
                title: "Office Romance"
                slug: "office-romance"
                status: "On Going"
                coverImage: "https://cdn.pornhwadb.com/covers/office-romance.jpg"
                releaseYear: 2024
                genreTags: ["Romance", "Slice of Life"]
                averageRating: 4.0
                totalRatings: 12
                createdAt: "2025-01-04T15:30:00Z"
              - id: 454
                title: "Campus Life"
                slug: "campus-life"
                status: "On Going"
                coverImage: "https://cdn.pornhwadb.com/covers/campus-life.jpg"
                releaseYear: 2024
                genreTags: ["Romance", "School Life", "Comedy"]
                averageRating: 3.8
                totalRatings: 25
                createdAt: "2025-01-03T09:00:00Z"
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            default: 1
            minimum: 1
            maximum: 1000
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            minimum: 1
            maximum: 100
        - name: status
          in: query
          description: Filter by publication status
          schema:
            type: string
            enum: [On Going, Completed, Hiatus]
      responses:
        '200':
          description: Successful response
          headers:
            Link:
              $ref: '#/components/headers/Link'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      allOf:
                        - $ref: '#/components/schemas/Pornhwa'
                        - type: object
                          properties:
                            averageRating:
                              type: number
                              nullable: true
                            totalRatings:
                              type: integer
                            createdAt:
                              type: string
                              format: date-time
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /pornhwa/{slug}/similar:
    get:
      operationId: getSimilarPornhwa
      summary: Get similar pornhwa
      description: |
        Find similar pornhwa based on shared genre tags and creators.
        Results are ordered by number of shared tags, then by shared authors/artists.
      tags:
        - Pornhwa
      x-examples:
        request:
          description: "Get pornhwa similar to Secret Class"
          url: "/api/v1/pornhwa/secret-class/similar?limit=5"
          headers:
            X-API-Key: "pwdb_your_api_key_here"
        response:
          status: 200
          headers:
            Content-Type: "application/json"
            X-Request-ID: "req_o7a4y0b6w3j1"
          body:
            requestId: "req_o7a4y0b6w3j1"
            data:
              - id: 342
                title: "Boarding Diary"
                slug: "boarding-diary"
                status: "Completed"
                orientation: null
                coverImage: "https://cdn.pornhwadb.com/covers/boarding-diary.jpg"
                releaseYear: 2021
                genreTags: ["Romance", "Drama", "School Life", "Mature"]
                averageRating: 4.2
                totalRatings: 1876
                sharedTags: 4
                hasSharedAuthor: false
                hasSharedArtist: false
              - id: 234
                title: "Queen Bee"
                slug: "queen-bee"
                status: "Completed"
                orientation: "yaoi"
                coverImage: "https://cdn.pornhwadb.com/covers/queen-bee.jpg"
                releaseYear: 2020
                genreTags: ["Romance", "Drama", "Mature"]
                averageRating: 4.78
                totalRatings: 3421
                sharedTags: 3
                hasSharedAuthor: false
                hasSharedArtist: false
              - id: 567
                title: "Private Lessons"
                slug: "private-lessons"
                status: "On Going"
                orientation: null
                coverImage: "https://cdn.pornhwadb.com/covers/private-lessons.jpg"
                releaseYear: 2023
                genreTags: ["Romance", "School Life"]
                averageRating: 4.1
                totalRatings: 456
                sharedTags: 2
                hasSharedAuthor: true
                hasSharedArtist: false
      parameters:
        - name: slug
          in: path
          required: true
          description: Current slug, retired slug, or numeric series ID
          schema:
            type: string
        - name: limit
          in: query
          schema:
            type: integer
            default: 10
            minimum: 1
            maximum: 50
      responses:
        '200':
          description: Successful response
          headers:
            Content-Location:
              $ref: '#/components/headers/Content-Location'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      allOf:
                        - $ref: '#/components/schemas/Pornhwa'
                        - type: object
                          properties:
                            averageRating:
                              type: number
                              nullable: true
                            totalRatings:
                              type: integer
                            sharedTags:
                              type: integer
                              description: Number of shared genre tags
                            hasSharedAuthor:
                              type: boolean
                              description: Whether this pornhwa shares an author
                            hasSharedArtist:
                              type: boolean
                              description: Whether this pornhwa shares an artist
                  requestId:
                    type: string
                    description: Unique request identifier
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Pornhwa not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /characters:
    get:
      operationId: searchCharacters
      summary: Search characters
      description: Search characters by name with pagination. Returns all characters if no name is provided.
      tags:
        - Characters
      x-examples:
        request:
          description: "Search for characters named 'Mia'"
          url: "/api/v1/characters?name=Mia&limit=5"
          headers:
            X-API-Key: "pwdb_your_api_key_here"
        response:
          status: 200
          headers:
            Content-Type: "application/json"
            X-Request-ID: "req_p8b5z1c7x4k2"
          body:
            requestId: "req_p8b5z1c7x4k2"
            data:
              - id: 102
                name: "Mia"
                image: "https://cdn.pornhwadb.com/characters/mia.jpg"
                role: "Main"
                tags: ["Female Lead", "Student", "Younger Sister"]
                alternativeNames: ["미아"]
                description: null
                pornhwa:
                  id: 1
                  title: "Secret Class"
                  slug: "secret-class"
              - id: 456
                name: "Mia Kim"
                image: "https://cdn.pornhwadb.com/characters/mia-kim.jpg"
                role: "Main"
                tags: ["Female Lead", "Office Worker"]
                alternativeNames: []
                description: null
                pornhwa:
                  id: 78
                  title: "Office Affairs"
                  slug: "office-affairs"
              - id: 789
                name: "Mia Park"
                image: null
                role: "Supporting"
                tags: ["Student", "Childhood Friend"]
                alternativeNames: []
                description: null
                pornhwa:
                  id: 234
                  title: "Campus Days"
                  slug: "campus-days"
            pagination:
              page: 1
              limit: 5
              total: 12
              totalPages: 3
              hasMore: true
              nextUrl: "/api/v1/characters?name=Mia&page=2&limit=5"
              prevUrl: null
              firstUrl: "/api/v1/characters?name=Mia&page=1&limit=5"
              lastUrl: "/api/v1/characters?name=Mia&page=3&limit=5"
      parameters:
        - name: name
          in: query
          description: Character name to search for (case-insensitive)
          schema:
            type: string
            maxLength: 200
        - name: page
          in: query
          schema:
            type: integer
            default: 1
            minimum: 1
            maximum: 1000
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            minimum: 1
            maximum: 100
        - name: sort
          in: query
          description: Sort characters by field
          schema:
            type: string
            enum: [name, role]
            default: name
        - name: order
          in: query
          description: Sort order
          schema:
            type: string
            enum: [asc, desc]
            default: asc
        - name: tags
          in: query
          description: Comma-separated list of character tags to filter by
          schema:
            type: string
        - name: tagMode
          in: query
          description: How to match tags - 'any' matches characters with at least one tag, 'all' requires all tags
          schema:
            type: string
            enum: [any, all]
            default: any
        - name: pornhwaId
          in: query
          description: Filter by pornhwa ID
          schema:
            type: integer
        - name: pornhwaSlug
          in: query
          description: Filter by pornhwa slug
          schema:
            type: string
            maxLength: 255
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: integer
                        name:
                          type: string
                        image:
                          type: string
                          nullable: true
                        role:
                          type: string
                          enum: [Main, Supporting]
                          nullable: true
                        tags:
                          type: array
                          items:
                            type: string
                        alternativeNames:
                          type: array
                          items:
                            type: string
                          description: Alternative names for the character
                        description:
                          type: string
                          nullable: true
                          description: Brief character description
                        pornhwa:
                          type: object
                          properties:
                            id:
                              type: integer
                            title:
                              type: string
                            slug:
                              type: string
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /character-ratings/{characterId}:
    get:
      operationId: getCharacterRatings
      summary: Get character ratings
      description: Get individual user ratings for a character with pagination.
      tags:
        - Characters
      x-examples:
        request:
          description: "Get ratings for a character"
          url: "/api/v1/character-ratings/102?page=1&limit=2"
          headers:
            X-API-Key: "pwdb_your_api_key_here"
        response:
          status: 200
          headers:
            Content-Type: "application/json"
            X-Request-ID: "req_c7h4r5t1n2g9"
            Link: '</api/v1/character-ratings/102?page=1&limit=2>; rel="first", </api/v1/character-ratings/102?page=2&limit=2>; rel="next"'
          body:
            requestId: "req_c7h4r5t1n2g9"
            data:
              - id: 5501
                characterId: 102
                rating: 4.5
                userDisplayName: "ManhwaFan123"
                avatarImage: "https://cdn.pornhwadb.com/avatars/manhwafan123.jpg"
                createdAt: "2024-12-15T14:30:00Z"
                updatedAt: "2024-12-15T14:30:00Z"
              - id: 5502
                characterId: 102
                rating: 3.5
                userDisplayName: "ArtLover"
                avatarImage: null
                createdAt: "2024-11-20T09:15:00Z"
                updatedAt: "2024-11-22T11:00:00Z"
            pagination:
              page: 1
              limit: 2
              total: 24
              totalPages: 12
              hasMore: true
              nextUrl: "/api/v1/character-ratings/102?page=2&limit=2"
              prevUrl: null
              firstUrl: "/api/v1/character-ratings/102?page=1&limit=2"
              lastUrl: "/api/v1/character-ratings/102?page=12&limit=2"
      parameters:
        - name: characterId
          in: path
          required: true
          description: Numeric ID of the character
          schema:
            type: integer
            minimum: 1
        - name: page
          in: query
          schema:
            type: integer
            default: 1
            minimum: 1
            maximum: 1000
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            minimum: 1
            maximum: 100
      responses:
        '200':
          description: Successful response
          headers:
            Link:
              $ref: '#/components/headers/Link'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/CharacterRating'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Character not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /character-reviews/{characterId}:
    get:
      operationId: getCharacterReviews
      summary: Get character reviews
      description: Get reviews for a character with pagination.
      tags:
        - Characters
      x-examples:
        request:
          description: "Get reviews for a character"
          url: "/api/v1/character-reviews/102?page=1&limit=2"
          headers:
            X-API-Key: "pwdb_your_api_key_here"
        response:
          status: 200
          headers:
            Content-Type: "application/json"
            X-Request-ID: "req_c7h4r5v1e2w9"
            Link: '</api/v1/character-reviews/102?page=1&limit=2>; rel="first", </api/v1/character-reviews/102?page=2&limit=2>; rel="next"'
          body:
            requestId: "req_c7h4r5v1e2w9"
            data:
              - id: 8801
                characterId: 102
                reviewText: "Mia's arc is the emotional core of the series. Fantastic writing."
                isEdited: false
                userDisplayName: "ManhwaFan123"
                userRating: 4.5
                avatarImage: "https://cdn.pornhwadb.com/avatars/manhwafan123.jpg"
                createdAt: "2024-12-15T14:30:00Z"
                updatedAt: "2024-12-15T14:30:00Z"
              - id: 8802
                characterId: 102
                reviewText: "Great design, but underused in the later chapters."
                isEdited: true
                userDisplayName: "ArtLover"
                userRating: 3.5
                avatarImage: null
                createdAt: "2024-11-20T09:15:00Z"
                updatedAt: "2024-11-22T11:00:00Z"
            pagination:
              page: 1
              limit: 2
              total: 24
              totalPages: 12
              hasMore: true
              nextUrl: "/api/v1/character-reviews/102?page=2&limit=2"
              prevUrl: null
              firstUrl: "/api/v1/character-reviews/102?page=1&limit=2"
              lastUrl: "/api/v1/character-reviews/102?page=12&limit=2"
      parameters:
        - name: characterId
          in: path
          required: true
          description: Numeric ID of the character
          schema:
            type: integer
            minimum: 1
        - name: page
          in: query
          schema:
            type: integer
            default: 1
            minimum: 1
            maximum: 1000
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            minimum: 1
            maximum: 100
      responses:
        '200':
          description: Successful response
          headers:
            Link:
              $ref: '#/components/headers/Link'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/CharacterReview'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Character not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /lists:
    get:
      operationId: browsePublicLists
      summary: Browse public lists
      description: |
        Browse public user-curated lists, ordered by popularity (like count, then
        recency). Only public lists that contain at least one item are returned.
      tags:
        - Lists
      x-examples:
        request:
          description: "Browse the most popular public lists"
          url: "/api/v1/lists?page=1&limit=2"
          headers:
            X-API-Key: "pwdb_your_api_key_here"
        response:
          status: 200
          headers:
            Content-Type: "application/json"
            X-Request-ID: "req_l1s7br0ws3ab"
            Link: '</api/v1/lists?page=1&limit=2>; rel="first", </api/v1/lists?page=2&limit=2>; rel="next"'
          body:
            requestId: "req_l1s7br0ws3ab"
            data:
              - slug: "best-office-romances"
                title: "Best Office Romances"
                description: "My favourite workplace slow-burns."
                isPublic: true
                likeCount: 128
                itemCount: 14
                coverImages:
                  - "https://cdn.pornhwadb.com/covers/series-a.jpg"
                  - "https://cdn.pornhwadb.com/covers/series-b.jpg"
                username: "ManhwaFan123"
                avatarImage: "https://cdn.pornhwadb.com/avatars/manhwafan123.jpg"
                createdAt: "2024-10-01T12:00:00Z"
                updatedAt: "2024-12-20T09:30:00Z"
            pagination:
              page: 1
              limit: 2
              total: 57
              totalPages: 29
              hasMore: true
              nextUrl: "/api/v1/lists?page=2&limit=2"
              prevUrl: null
              firstUrl: "/api/v1/lists?page=1&limit=2"
              lastUrl: "/api/v1/lists?page=29&limit=2"
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            default: 1
            minimum: 1
            maximum: 1000
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            minimum: 1
            maximum: 100
      responses:
        '200':
          description: Successful response
          headers:
            Link:
              $ref: '#/components/headers/Link'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/ListSummary'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /lists/{username}/{slug}:
    get:
      operationId: getPublicList
      summary: Get a public list
      description: |
        Get a single public list by its owner's username and slug, including its
        items and collaborators. Private lists return `404`.
      tags:
        - Lists
      x-examples:
        request:
          description: "Get a public list with its items"
          url: "/api/v1/lists/ManhwaFan123/best-office-romances"
          headers:
            X-API-Key: "pwdb_your_api_key_here"
        response:
          status: 200
          headers:
            Content-Type: "application/json"
            X-Request-ID: "req_l1s7d3741l5x"
            ETag: '"1703068200000"'
          body:
            requestId: "req_l1s7d3741l5x"
            data:
              slug: "best-office-romances"
              title: "Best Office Romances"
              description: "My favourite workplace slow-burns."
              isPublic: true
              likeCount: 128
              itemCount: 1
              username: "ManhwaFan123"
              avatarImage: "https://cdn.pornhwadb.com/avatars/manhwafan123.jpg"
              isCollaborative: false
              createdAt: "2024-10-01T12:00:00Z"
              updatedAt: "2024-12-20T09:30:00Z"
              items:
                - id: 9012
                  pornhwaId: 341
                  position: 0
                  addedAt: "2024-10-01T12:05:00Z"
                  title: "Corporate Heat"
                  slug: "corporate-heat"
                  coverImage: "https://cdn.pornhwadb.com/covers/series-a.jpg"
                  status: "On Going"
                  notes: "Peak from chapter 40 onward."
                  addedByUsername: "ManhwaFan123"
                  chapters: []
              collaborators: []
      parameters:
        - name: username
          in: path
          required: true
          description: Username of the list owner
          schema:
            type: string
        - name: slug
          in: path
          required: true
          description: URL-friendly identifier of the list
          schema:
            type: string
      responses:
        '200':
          description: Successful response
          headers:
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/ListDetail'
                  requestId:
                    type: string
        '304':
          $ref: '#/components/responses/NotModified'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: List not found or not public
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /activity:
    get:
      operationId: getActivityFeed
      summary: Get the global activity feed
      description: |
        Get the global community activity feed with pagination. Events include
        reviews, ratings, tracking-status changes, and list activity. Filter by
        one or more event families using the `types` parameter.
      tags:
        - Activity
      x-examples:
        request:
          description: "Get the latest activity, reviews and ratings only"
          url: "/api/v1/activity?page=1&limit=2&types=reviews,ratings"
          headers:
            X-API-Key: "pwdb_your_api_key_here"
        response:
          status: 200
          headers:
            Content-Type: "application/json"
            X-Request-ID: "req_4c71v17yf33d"
            Link: '</api/v1/activity?page=1&limit=2&types=reviews,ratings>; rel="first", </api/v1/activity?page=2&limit=2&types=reviews,ratings>; rel="next"'
          body:
            requestId: "req_4c71v17yf33d"
            data:
              - id: "reviews:8801"
                eventType: "review_posted"
                occurredAt: "2024-12-20T09:30:00Z"
                actor:
                  username: "ManhwaFan123"
                  avatarImage: "https://cdn.pornhwadb.com/avatars/manhwafan123.jpg"
                target:
                  kind: "pornhwa"
                  id: 341
                  title: "Corporate Heat"
                  slug: "corporate-heat"
                  coverImage: "https://cdn.pornhwadb.com/covers/series-a.jpg"
                metadata:
                  reviewText: "The slow-burn payoff is worth it."
                  rating: 4.5
            pagination:
              page: 1
              limit: 2
              total: 240
              totalPages: 120
              hasMore: true
              nextUrl: "/api/v1/activity?page=2&limit=2&types=reviews,ratings"
              prevUrl: null
              firstUrl: "/api/v1/activity?page=1&limit=2&types=reviews,ratings"
              lastUrl: "/api/v1/activity?page=120&limit=2&types=reviews,ratings"
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            default: 1
            minimum: 1
            maximum: 1000
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            minimum: 1
            maximum: 50
        - name: types
          in: query
          description: |
            Comma-separated list of event families to include. Omit for all types.
          schema:
            type: string
            example: "reviews,ratings"
          x-enum-values:
            - reviews
            - ratings
            - tracking
            - lists
      responses:
        '200':
          description: Successful response
          headers:
            Link:
              $ref: '#/components/headers/Link'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/ActivityFeedItem'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /creators:
    get:
      operationId: searchCreators
      summary: Search creators
      description: |
        Search for authors and artists. Searches both canonical names and aliases. When a search matches an alias, the `matchedAlias` field indicates which alias was matched.
      tags:
        - Creators
      x-examples:
        request:
          description: "Search for creators with 'Park' in their name"
          url: "/api/v1/creators?name=Park&type=all&limit=5"
          headers:
            X-API-Key: "pwdb_your_api_key_here"
        response:
          status: 200
          headers:
            Content-Type: "application/json"
            X-Request-ID: "req_q9c6a2d8y5l3"
          body:
            requestId: "req_q9c6a2d8y5l3"
            data:
              - id: 42
                name: "Park Hyeongjun"
                canonicalName: "Park Hyeongjun"
                aliases: ["Park HJ", "PHJ"]
                types: ["author", "artist"]
                totalWorks: 8
              - id: 87
                name: "Park Seongjin"
                canonicalName: "Park Seongjin"
                aliases: []
                types: ["author"]
                totalWorks: 5
              - id: 103
                name: "Park Mina"
                canonicalName: "Park Mina"
                aliases: ["Mina P"]
                types: ["artist"]
                totalWorks: 3
            pagination:
              page: 1
              limit: 5
              total: 4
              totalPages: 1
              hasMore: false
              nextUrl: null
              prevUrl: null
              firstUrl: "/api/v1/creators?name=Park&type=all&page=1&limit=5"
              lastUrl: "/api/v1/creators?name=Park&type=all&page=1&limit=5"
      parameters:
        - name: name
          in: query
          description: Creator name to search for (case-insensitive)
          schema:
            type: string
            maxLength: 200
        - name: type
          in: query
          description: Filter by creator type
          schema:
            type: string
            enum: [author, artist, all]
            default: all
        - name: page
          in: query
          schema:
            type: integer
            default: 1
            minimum: 1
            maximum: 1000
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            minimum: 1
            maximum: 100
        - name: sort
          in: query
          description: Sort creators by field
          schema:
            type: string
            enum: [name, works]
            default: works
        - name: order
          in: query
          description: Sort order
          schema:
            type: string
            enum: [asc, desc]
            default: desc
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: integer
                          description: Creator ID
                        name:
                          type: string
                          description: Creator name (same as canonicalName, kept for convenience)
                        canonicalName:
                          type: string
                          description: Primary/canonical name of the creator
                        aliases:
                          type: array
                          description: Alternative names for this creator
                          items:
                            type: string
                        types:
                          type: array
                          items:
                            type: string
                            enum: [author, artist]
                          description: Roles this creator has (author, artist, or both)
                        totalWorks:
                          type: integer
                          description: Total number of works by this creator
                        matchedAlias:
                          type: string
                          nullable: true
                          description: The alias that matched the search query, if the search matched an alias instead of the canonical name
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /stats:
    get:
      operationId: getStats
      summary: Get API statistics
      description: Get aggregate statistics about the database content.
      tags:
        - Stats
      x-examples:
        request:
          description: "Get database statistics"
          url: "/api/v1/stats"
          headers:
            X-API-Key: "pwdb_your_api_key_here"
        response:
          status: 200
          headers:
            Content-Type: "application/json"
            Cache-Control: "public, max-age=300, s-maxage=600"
            X-Request-ID: "req_r0d7b3e9z6m4"
          body:
            requestId: "req_r0d7b3e9z6m4"
            data:
              totalPornhwa: 847
              totalCharacters: 12456
              totalChapters: 45678
              totalReviews: 3421
              totalRatings: 28945
              totalTags:
                genre: 52
                character: 128
                chapter: 87
      responses:
        '200':
          description: Successful response
          headers:
            Cache-Control:
              $ref: '#/components/headers/Cache-Control'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/ApiStats'
                  requestId:
                    type: string
              example:
                data:
                  totalPornhwa: 450
                  totalCharacters: 5000
                  totalChapters: 15000
                  totalReviews: 1200
                  totalRatings: 8500
                  totalTags:
                    genre: 50
                    character: 100
                    chapter: 75
                requestId: "550e8400-e29b-41d4-a716-446655440000"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /health:
    get:
      operationId: healthCheck
      summary: Health check
      description: |
        Check if the API is healthy and responding.

        **Note**: This endpoint does NOT require API key authentication.
      tags:
        - Health
      security: []
      x-examples:
        request:
          description: "Check API health (no authentication required)"
          url: "/api/v1/health"
          headers: {}
        response:
          status: 200
          headers:
            Content-Type: "application/json"
            X-Request-ID: "req_s1e8c4f0a7n5"
          body:
            requestId: "req_s1e8c4f0a7n5"
            status: "healthy"
            version: "1.0.0"
            timestamp: "2025-01-05T12:00:00Z"
      responses:
        '200':
          description: API is healthy
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: "healthy"
                  version:
                    type: string
                    description: API version
                    example: "1.0.0"
                  timestamp:
                    type: string
                    format: date-time
                  requestId:
                    type: string
                    description: Unique request identifier
        '500':
          $ref: '#/components/responses/InternalError'

  /me/tracking:
    get:
      operationId: getMyTracking
      summary: List my tracking
      description: |
        Returns the authenticated user's tracking library, paginated.

        The API key identifies which user's data is returned — you always see your own library.
        Results are ordered by last updated (most recent first) by default.

        Use the `status` filter to view a specific shelf (e.g. currently reading, plan to read).
        `Cache-Control: private, no-store` — responses are never cached.
      tags:
        - User
      x-examples:
        request:
          description: "List currently reading titles"
          url: "/api/v1/me/tracking?status=reading&page=1&limit=3"
          headers:
            X-API-Key: "pwdb_your_api_key_here"
        response:
          status: 200
          headers:
            Content-Type: "application/json"
            Cache-Control: "private, no-store"
            X-Request-ID: "req_u1a8r4e0l7s5"
          body:
            requestId: "req_u1a8r4e0l7s5"
            data:
              - id: 1001
                pornhwa:
                  id: 42
                  title: "Secret Class"
                  slug: "secret-class"
                  coverImage: "https://cdn.pornhwadb.com/covers/secret-class.jpg"
                  status: "Completed"
                  totalChapters: 178
                  genreTags: ["Romance", "Drama"]
                status: "reading"
                currentChapter: 45
                startedAt: "2025-01-10"
                finishedAt: null
                notes: "Great so far"
                timesCompleted: 0
                priority: null
                isPrivate: false
                hasReview: false
                createdAt: "2025-01-10T08:00:00Z"
                updatedAt: "2025-04-20T14:30:00Z"
            pagination:
              page: 1
              limit: 3
              total: 12
              totalPages: 4
              hasMore: true
              nextUrl: "/api/v1/me/tracking?status=reading&page=2&limit=3"
              prevUrl: null
              firstUrl: "/api/v1/me/tracking?status=reading&page=1&limit=3"
              lastUrl: "/api/v1/me/tracking?status=reading&page=4&limit=3"
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            default: 1
            minimum: 1
            maximum: 1000
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            minimum: 1
            maximum: 100
        - name: status
          in: query
          description: Filter by tracking status
          schema:
            type: string
            enum: [reading, completed, dropped, plan_to_read, on_hold, re_reading]
        - name: sort
          in: query
          description: Sort field
          schema:
            type: string
            enum: [updated_at, created_at]
            default: updated_at
      responses:
        '200':
          description: Successful response
          headers:
            Link:
              $ref: '#/components/headers/Link'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/TrackingEntry'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
                  requestId:
                    type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /me/tracking/{identifier}:
    get:
      operationId: getMyTrackingEntry
      summary: Get single tracking entry
      description: |
        Returns a single tracking entry for a series by slug or numeric ID.
        Returns 404 if the series exists but is not in your tracking list.
      tags:
        - User
      x-examples:
        request:
          description: "Get tracking entry for Secret Class"
          url: "/api/v1/me/tracking/secret-class"
          headers:
            X-API-Key: "pwdb_your_api_key_here"
        response:
          status: 200
          headers:
            Content-Type: "application/json"
            Cache-Control: "private, no-store"
            X-Request-ID: "req_u2b9s5f1m8t6"
          body:
            requestId: "req_u2b9s5f1m8t6"
            data:
              id: 1001
              pornhwa:
                id: 42
                title: "Secret Class"
                slug: "secret-class"
                coverImage: "https://cdn.pornhwadb.com/covers/secret-class.jpg"
                status: "Completed"
                totalChapters: 178
                genreTags: ["Romance", "Drama"]
              status: "reading"
              currentChapter: 45
              startedAt: "2025-01-10"
              finishedAt: null
              notes: "Great so far"
              timesCompleted: 0
              priority: null
              isPrivate: false
              hasReview: false
              createdAt: "2025-01-10T08:00:00Z"
              updatedAt: "2025-04-20T14:30:00Z"
      parameters:
        - name: identifier
          in: path
          required: true
          description: Series slug (e.g. `secret-class`) or numeric ID (e.g. `42`)
          schema:
            type: string
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/TrackingEntry'
                  requestId:
                    type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Series not found, or not in your tracking list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

    put:
      operationId: upsertMyTrackingEntry
      summary: Add or update tracking entry
      description: |
        Creates or updates a tracking entry for a series.

        **Upsert semantics**: if no entry exists it is created (returns `201`); if one already exists it is updated (returns `200`).

        All fields except `status` are optional. Fields you omit are left unchanged on update, or set to null on create.

        **No auto-transitions**: supply field values explicitly. The API does not silently set dates or increment counters based on status.

        **Setting `isPrivate: true`** hides this entry from your public profile at `/u/{uid}/tracking`.

        **Priority labels**: 1 = Low, 2 = Medium-Low, 3 = Medium, 4 = Medium-High, 5 = High.
      tags:
        - User
      x-examples:
        request:
          description: "Start tracking Secret Class at chapter 1"
          url: "/api/v1/me/tracking/secret-class"
          headers:
            X-API-Key: "pwdb_your_api_key_here"
            Content-Type: "application/json"
          body:
            status: "reading"
            currentChapter: 1
            startedAt: "2025-05-01"
        response:
          status: 201
          headers:
            Content-Type: "application/json"
            X-Request-ID: "req_u3c0t6g2n9u7"
          body:
            requestId: "req_u3c0t6g2n9u7"
            data:
              id: 1001
              pornhwa:
                id: 42
                title: "Secret Class"
                slug: "secret-class"
                coverImage: "https://cdn.pornhwadb.com/covers/secret-class.jpg"
                status: "Completed"
                totalChapters: 178
                genreTags: ["Romance", "Drama"]
              status: "reading"
              currentChapter: 1
              startedAt: "2025-05-01"
              finishedAt: null
              notes: null
              timesCompleted: 0
              priority: null
              isPrivate: false
              hasReview: false
              createdAt: "2025-05-01T09:00:00Z"
              updatedAt: "2025-05-01T09:00:00Z"
      parameters:
        - name: identifier
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TrackingUpsertInput'
      responses:
        '200':
          description: Entry updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/TrackingEntry'
                  requestId:
                    type: string
        '201':
          description: Entry created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/TrackingEntry'
                  requestId:
                    type: string
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Series not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

    delete:
      operationId: deleteMyTrackingEntry
      summary: Remove tracking entry
      description: |
        Removes a series from your tracking list.

        **Idempotent**: returns `204` whether or not the entry existed.

        Your rating and favorites for this series are **not** removed — only the tracking entry itself.
      tags:
        - User
      parameters:
        - name: identifier
          in: path
          required: true
          schema:
            type: string
      responses:
        '204':
          description: Entry removed (or did not exist)
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Series not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /me/tracking/{identifier}/rating:
    patch:
      operationId: upsertMyRating
      summary: Rate a series
      description: |
        Creates or updates your rating for a series.

        **Requires a tracking entry** — add the series to your tracking list first.

        Ratings use a 0.5-step scale from 0.5 to 5.0. Invalid values (e.g. 2.3) return a 400.
      tags:
        - User
      x-examples:
        request:
          description: "Rate Secret Class 4.5 stars"
          url: "/api/v1/me/tracking/secret-class/rating"
          headers:
            X-API-Key: "pwdb_your_api_key_here"
            Content-Type: "application/json"
          body:
            rating: 4.5
        response:
          status: 200
          headers:
            Content-Type: "application/json"
            X-Request-ID: "req_u4d1r7h3o0v8"
          body:
            requestId: "req_u4d1r7h3o0v8"
            data:
              rating: 4.5
      parameters:
        - name: identifier
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [rating]
              properties:
                rating:
                  type: number
                  description: Rating value (0.5 to 5.0 in 0.5 increments)
                  minimum: 0.5
                  maximum: 5.0
                  multipleOf: 0.5
                  example: 4.5
      responses:
        '200':
          description: Rating saved
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      rating:
                        type: number
                  requestId:
                    type: string
        '400':
          description: Invalid rating value
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Series not found, or no tracking entry exists
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

    delete:
      operationId: deleteMyRating
      summary: Remove rating
      description: |
        Removes your rating for a series.

        **Idempotent**: returns `204` whether or not a rating existed.
      tags:
        - User
      parameters:
        - name: identifier
          in: path
          required: true
          schema:
            type: string
      responses:
        '204':
          description: Rating removed (or did not exist)
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Series not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /me/tracking/{identifier}/favorite:
    put:
      operationId: addMyFavorite
      summary: Favorite a series
      description: |
        Adds a series to your favorites.

        **Requires a tracking entry** — add the series to your tracking list first.

        **Idempotent**: safe to call multiple times. Returns `200` whether or not it was already favorited.
        No request body needed.
      tags:
        - User
      parameters:
        - name: identifier
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Series favorited
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      favorited:
                        type: boolean
                        example: true
                      created:
                        type: boolean
                        description: true if newly added, false if already existed
                  requestId:
                    type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Series not found, or no tracking entry exists
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

    delete:
      operationId: removeMyFavorite
      summary: Unfavorite a series
      description: |
        Removes a series from your favorites.

        **Idempotent**: returns `204` whether or not it was favorited.
      tags:
        - User
      parameters:
        - name: identifier
          in: path
          required: true
          schema:
            type: string
      responses:
        '204':
          description: Series unfavorited (or was not favorited)
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Series not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /me/tracking/{identifier}/actions/mark-completed:
    post:
      operationId: actionMarkCompleted
      summary: Mark as completed
      description: |
        Compound action: sets status to `completed`, records today's date as `finishedAt`,
        and increments `timesCompleted`.

        **Requires a tracking entry** — add the series to your tracking list first.
        No request body needed.
      tags:
        - User
      parameters:
        - name: identifier
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Entry updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/TrackingEntry'
                  requestId:
                    type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Series not found, or no tracking entry exists
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /me/tracking/{identifier}/actions/start-rereading:
    post:
      operationId: actionStartRereading
      summary: Start re-reading
      description: |
        Compound action: sets status to `re_reading`, records today's date as `startedAt`,
        and clears `currentChapter` and `finishedAt` to start fresh.

        **Requires a tracking entry** — add the series to your tracking list first.
        No request body needed.
      tags:
        - User
      parameters:
        - name: identifier
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Entry updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/TrackingEntry'
                  requestId:
                    type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Series not found, or no tracking entry exists
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /me/tracking/{identifier}/actions/increment-chapter:
    post:
      operationId: actionIncrementChapter
      summary: Increment chapter progress
      description: |
        Increments `currentChapter` by 1.

        **Requires a tracking entry** — add the series to your tracking list first.
        No request body needed.
      tags:
        - User
      parameters:
        - name: identifier
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Chapter incremented
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      currentChapter:
                        type: integer
                        description: New currentChapter value after increment
                  requestId:
                    type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Series not found, or no tracking entry exists
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /creators/{id}:
    get:
      operationId: getCreator
      summary: Get creator details
      description: Get a single creator (author/artist) with aliases, roles, work count and social links.
      tags:
        - Creators
      parameters:
        - name: id
          in: path
          required: true
          description: Numeric creator ID
          schema:
            type: integer
            minimum: 1
            example: 42
        - $ref: '#/components/parameters/If-None-Match'
        - $ref: '#/components/parameters/If-Modified-Since'
      responses:
        '200':
          description: Successful response
          headers:
            Cache-Control:
              $ref: '#/components/headers/Cache-Control'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
                      name:
                        type: string
                      canonicalName:
                        type: string
                      aliases:
                        type: array
                        items:
                          type: string
                      types:
                        type: array
                        items:
                          type: string
                          enum: [author, artist]
                      totalWorks:
                        type: integer
                      socialLinks:
                        type: array
                        items:
                          type: object
                          properties:
                            id:
                              type: integer
                            platform:
                              type: string
                            url:
                              type: string
                            label:
                              type: string
                              nullable: true
                      createdAt:
                        type: string
                        format: date-time
                      updatedAt:
                        type: string
                        format: date-time
                  requestId:
                    type: string
        '304':
          $ref: '#/components/responses/NotModified'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Creator not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /reviews:
    get:
      operationId: listReviews
      summary: Browse all reviews
      description: Get a paginated list of series reviews across the database. Optionally scope to a single series with pornhwaId.
      tags:
        - Reviews
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: pornhwaId
          in: query
          description: Restrict results to a single series
          schema:
            type: integer
            minimum: 1
      responses:
        '200':
          description: Successful response
          headers:
            Cache-Control:
              $ref: '#/components/headers/Cache-Control'
            Link:
              $ref: '#/components/headers/Link'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: integer
                        pornhwaId:
                          type: integer
                        pornhwaTitle:
                          type: string
                        reviewText:
                          type: string
                        isEdited:
                          type: boolean
                        userDisplayName:
                          type: string
                          nullable: true
                        userRating:
                          type: number
                          nullable: true
                        avatarImage:
                          type: string
                          nullable: true
                        createdAt:
                          type: string
                          format: date-time
                        updatedAt:
                          type: string
                          format: date-time
                  pagination:
                    $ref: '#/components/schemas/Pagination'
                  requestId:
                    type: string
        '400':
          description: Invalid parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /ratings:
    get:
      operationId: listRatings
      summary: Browse all ratings
      description: Get a paginated list of per-series rating aggregates. Each entry is one series' aggregate rating.
      tags:
        - Ratings
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: sort
          in: query
          schema:
            type: string
            enum: [rating, ratings, updated]
            default: ratings
        - name: order
          in: query
          schema:
            type: string
            enum: [asc, desc]
            default: desc
        - name: minRatings
          in: query
          description: Only include series with at least this many ratings
          schema:
            type: integer
            minimum: 0
            default: 1
      responses:
        '200':
          description: Successful response
          headers:
            Cache-Control:
              $ref: '#/components/headers/Cache-Control'
            Link:
              $ref: '#/components/headers/Link'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        pornhwaId:
                          type: integer
                        title:
                          type: string
                        slug:
                          type: string
                        coverImage:
                          type: string
                          nullable: true
                        averageRating:
                          type: number
                          nullable: true
                        totalRatings:
                          type: integer
                        ratingDistribution:
                          type: object
                          additionalProperties:
                            type: integer
                        lastUpdated:
                          type: string
                          format: date-time
                  pagination:
                    $ref: '#/components/schemas/Pagination'
                  requestId:
                    type: string
        '400':
          description: Invalid parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /chapters/search:
    get:
      operationId: searchChapters
      summary: Search chapters
      description: Search chapters by chapter tags, character tags, series status and minimum rating.
      tags:
        - Chapters
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: tags
          in: query
          description: Comma-separated chapter tag names
          schema:
            type: string
            example: "Threesome,Public"
        - name: tagMode
          in: query
          description: Match any or all of the supplied chapter tags
          schema:
            type: string
            enum: [any, all]
            default: any
        - name: characterTags
          in: query
          description: Comma-separated character tag names
          schema:
            type: string
        - name: status
          in: query
          description: Comma-separated series statuses
          schema:
            type: string
            example: "Completed,On Going"
        - name: minRating
          in: query
          schema:
            type: number
            minimum: 0
            maximum: 10
        - name: sort
          in: query
          schema:
            type: string
            enum: [default, tag-matches, release-newest, release-oldest, added-newest, added-oldest]
            default: default
      responses:
        '200':
          description: Successful response
          headers:
            Cache-Control:
              $ref: '#/components/headers/Cache-Control'
            Link:
              $ref: '#/components/headers/Link'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        chapter:
                          $ref: '#/components/schemas/Chapter'
                        pornhwa:
                          type: object
                          properties:
                            id:
                              type: integer
                            title:
                              type: string
                            slug:
                              type: string
                            coverImage:
                              type: string
                            status:
                              type: string
                            rating:
                              type: number
                              nullable: true
                  pagination:
                    $ref: '#/components/schemas/Pagination'
                  requestId:
                    type: string
        '400':
          description: Invalid parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /chapters/tags:
    get:
      operationId: listChapterTags
      summary: Get chapter tags
      description: List all chapter tags with their chapter counts and synonyms.
      tags:
        - Chapters
      parameters:
        - name: minRating
          in: query
          description: Only count chapters from series with at least this average rating
          schema:
            type: number
            minimum: 0
            maximum: 10
      responses:
        '200':
          description: Successful response
          headers:
            Cache-Control:
              $ref: '#/components/headers/Cache-Control'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        name:
                          type: string
                        count:
                          type: integer
                        synonyms:
                          type: array
                          items:
                            type: string
                  requestId:
                    type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /chapters/tag/{tag}:
    get:
      operationId: getChaptersByTag
      summary: Get chapters by tag
      description: List chapters that carry the given chapter tag, with pagination.
      tags:
        - Chapters
      parameters:
        - name: tag
          in: path
          required: true
          description: Chapter tag name (URL-encoded)
          schema:
            type: string
            example: "Threesome"
        - name: page
          in: query
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
      responses:
        '200':
          description: Successful response
          headers:
            Cache-Control:
              $ref: '#/components/headers/Cache-Control'
            Link:
              $ref: '#/components/headers/Link'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        chapter:
                          $ref: '#/components/schemas/Chapter'
                        pornhwa:
                          type: object
                          properties:
                            id:
                              type: integer
                            title:
                              type: string
                            slug:
                              type: string
                            coverImage:
                              type: string
                            status:
                              type: string
                  pagination:
                    $ref: '#/components/schemas/Pagination'
                  requestId:
                    type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Chapter tag not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /stats/leaderboard:
    get:
      operationId: getLeaderboard
      summary: Get user leaderboard
      description: User leaderboard ranked by contribution or gallery activity. Returns up to the top 50 users, paginated.
      tags:
        - Stats
      parameters:
        - name: type
          in: query
          schema:
            type: string
            enum: [contributors, gallery]
            default: contributors
        - name: period
          in: query
          schema:
            type: string
            enum: [all, month, week]
            default: all
        - name: page
          in: query
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
      responses:
        '200':
          description: Successful response
          headers:
            Cache-Control:
              $ref: '#/components/headers/Cache-Control'
            Link:
              $ref: '#/components/headers/Link'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        rank:
                          type: integer
                        userId:
                          type: string
                        username:
                          type: string
                        avatarImage:
                          type: string
                          nullable: true
                        total:
                          type: integer
                        seriesCount:
                          type: integer
                        editsCount:
                          type: integer
                        galleryCount:
                          type: integer
                        reviewsCount:
                          type: integer
                        isEditor:
                          type: boolean
                  pagination:
                    $ref: '#/components/schemas/Pagination'
                  requestId:
                    type: string
        '400':
          description: Invalid parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /u/{uid}/profile:
    get:
      operationId: getUserProfile
      summary: Get user profile
      description: Returns a user's public profile and aggregate contribution stats.
      tags:
        - Users
      parameters:
        - name: uid
          in: path
          required: true
          description: Canonical user identifier
          schema:
            type: string
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      profile:
                        $ref: '#/components/schemas/PublicUserProfile'
                      stats:
                        $ref: '#/components/schemas/UserContributionStats'
                  requestId:
                    type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: User not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /u/{uid}/ratings:
    get:
      operationId: getUserRatings
      summary: Get user pornhwa ratings
      description: A user's pornhwa ratings, most recent first, paginated.
      tags:
        - Users
      parameters:
        - name: uid
          in: path
          required: true
          description: Canonical user identifier
          schema:
            type: string
        - name: page
          in: query
          schema:
            type: integer
            default: 1
            minimum: 1
            maximum: 1000
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            minimum: 1
            maximum: 100
      responses:
        '200':
          description: Successful response
          headers:
            Link:
              $ref: '#/components/headers/Link'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/UserRating'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: User not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /u/{uid}/reviews:
    get:
      operationId: getUserReviews
      summary: Get user pornhwa reviews
      description: A user's pornhwa reviews, most recent first, paginated.
      tags:
        - Users
      parameters:
        - name: uid
          in: path
          required: true
          description: Canonical user identifier
          schema:
            type: string
        - name: page
          in: query
          schema:
            type: integer
            default: 1
            minimum: 1
            maximum: 1000
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            minimum: 1
            maximum: 100
      responses:
        '200':
          description: Successful response
          headers:
            Link:
              $ref: '#/components/headers/Link'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/UserReview'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: User not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /u/{uid}/character-ratings:
    get:
      operationId: getUserCharacterRatings
      summary: Get user character ratings
      description: A user's character ratings, most recent first, paginated.
      tags:
        - Users
      parameters:
        - name: uid
          in: path
          required: true
          description: Canonical user identifier
          schema:
            type: string
        - name: page
          in: query
          schema:
            type: integer
            default: 1
            minimum: 1
            maximum: 1000
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            minimum: 1
            maximum: 100
      responses:
        '200':
          description: Successful response
          headers:
            Link:
              $ref: '#/components/headers/Link'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/UserCharacterRating'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: User not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /u/{uid}/character-reviews:
    get:
      operationId: getUserCharacterReviews
      summary: Get user character reviews
      description: A user's character reviews, most recent first, paginated.
      tags:
        - Users
      parameters:
        - name: uid
          in: path
          required: true
          description: Canonical user identifier
          schema:
            type: string
        - name: page
          in: query
          schema:
            type: integer
            default: 1
            minimum: 1
            maximum: 1000
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            minimum: 1
            maximum: 100
      responses:
        '200':
          description: Successful response
          headers:
            Link:
              $ref: '#/components/headers/Link'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/UserCharacterReview'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: User not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /u/{uid}/tracking:
    get:
      operationId: getUserTracking
      summary: Get user tracking list
      description: >-
        A user's public tracking list, paginated. Returns an empty collection
        when the user has not made their tracking public.
      tags:
        - Users
      parameters:
        - name: uid
          in: path
          required: true
          description: Canonical user identifier
          schema:
            type: string
        - name: page
          in: query
          schema:
            type: integer
            default: 1
            minimum: 1
            maximum: 1000
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            minimum: 1
            maximum: 100
      responses:
        '200':
          description: Successful response
          headers:
            Link:
              $ref: '#/components/headers/Link'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/UserTrackingEntry'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: User not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /u/{uid}/lists:
    get:
      operationId: getUserLists
      summary: Get user public lists
      description: >-
        A user's public lists, and public lists they collaborate on, most
        recently updated first.
      tags:
        - Users
      parameters:
        - name: uid
          in: path
          required: true
          description: Canonical user identifier
          schema:
            type: string
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/UserList'
                  requestId:
                    type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: User not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /u/{uid}/contributions:
    get:
      operationId: getUserContributions
      summary: Get user contribution history
      description: >-
        A user's contribution history (added series and approved edits), most
        recent first, paginated. The `period` filter restricts results to the
        current month or week; `kind` restricts results to added or edited series.
      tags:
        - Users
      parameters:
        - name: uid
          in: path
          required: true
          description: Canonical user identifier
          schema:
            type: string
        - name: period
          in: query
          description: Restrict contributions to a time window
          schema:
            type: string
            enum: [all, month, week]
            default: all
        - name: kind
          in: query
          description: Restrict contributions to added series or approved edits
          schema:
            type: string
            enum: [all, added, edited]
            default: all
        - name: page
          in: query
          schema:
            type: integer
            default: 1
            minimum: 1
            maximum: 1000
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            minimum: 1
            maximum: 100
      responses:
        '200':
          description: Successful response
          headers:
            Link:
              $ref: '#/components/headers/Link'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/UserContribution'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: User not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /u/{uid}/editor-activity:
    get:
      operationId: getUserEditorActivity
      summary: Get user editor activity
      description: >-
        The series a user wants to edit, is working on, or has completed.
        Restricted to the profile owner (the API key holder) or active editors;
        other callers receive 403.
      tags:
        - Users
      parameters:
        - name: uid
          in: path
          required: true
          description: Canonical user identifier
          schema:
            type: string
        - name: status
          in: query
          description: Filter by editor activity status
          schema:
            type: string
            enum: [want_to_edit, working_on, done]
        - name: page
          in: query
          schema:
            type: integer
            default: 1
            minimum: 1
            maximum: 1000
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            minimum: 1
            maximum: 100
      responses:
        '200':
          description: Successful response
          headers:
            Link:
              $ref: '#/components/headers/Link'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/UserEditorActivityItem'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: User not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /u/{uid}/followers:
    get:
      operationId: getUserFollowers
      summary: Get user followers
      description: Users who follow this user, most recent first, paginated.
      tags:
        - Users
      parameters:
        - name: uid
          in: path
          required: true
          description: Canonical user identifier
          schema:
            type: string
        - name: page
          in: query
          schema:
            type: integer
            default: 1
            minimum: 1
            maximum: 1000
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            minimum: 1
            maximum: 100
      responses:
        '200':
          description: Successful response
          headers:
            Link:
              $ref: '#/components/headers/Link'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/UserFollowUser'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: User not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /u/{uid}/following:
    get:
      operationId: getUserFollowing
      summary: Get users a user follows
      description: Users this user follows, most recent first, paginated.
      tags:
        - Users
      parameters:
        - name: uid
          in: path
          required: true
          description: Canonical user identifier
          schema:
            type: string
        - name: page
          in: query
          schema:
            type: integer
            default: 1
            minimum: 1
            maximum: 1000
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            minimum: 1
            maximum: 100
      responses:
        '200':
          description: Successful response
          headers:
            Link:
              $ref: '#/components/headers/Link'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/UserFollowUser'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: User not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
