openapi: 3.0.3
info:
  title: Cinetok API
  version: 0.9.0
  description: |
    Batch 9 peer tracker + mobile peer manager contract.
servers:
  - url: https://cinetokpro.com
paths:
  /health/live:
    get:
      summary: Liveness probe
      operationId: getLive
      responses:
        '200':
          description: API process is alive
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthResponse'
  /health/ready:
    get:
      summary: Readiness probe
      operationId: getReady
      responses:
        '200':
          description: Dependencies are reachable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthResponse'
        '503':
          description: Dependency failure
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/auth/register:
    post:
      summary: Register account
      operationId: register
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RegisterRequest'
      responses:
        '201':
          description: Registered
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthResponse'
  /v1/auth/login:
    post:
      summary: Login
      operationId: login
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LoginRequest'
      responses:
        '200':
          description: Authenticated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthResponse'
  /v1/auth/refresh:
    post:
      summary: Rotate refresh token
      operationId: refresh
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RefreshRequest'
      responses:
        '200':
          description: Refreshed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthResponse'
  /v1/auth/logout:
    post:
      summary: Revoke session
      responses:
        '200':
          description: Logged out
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: ok
  /v1/me:
    get:
      summary: Current user
      operationId: me
      security:
        - bearerAuth: []
      responses:
        '200':
          description: User profile
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
  /v1/users/{id}:
    get:
      summary: User profile by id
      operationId: getUserById
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: User profile
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/videos/{id}:
    get:
      summary: Get video metadata and playable HLS URL
      operationId: getVideo
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Video metadata
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VideoResponse'
        '404':
          description: Video not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    delete:
      summary: Soft-delete a video uploaded by the authenticated user
      operationId: deleteVideo
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '204':
          description: Video deleted
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Video not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/videos/drafts:
    post:
      summary: Create video draft and presigned upload URL
      operationId: createVideoDraft
      security:
        - bearerAuth: []
      parameters:
        - name: Idempotency-Key
          in: header
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateVideoDraftRequest'
      responses:
        '201':
          description: Draft created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateVideoDraftResponse'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Request conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/videos/{id}/complete-upload:
    post:
      summary: Mark upload completed and enqueue transcode
      operationId: completeVideoUpload
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: Idempotency-Key
          in: header
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CompleteVideoUploadRequest'
      responses:
        '202':
          description: Upload accepted and transcode job enqueued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CompleteVideoUploadResponse'
        '404':
          description: Video not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Conflict (missing object or stale state)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/admin/me:
    get:
      summary: Current admin representative role assignment
      operationId: adminMe
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Role assignment
          content:
            application/json:
              schema:
                type: object
        '403':
          description: Forbidden
  /v1/admin/overview:
    get:
      summary: Admin analytics overview
      operationId: adminOverview
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Overview metrics
          content:
            application/json:
              schema:
                type: object
        '403':
          description: Forbidden
  /v1/admin/users:
    get:
      summary: List users for moderation
      operationId: adminListUsers
      security:
        - bearerAuth: []
      parameters:
        - name: query
          in: query
          required: false
          schema:
            type: string
        - name: limit
          in: query
          required: false
          schema:
            type: integer
      responses:
        '200':
          description: Users
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
  /v1/admin/users/{id}/state:
    patch:
      summary: Ban/restrict/soft-delete/restore user
      operationId: adminUpdateUserState
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
      responses:
        '204':
          description: Updated
  /v1/admin/videos:
    get:
      summary: List videos for moderation
      operationId: adminListVideos
      security:
        - bearerAuth: []
      parameters:
        - name: query
          in: query
          required: false
          schema:
            type: string
        - name: limit
          in: query
          required: false
          schema:
            type: integer
      responses:
        '200':
          description: Videos
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
  /v1/admin/videos/{id}/state:
    patch:
      summary: Block/unblock/soft-delete/restore video
      operationId: adminUpdateVideoState
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
      responses:
        '204':
          description: Updated
  /v1/admin/reports:
    get:
      summary: List moderation reports
      operationId: adminListReports
      security:
        - bearerAuth: []
      parameters:
        - name: status
          in: query
          required: false
          schema:
            type: string
        - name: limit
          in: query
          required: false
          schema:
            type: integer
      responses:
        '200':
          description: Reports
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
  /v1/admin/reports/{id}/state:
    patch:
      summary: Update report status
      operationId: adminUpdateReportStatus
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
      responses:
        '200':
          description: Updated report
          content:
            application/json:
              schema:
                type: object
  /v1/admin/tickets:
    get:
      summary: List support queries
      operationId: adminListTickets
      security:
        - bearerAuth: []
      parameters:
        - name: status
          in: query
          required: false
          schema:
            type: string
        - name: limit
          in: query
          required: false
          schema:
            type: integer
      responses:
        '200':
          description: Tickets
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
    post:
      summary: Create support query
      operationId: adminCreateTicket
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
      responses:
        '201':
          description: Created ticket
          content:
            application/json:
              schema:
                type: object
  /v1/admin/tickets/{id}/state:
    patch:
      summary: Update support query status
      operationId: adminUpdateTicket
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
      responses:
        '200':
          description: Updated ticket
          content:
            application/json:
              schema:
                type: object
  /v1/admin/representatives:
    get:
      summary: List admin representatives
      operationId: adminListRepresentatives
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Representatives
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
  /v1/admin/representatives/{id}:
    put:
      summary: Assign or update representative role
      operationId: adminUpsertRepresentative
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
      responses:
        '200':
          description: Representative assignment
          content:
            application/json:
              schema:
                type: object
  /v1/admin/accounts:
    post:
      summary: Super-admin creates account using email/password
      operationId: adminCreateAccount
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
      responses:
        '201':
          description: Account created
          content:
            application/json:
              schema:
                type: object
        '403':
          description: Forbidden
        '409':
          description: Conflict
  /v1/feeds/for-you:
    get:
      summary: For You feed page
      operationId: getForYouFeed
      parameters:
        - name: cursor
          in: query
          required: false
          schema:
            type: string
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 20
      responses:
        '200':
          description: Feed page
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FeedPageResponse'
  /v1/feeds/following:
    get:
      summary: Following feed page
      operationId: getFollowingFeed
      security:
        - bearerAuth: []
      parameters:
        - name: cursor
          in: query
          required: false
          schema:
            type: string
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 20
      responses:
        '200':
          description: Feed page
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FeedPageResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/search:
    get:
      summary: Search creators and live videos
      operationId: searchContent
      parameters:
        - name: q
          in: query
          required: true
          schema:
            type: string
            minLength: 2
            maxLength: 128
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 30
      responses:
        '200':
          description: Search result payload
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SearchResponse'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/engagement/videos/{id}/likes/toggle:
    post:
      summary: Toggle like on a video
      operationId: toggleVideoLike
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Like toggled
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ToggleLikeResponse'
  /v1/engagement/videos/{id}/comments:
    post:
      summary: Create comment
      operationId: createComment
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateCommentRequest'
      responses:
        '201':
          description: Comment created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateCommentResponse'
    get:
      summary: List comments
      operationId: listComments
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: cursor
          in: query
          required: false
          schema:
            type: string
        - name: limit
          in: query
          required: false
          schema:
            type: integer
      responses:
        '200':
          description: Comments page
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CommentPageResponse'
  /v1/engagement/users/{id}/follow-toggle:
    post:
      summary: Toggle follow relationship
      operationId: toggleFollow
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Follow toggled
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ToggleFollowResponse'
  /v1/reports:
    post:
      summary: Submit a moderation report for a video
      operationId: createReport
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateReportRequest'
      responses:
        '201':
          description: Report created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateReportResponse'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Video not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Duplicate open report
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/notifications:
    get:
      summary: List notifications for current user
      operationId: listNotifications
      security:
        - bearerAuth: []
      parameters:
        - name: cursor
          in: query
          required: false
          schema:
            type: string
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 50
      responses:
        '200':
          description: Notification page
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NotificationPageResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/realtime:
    get:
      summary: Websocket stream for realtime unread-count updates
      operationId: realtimeNotifications
      security:
        - bearerAuth: []
      parameters:
        - name: access_token
          in: query
          required: false
          schema:
            type: string
      responses:
        '101':
          description: Switching protocols to websocket
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /peer/heartbeat:
    post:
      summary: Peer presence heartbeat and segment inventory update
      operationId: peerHeartbeat
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PeerHeartbeatRequest'
      responses:
        '200':
          description: Presence accepted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PeerHeartbeatResponse'
        '404':
          description: Feature disabled
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /peer/candidates:
    get:
      summary: Peer candidate directory for one video segment
      operationId: peerCandidates
      security:
        - bearerAuth: []
      parameters:
        - name: peerId
          in: query
          required: false
          schema:
            type: string
            format: uuid
        - name: videoId
          in: query
          required: true
          schema:
            type: string
            format: uuid
        - name: variant
          in: query
          required: true
          schema:
            type: string
        - name: segmentIndex
          in: query
          required: true
          schema:
            type: integer
            minimum: 0
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 30
      responses:
        '200':
          description: Candidate list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PeerCandidatesResponse'
        '404':
          description: Feature disabled
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /peer/result:
    post:
      summary: Report peer segment transfer result and telemetry
      operationId: peerResult
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PeerResultRequest'
      responses:
        '200':
          description: Result recorded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PeerResultResponse'
        '404':
          description: Feature disabled
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /openapi:
    get:
      summary: OpenAPI document
      operationId: getOpenAPI
      responses:
        '200':
          description: OpenAPI YAML
          content:
            text/yaml:
              schema:
                type: string
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
  schemas:
    HealthResponse:
      type: object
      required: [status, service, timestamp]
      properties:
        status:
          type: string
          example: ok
        service:
          type: string
          example: cinetok-api
        timestamp:
          type: string
          format: date-time
    ErrorResponse:
      type: object
      required: [error, requestId]
      properties:
        error:
          type: string
        requestId:
          type: string
    RegisterRequest:
      type: object
      required: [username, email, password]
      properties:
        username:
          type: string
        email:
          type: string
          format: email
        password:
          type: string
        adminCandidate:
          type: boolean
          description: When true, creates a pending (inactive) admin representative request for approval.
    LoginRequest:
      type: object
      required: [login, password]
      properties:
        login:
          type: string
        password:
          type: string
    RefreshRequest:
      type: object
      required: [refreshToken]
      properties:
        refreshToken:
          type: string
    AuthTokens:
      type: object
      required: [accessToken, accessTokenExpiresAt, refreshToken, refreshTokenExpiresAt, tokenType]
      properties:
        accessToken:
          type: string
        accessTokenExpiresAt:
          type: string
          format: date-time
        refreshToken:
          type: string
        refreshTokenExpiresAt:
          type: string
          format: date-time
        tokenType:
          type: string
          example: Bearer
    User:
      type: object
      required: [id, username, email, bio, createdAt, updatedAt]
      properties:
        id:
          type: string
          format: uuid
        username:
          type: string
        email:
          type: string
        bio:
          type: string
        avatarUrl:
          type: string
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    AuthResponse:
      type: object
      required: [user, tokens]
      properties:
        user:
          $ref: '#/components/schemas/User'
        tokens:
          $ref: '#/components/schemas/AuthTokens'
    CreateVideoDraftRequest:
      type: object
      required: [fileName, contentType, fileSizeBytes]
      properties:
        caption:
          type: string
          maxLength: 2200
        fileName:
          type: string
          example: clip.mp4
        contentType:
          type: string
          example: video/mp4
        fileSizeBytes:
          type: integer
          format: int64
    SignedUpload:
      type: object
      required: [method, url, expiresAt]
      properties:
        method:
          type: string
          example: PUT
        url:
          type: string
        headers:
          type: object
          additionalProperties:
            type: string
        expiresAt:
          type: string
          format: date-time
    CreateVideoDraftResponse:
      type: object
      required: [videoId, status, sourceObjectKey, upload, createdAt, updatedAt, idempotent]
      properties:
        videoId:
          type: string
          format: uuid
        status:
          type: string
          example: draft
        caption:
          type: string
        sourceObjectKey:
          type: string
        upload:
          $ref: '#/components/schemas/SignedUpload'
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        idempotent:
          type: boolean
    CompleteVideoUploadRequest:
      type: object
      required: [sourceObjectKey]
      properties:
        sourceObjectKey:
          type: string
    CompleteVideoUploadResponse:
      type: object
      required: [videoId, status, sourceObjectKey, jobId, enqueuedAt, idempotent]
      properties:
        videoId:
          type: string
          format: uuid
        status:
          type: string
          example: processing
        sourceObjectKey:
          type: string
        jobId:
          type: string
        enqueuedAt:
          type: string
          format: date-time
        idempotent:
          type: boolean
    VideoResponse:
      type: object
      required: [videoId, userId, status, createdAt, updatedAt]
      properties:
        videoId:
          type: string
          format: uuid
        userId:
          type: string
          format: uuid
        caption:
          type: string
        status:
          type: string
          enum: [draft, processing, live, failed, blocked]
        hlsMasterUrl:
          type: string
        durationMs:
          type: integer
        aspectRatio:
          type: number
        publishedAt:
          type: string
          format: date-time
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    FeedItem:
      type: object
      required: [videoId, userId, creatorUsername, hlsMasterUrl, publishedAt, likeCount, commentCount, viewCount]
      properties:
        videoId:
          type: string
          format: uuid
        userId:
          type: string
          format: uuid
        creatorUsername:
          type: string
        creatorAvatarUrl:
          type: string
        caption:
          type: string
        hlsMasterUrl:
          type: string
        durationMs:
          type: integer
        aspectRatio:
          type: number
        publishedAt:
          type: string
          format: date-time
        likeCount:
          type: integer
          format: int64
        commentCount:
          type: integer
          format: int64
        viewCount:
          type: integer
          format: int64
    FeedPageResponse:
      type: object
      required: [items, hasMore, limit, cached]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/FeedItem'
        nextCursor:
          type: string
        hasMore:
          type: boolean
        limit:
          type: integer
        cached:
          type: boolean
    SearchVideoResult:
      type: object
      required: [videoId, userId, creatorUsername, hlsMasterUrl, durationMs, aspectRatio, likeCount, commentCount, viewCount, publishedAt, score]
      properties:
        videoId:
          type: string
          format: uuid
        userId:
          type: string
          format: uuid
        creatorUsername:
          type: string
        creatorAvatarUrl:
          type: string
        caption:
          type: string
        hlsMasterUrl:
          type: string
        durationMs:
          type: integer
        aspectRatio:
          type: number
        likeCount:
          type: integer
          format: int64
        commentCount:
          type: integer
          format: int64
        viewCount:
          type: integer
          format: int64
        publishedAt:
          type: string
          format: date-time
        score:
          type: number
    SearchUserResult:
      type: object
      required: [userId, username, followersCount]
      properties:
        userId:
          type: string
          format: uuid
        username:
          type: string
        avatarUrl:
          type: string
        followersCount:
          type: integer
          format: int64
    SearchResponse:
      type: object
      required: [query, limit, videos, users]
      properties:
        query:
          type: string
        limit:
          type: integer
        videos:
          type: array
          items:
            $ref: '#/components/schemas/SearchVideoResult'
        users:
          type: array
          items:
            $ref: '#/components/schemas/SearchUserResult'
    ToggleLikeResponse:
      type: object
      required: [videoId, liked, likeCount, updatedAt]
      properties:
        videoId:
          type: string
          format: uuid
        liked:
          type: boolean
        likeCount:
          type: integer
          format: int64
        updatedAt:
          type: string
          format: date-time
    CreateCommentRequest:
      type: object
      required: [body]
      properties:
        body:
          type: string
          maxLength: 280
        parentCommentId:
          type: integer
          format: int64
          minimum: 1
    Comment:
      type: object
      required: [commentId, videoId, userId, username, body, createdAt, updatedAt]
      properties:
        commentId:
          type: integer
          format: int64
        videoId:
          type: string
          format: uuid
        userId:
          type: string
          format: uuid
        username:
          type: string
        userAvatarUrl:
          type: string
        body:
          type: string
        parentCommentId:
          type: integer
          format: int64
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    CreateCommentResponse:
      type: object
      required: [comment, commentCount]
      properties:
        comment:
          $ref: '#/components/schemas/Comment'
        commentCount:
          type: integer
          format: int64
    CommentPageResponse:
      type: object
      required: [items, hasMore, limit]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/Comment'
        nextCursor:
          type: string
        hasMore:
          type: boolean
        limit:
          type: integer
    ToggleFollowResponse:
      type: object
      required: [targetUserId, following, targetFollowerCount, viewerFollowingCount, updatedAt]
      properties:
        targetUserId:
          type: string
          format: uuid
        following:
          type: boolean
        targetFollowerCount:
          type: integer
          format: int64
        viewerFollowingCount:
          type: integer
          format: int64
        updatedAt:
          type: string
          format: date-time
    CreateReportRequest:
      type: object
      required: [videoId, reason]
      properties:
        videoId:
          type: string
          format: uuid
        reason:
          type: string
          enum: [spam, nudity, violence, hate, harassment, self_harm, copyright, misinformation, other]
        details:
          type: string
          maxLength: 500
    CreateReportResponse:
      type: object
      required: [reportId, videoId, reason, status, createdAt, videoBlocked, openReportCount, blockThreshold, cacheEpochBumped]
      properties:
        reportId:
          type: integer
          format: int64
        videoId:
          type: string
          format: uuid
        reason:
          type: string
        status:
          type: string
          enum: [open, triaged, resolved, rejected]
        createdAt:
          type: string
          format: date-time
        videoBlocked:
          type: boolean
        openReportCount:
          type: integer
          format: int64
        blockThreshold:
          type: integer
          format: int64
        cacheEpochBumped:
          type: boolean
    PeerSegmentRef:
      type: object
      required: [videoId, variant, segmentIndex]
      properties:
        videoId:
          type: string
          format: uuid
        variant:
          type: string
        segmentIndex:
          type: integer
          minimum: 0
    PeerHeartbeatRequest:
      type: object
      required: [peerId, networkType, batteryPct, activeUploads, activeDownloads, segments]
      properties:
        peerId:
          type: string
          format: uuid
        localityBucket:
          type: string
        asn:
          type: string
        city:
          type: string
        networkType:
          type: string
          enum: [wifi, cellular, unknown]
        batteryPct:
          type: integer
          minimum: 0
          maximum: 100
        activeUploads:
          type: integer
        activeDownloads:
          type: integer
        segments:
          type: array
          items:
            $ref: '#/components/schemas/PeerSegmentRef'
    PeerHeartbeatResponse:
      type: object
      required: [peerId, accepted, expiresAt, featureFlag, maxActivePeers, maxIdlePeers, backgroundUploadAllowed]
      properties:
        peerId:
          type: string
          format: uuid
        accepted:
          type: boolean
        expiresAt:
          type: string
          format: date-time
        featureFlag:
          type: string
        maxActivePeers:
          type: integer
        maxIdlePeers:
          type: integer
        backgroundUploadAllowed:
          type: boolean
    PeerCandidate:
      type: object
      required: [peerId, batteryPct, activeUploads, activeDownloads, ewmaRttMs, ewmaJitterMs, ewmaPacketLoss, suspiciousFailRate, score, segmentUrl, segmentSha256, token, tokenExpiresAt, lastSeenAt, localityPreference]
      properties:
        peerId:
          type: string
          format: uuid
        localityBucket:
          type: string
        asn:
          type: string
        city:
          type: string
        networkType:
          type: string
        batteryPct:
          type: integer
        activeUploads:
          type: integer
        activeDownloads:
          type: integer
        ewmaRttMs:
          type: number
        ewmaJitterMs:
          type: number
        ewmaPacketLoss:
          type: number
        suspiciousFailRate:
          type: number
        score:
          type: number
        segmentUrl:
          type: string
        segmentSha256:
          type: string
        token:
          type: string
        tokenExpiresAt:
          type: string
          format: date-time
        lastSeenAt:
          type: string
          format: date-time
        localityPreference:
          type: string
    PeerCandidatesResponse:
      type: object
      required: [videoId, variant, segmentIndex, candidates, generatedAt]
      properties:
        videoId:
          type: string
          format: uuid
        variant:
          type: string
        segmentIndex:
          type: integer
        candidates:
          type: array
          items:
            $ref: '#/components/schemas/PeerCandidate'
        generatedAt:
          type: string
          format: date-time
    PeerResultRequest:
      type: object
      required: [peerId, candidatePeerId, videoId, variant, segmentIndex, success, rttMs, jitterMs, packetLoss]
      properties:
        peerId:
          type: string
          format: uuid
        candidatePeerId:
          type: string
          format: uuid
        videoId:
          type: string
          format: uuid
        variant:
          type: string
        segmentIndex:
          type: integer
        success:
          type: boolean
        rttMs:
          type: number
        jitterMs:
          type: number
        packetLoss:
          type: number
        failureCode:
          type: string
        source:
          type: string
          enum: [peer, cdn]
    PeerResultResponse:
      type: object
      required: [status, candidatePeerId, recordedAt]
      properties:
        status:
          type: string
        candidatePeerId:
          type: string
          format: uuid
        recordedAt:
          type: string
          format: date-time
    NotificationItem:
      type: object
      required: [id, userId, entityType, kind, isRead, payload, createdAt]
      properties:
        id:
          type: integer
          format: int64
        userId:
          type: string
          format: uuid
        actorUserId:
          type: string
          format: uuid
        entityType:
          type: string
          example: video
        entityId:
          type: string
          format: uuid
        kind:
          type: string
          enum: [like, comment, follow, system]
        isRead:
          type: boolean
        payload:
          type: object
          additionalProperties: true
        createdAt:
          type: string
          format: date-time
    NotificationPageResponse:
      type: object
      required: [items, hasMore, limit, unreadCount]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/NotificationItem'
        nextCursor:
          type: string
        hasMore:
          type: boolean
        limit:
          type: integer
        unreadCount:
          type: integer
          format: int64
    RealtimeNotificationMessage:
      type: object
      required: [type, userId, unreadCount, timestamp]
      properties:
        type:
          type: string
          enum: [unread_count]
        userId:
          type: string
          format: uuid
        unreadCount:
          type: integer
          format: int64
        notificationId:
          type: integer
          format: int64
        timestamp:
          type: string
          format: date-time
