openapi: 3.0.3
info:
  title: GameTime Server API
  version: 0.1.0
  description: Headless API for GameTime authentication, incremental sync and targeted sharing.
servers:
  - url: http://localhost:8080
    description: Local Docker or development server
tags:
  - name: Health
  - name: Auth
  - name: Sync
  - name: Shares
paths:
  /health:
    get:
      tags: [Health]
      summary: Healthcheck
      responses:
        '200':
          description: Server is reachable.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthResponse'

  /auth/register:
    post:
      tags: [Auth]
      summary: Register a user account
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RegisterRequest'
      responses:
        '201':
          description: User created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RegisterResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '409':
          description: Email already taken.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /auth/login:
    post:
      tags: [Auth]
      summary: Login and create an API token session
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LoginRequest'
      responses:
        '200':
          description: Login accepted. The clear token is returned once.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LoginResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /auth/logout:
    post:
      tags: [Auth]
      summary: Revoke the current API token session
      security:
        - bearerAuth: []
      responses:
        '204':
          description: Session revoked.
        '401':
          $ref: '#/components/responses/Unauthorized'

  /sync/push:
    post:
      tags: [Sync]
      summary: Push client-side resource mutations
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SyncPushRequest'
      responses:
        '200':
          description: Batch processed. Item-level validation errors are returned in results.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SyncPushResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /sync/pull:
    get:
      tags: [Sync]
      summary: Pull resources updated after a server cursor
      security:
        - bearerAuth: []
      parameters:
        - name: since
          in: query
          required: false
          schema:
            type: string
            format: date-time
          description: ISO8601 UTC server cursor. If omitted, returns all resources.
      responses:
        '200':
          description: Resources for the authenticated user.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SyncPullResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /sync/exchange:
    post:
      tags: [Sync]
      summary: Push then pull in one request
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SyncExchangeRequest'
      responses:
        '200':
          description: Pull response after applying the push batch.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SyncExchangeResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /shares:
    post:
      tags: [Shares]
      summary: Create a targeted share
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateShareRequest'
      responses:
        '201':
          description: Share created. Unknown recipient emails are reported but do not fail the request.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateShareResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /shares/inbox:
    get:
      tags: [Shares]
      summary: List shares received by the authenticated user
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Inbox sorted from newest to oldest.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ShareInboxResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /shares/{id}/accept:
    post:
      tags: [Shares]
      summary: Accept a received share
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/ShareId'
      responses:
        '200':
          description: Share accepted and copied into synced resources.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AcceptShareResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'

  /shares/{id}/decline:
    post:
      tags: [Shares]
      summary: Decline a received share
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/ShareId'
      responses:
        '204':
          description: Share declined.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'

  /shares/{id}/revoke:
    post:
      tags: [Shares]
      summary: Revoke a share sent by the authenticated user
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/ShareId'
      responses:
        '204':
          description: Share revoked.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: opaque

  parameters:
    ShareId:
      name: id
      in: path
      required: true
      schema:
        type: string
      description: Share identifier.

  responses:
    BadRequest:
      description: Invalid JSON body, invalid parameter, or validation failure.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Unauthorized:
      description: Missing, malformed, expired, revoked or unknown bearer token.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    NotFound:
      description: Resource not found or not accessible by the authenticated user.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Conflict:
      description: Current resource state rejects the requested transition.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'

  schemas:
    ErrorResponse:
      type: object
      required: [error]
      properties:
        error:
          type: string

    HealthResponse:
      type: object
      required: [status]
      properties:
        status:
          type: string
          enum: [ok]

    RegisterRequest:
      type: object
      required: [email, password]
      properties:
        email:
          type: string
          format: email
        password:
          type: string
          minLength: 8
        displayName:
          type: string
          nullable: true

    RegisterResponse:
      type: object
      required: [userId, email]
      properties:
        userId:
          type: string
        email:
          type: string
          format: email

    LoginRequest:
      type: object
      required: [email, password]
      properties:
        email:
          type: string
          format: email
        password:
          type: string
        deviceLabel:
          type: string
          nullable: true

    LoginResponse:
      type: object
      required: [token, expiresAt]
      properties:
        token:
          type: string
        expiresAt:
          type: string
          format: date-time

    ResourceType:
      type: string
      enum: [exercise, program, workoutTemplate, workoutHistory, mediaAsset]

    ShareResourceType:
      type: string
      enum: [program, workoutTemplate]

    ShareRecipientStatus:
      type: string
      enum: [pending, accepted, declined, revoked]

    JsonObject:
      type: object
      additionalProperties: true

    SyncPushRequest:
      type: object
      required: [items]
      properties:
        deviceId:
          type: string
          nullable: true
          description: Required by the application use case for each valid item.
        items:
          type: array
          items:
            $ref: '#/components/schemas/SyncPushItem'

    SyncPushItem:
      type: object
      required: [resourceType, clientId, schemaVersion, clientUpdatedAt, payload]
      properties:
        resourceType:
          $ref: '#/components/schemas/ResourceType'
        clientId:
          type: string
        schemaVersion:
          type: integer
          minimum: 1
        clientUpdatedAt:
          type: string
          format: date-time
        deletedAt:
          type: string
          format: date-time
          nullable: true
        payload:
          $ref: '#/components/schemas/JsonObject'

    SyncPushResponse:
      type: object
      required: [serverCursor, results]
      properties:
        serverCursor:
          type: string
          format: date-time
        results:
          type: array
          items:
            $ref: '#/components/schemas/SyncPushResultItem'

    SyncPushResultItem:
      type: object
      required: [status]
      properties:
        resourceType:
          type: string
          nullable: true
        clientId:
          type: string
          nullable: true
        serverId:
          type: string
          nullable: true
        status:
          type: string
          enum: [accepted, ignoredOlder, error]
        serverUpdatedAt:
          type: string
          format: date-time
          nullable: true
        message:
          type: string

    SyncPullResponse:
      type: object
      required: [serverCursor, items]
      properties:
        serverCursor:
          type: string
          format: date-time
        items:
          type: array
          items:
            $ref: '#/components/schemas/SyncedResourceItem'

    SyncExchangeRequest:
      allOf:
        - $ref: '#/components/schemas/SyncPushRequest'
        - type: object
          properties:
            since:
              type: string
              format: date-time
              nullable: true

    SyncExchangeResponse:
      type: object
      required: [serverCursor, pushResults, items]
      properties:
        serverCursor:
          type: string
          format: date-time
        pushResults:
          type: array
          items:
            $ref: '#/components/schemas/SyncPushResultItem'
        items:
          type: array
          items:
            $ref: '#/components/schemas/SyncedResourceItem'

    SyncedResourceItem:
      type: object
      required:
        - resourceType
        - clientId
        - serverId
        - schemaVersion
        - clientUpdatedAt
        - serverUpdatedAt
        - payload
      properties:
        resourceType:
          $ref: '#/components/schemas/ResourceType'
        clientId:
          type: string
        serverId:
          type: string
        schemaVersion:
          type: integer
          minimum: 1
        clientUpdatedAt:
          type: string
          format: date-time
        serverUpdatedAt:
          type: string
          format: date-time
        deletedAt:
          type: string
          format: date-time
          nullable: true
        payload:
          $ref: '#/components/schemas/JsonObject'

    CreateShareRequest:
      type: object
      required: [resourceType, payload, recipientEmails]
      properties:
        resourceType:
          $ref: '#/components/schemas/ShareResourceType'
        payload:
          $ref: '#/components/schemas/JsonObject'
        recipientEmails:
          type: array
          minItems: 1
          items:
            type: string
            format: email

    CreateShareResponse:
      type: object
      required: [shareId, recipientUserIds, unresolvedEmails]
      properties:
        shareId:
          type: string
        recipientUserIds:
          type: array
          items:
            type: string
        unresolvedEmails:
          type: array
          items:
            type: string
            format: email

    ShareInboxResponse:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/ShareInboxItem'

    ShareInboxItem:
      type: object
      required:
        - shareId
        - senderUserId
        - resourceType
        - payload
        - status
        - createdAt
      properties:
        shareId:
          type: string
        senderUserId:
          type: string
        resourceType:
          $ref: '#/components/schemas/ShareResourceType'
        payload:
          $ref: '#/components/schemas/JsonObject'
        status:
          $ref: '#/components/schemas/ShareRecipientStatus'
        createdAt:
          type: string
          format: date-time
        respondedAt:
          type: string
          format: date-time
          nullable: true

    AcceptShareResponse:
      type: object
      required: [createdResource]
      properties:
        createdResource:
          $ref: '#/components/schemas/SyncedResourceItem'
