openapi: 3.0.3
info:
  title: DojoOS API
  version: 1.0.0
  description: |
    REST API for DojoOS -- the AI-native developer platform by Dojo Coding.

    ## Authentication
    All endpoints require a Bearer token (Supabase JWT) unless noted otherwise.
    The token is obtained via Supabase Auth (email/password, OAuth, or magic link).

    ## Base URL
    All Edge Function endpoints live under `https://{project-ref}.supabase.co/functions/v1`.

    ## Rate Limiting
    Several endpoints enforce per-user rate limits. When exceeded, the response
    includes a `Retry-After` header (seconds) and returns HTTP 429.

    ## CORS
    All endpoints support CORS preflight (`OPTIONS`) and return appropriate headers.
  contact:
    name: Dojo Coding
    url: https://dojocoding.io
    email: daniel@dojocoding.io

servers:
  - url: https://api.dojocoding.io/v1
    description: Production (via API Gateway — clean URLs, planned)
  - url: https://{project_ref}.supabase.co/functions/v1
    description: Direct Supabase Edge Functions
    variables:
      project_ref:
        default: pphagffyuibcfulgrpjb
        description: Supabase project reference (varies per environment)

security:
  - bearerAuth: []

tags:
  - name: Agent
    description: AI assistant chat proxy and text-to-speech
  - name: Courses
    description: Course enrollment, progress tracking, and certificates
  - name: Projects
    description: B2B project and training lead intake
  - name: Billing
    description: Stripe checkout and customer portal
  - name: Users
    description: User roles, account management, and promo codes
  - name: Admin
    description: Administrative endpoints (require admin role)
  - name: Media
    description: Video playback tokens and certificate generation
  - name: Email
    description: Transactional email sending

paths:
  # ---------------------------------------------------------------------------
  # Agent
  # ---------------------------------------------------------------------------
  /agent-chat-proxy:
    post:
      operationId: agentChatProxy
      summary: Send a message to the Doji AI agent or manage sessions
      description: |
        Authenticated proxy to the agent gateway. The `path` query parameter
        selects the upstream operation:

        | path | Method | Description |
        |------|--------|-------------|
        | `/api/web-chat` | POST | Stream a chat response (SSE) |
        | `/api/sessions` | GET | List chat sessions |
        | `/api/sessions` | POST | Create a chat session |
        | `/api/sessions/{id}` | DELETE | Delete a session |
        | `/api/sessions/{id}/messages` | GET | Get session messages |

        For `/api/web-chat`, the response is `text/event-stream` in
        OpenAI-compatible format.

        **Rate limit:** 12 requests per minute per user (chat only).
      tags:
        - Agent
      parameters:
        - name: path
          in: query
          required: true
          description: |
            Upstream path to proxy. Allowed values:
            `/api/web-chat`, `/api/chat`, `/api/sessions`,
            `/api/sessions/{id}`, `/api/sessions/{id}/messages`.
          schema:
            type: string
            example: /api/web-chat
      requestBody:
        required: false
        description: |
          Required for `/api/web-chat`. Ignored for GET/DELETE operations.
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AgentChatRequest'
      responses:
        '200':
          description: |
            For `/api/web-chat`: SSE stream (`text/event-stream`).
            For session CRUD: JSON response.
          content:
            text/event-stream:
              schema:
                type: string
                description: OpenAI-compatible SSE stream
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/AgentSession'
                  - type: array
                    items:
                      $ref: '#/components/schemas/AgentSession'
                  - type: array
                    items:
                      $ref: '#/components/schemas/AgentMessage'
        '204':
          description: Session deleted successfully (DELETE operations)
        '400':
          description: Invalid or missing proxy path, or invalid request body
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Missing or invalid authentication token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '405':
          description: HTTP method not allowed for the given path
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until the rate limit window resets
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RateLimitError'
        '503':
          description: Agent web chat disabled by admin or temporarily unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '504':
          description: Agent gateway request timed out (30s for chat, 15s for CRUD)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  # ---------------------------------------------------------------------------
  # Text-to-Speech
  # ---------------------------------------------------------------------------
  /text-to-speech:
    post:
      operationId: textToSpeech
      summary: Convert text to speech audio
      description: |
        Synthesizes text into MP3 audio using Google Cloud Text-to-Speech.
        Available to **Pro and VIP subscribers only**.

        **Rate limit:** 10 requests per hour per user.
      tags:
        - Agent
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TextToSpeechRequest'
      responses:
        '200':
          description: MP3 audio bytes
          headers:
            Content-Length:
              schema:
                type: integer
            Cache-Control:
              schema:
                type: string
                example: "private, max-age=3600"
          content:
            audio/mpeg:
              schema:
                type: string
                format: binary
        '400':
          description: Missing or invalid text, or text exceeds 5,000 characters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Missing or invalid authentication token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: User is not a Pro or VIP subscriber
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '405':
          description: Method not allowed (only POST)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: TTS rate limit exceeded (10/hour)
          headers:
            Retry-After:
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RateLimitError'
        '502':
          description: Google TTS API failure or empty audio response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  # ---------------------------------------------------------------------------
  # B2B Lead
  # ---------------------------------------------------------------------------
  /submit-b2b-lead:
    post:
      operationId: submitB2bLead
      summary: Submit a B2B project or training lead
      description: |
        Accepts B2B contact requests for software development, team training,
        or both. Stores the lead in the database, sends a confirmation email
        to the contact, and notifies the sales team.

        Supports `leadType: "both"` to insert two DB rows (hire + train) with
        a single combined email. Includes 5-minute time-based deduplication.
      tags:
        - Projects
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/B2BLeadRequest'
      responses:
        '200':
          description: Lead created (or deduplicated)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/B2BLeadResponse'
        '400':
          description: |
            Missing required fields, invalid lead type, invalid contact method,
            or invalid email format
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Authentication required or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: userId in body does not match authenticated user
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '405':
          description: Method not allowed (only POST)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Database insert failure or server configuration error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          description: Temporary deduplication check failure
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  # ---------------------------------------------------------------------------
  # Generate Certificate
  # ---------------------------------------------------------------------------
  /generate-certificate:
    post:
      operationId: generateCertificate
      summary: Generate a course completion certificate
      description: |
        Generates a PDF certificate and an OG image (1200x630 PNG) for a
        completed course. The certificate is stored in Supabase Storage and
        recorded in the `certificates` table.

        If a certificate already exists for the user/course pair, it returns
        the existing one.
      tags:
        - Media
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - courseId
              properties:
                courseId:
                  type: string
                  format: uuid
                  description: ID of the completed course
      responses:
        '200':
          description: Certificate generated or already exists
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CertificateResponse'
        '400':
          description: Missing courseId
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Not authenticated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: User has not completed the course (progress < 100%)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: PDF generation or storage failure
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  # ---------------------------------------------------------------------------
  # User Courses
  # ---------------------------------------------------------------------------
  /user-courses:
    get:
      operationId: getUserCourses
      summary: Get the authenticated user's enrolled courses
      description: |
        Returns the user's in-progress courses with progress data, ordered by
        progress (highest first) and last accessed date. Only courses where
        `completed = false` are returned.
      tags:
        - Courses
      parameters:
        - name: limit
          in: query
          required: false
          description: Number of courses to return (1-20, default 5)
          schema:
            type: integer
            minimum: 1
            maximum: 20
            default: 5
      responses:
        '200':
          description: List of enrolled courses with progress
          headers:
            Cache-Control:
              schema:
                type: string
                example: "private, max-age=60"
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserCoursesResponse'
        '401':
          description: Missing or invalid authentication token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '405':
          description: Method not allowed (only GET or POST)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Database query failure
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  # ---------------------------------------------------------------------------
  # Track Progress
  # ---------------------------------------------------------------------------
  /track-progress:
    post:
      operationId: trackProgress
      summary: Update course module progress
      description: |
        Tracks a user's progress in a specific course module. Creates or
        updates the progress record in the `course_progress` table. Overall
        course progress is recalculated as the average of all module progress
        values. The course is marked complete when overall progress reaches 100%.
      tags:
        - Courses
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TrackProgressRequest'
      responses:
        '200':
          description: Progress tracked successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TrackProgressResponse'
        '400':
          description: Missing required fields (courseId, moduleId, or progress)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Missing or invalid authentication token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Database operation failure
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  # ---------------------------------------------------------------------------
  # Role Switch
  # ---------------------------------------------------------------------------
  /role-switch:
    post:
      operationId: roleSwitch
      summary: Switch the user's active role
      description: |
        Switches the authenticated user's active role between `learning`,
        `working`, and `founding`. Records the transition, tracks analytics,
        and sends notifications.
      tags:
        - Users
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RoleSwitchRequest'
      responses:
        '200':
          description: Role switched successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RoleSwitchResponse'
        '401':
          description: Missing or invalid authentication token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '405':
          description: Method not allowed (only POST)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: |
            Role switch failed. Response includes `success: false` with
            an error message.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RoleSwitchErrorResponse'

  # ---------------------------------------------------------------------------
  # Create Checkout Session
  # ---------------------------------------------------------------------------
  /create-checkout-session:
    post:
      operationId: createCheckoutSession
      summary: Create a Stripe checkout session
      description: |
        Creates a Stripe Checkout session for subscription (Pro/VIP monthly or
        annual) or one-time purchase (Blueprint). Validates the user, resolves
        the Stripe price ID from environment, and returns an embedded checkout
        session.

        Pro tier includes a 7-day free trial. VIP has no trial.

        **Rate limit:** 5 requests per minute per IP.
      tags:
        - Billing
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateCheckoutSessionRequest'
      responses:
        '200':
          description: Checkout session created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateCheckoutSessionResponse'
        '400':
          description: |
            Invalid request data, user already has active subscription,
            or user already owns the product
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Missing or invalid authentication
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '405':
          description: Method not allowed (only POST)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded (5 per minute)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Stripe configuration missing or checkout creation failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  # ---------------------------------------------------------------------------
  # Create Customer Portal Session
  # ---------------------------------------------------------------------------
  /create-customer-portal-session:
    post:
      operationId: createCustomerPortalSession
      summary: Create a Stripe Customer Portal session
      description: |
        Creates a Stripe Customer Portal session that allows the user to
        manage their subscription: update payment methods, cancel, view
        invoices, change plans.
      tags:
        - Billing
      requestBody:
        required: false
        description: No body required. The user is identified from the JWT.
        content:
          application/json:
            schema:
              type: object
      responses:
        '200':
          description: Portal session created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CustomerPortalSessionResponse'
        '401':
          description: Missing or invalid authentication
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: No Stripe customer found for this user
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Failed to create portal session
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  # ---------------------------------------------------------------------------
  # Get Signed Playback Token
  # ---------------------------------------------------------------------------
  /get-signed-playback-token:
    post:
      operationId: getSignedPlaybackToken
      summary: Get a signed Mux video playback token
      description: |
        Generates a signed JWT token for secure Mux video playback. Validates
        that the user is authenticated, enrolled in the course containing the
        video, and has a sufficient subscription tier.

        Token types: `video` (default), `thumbnail`, `storyboard`.
        Token lifetime: up to 2 hours (7200 seconds).
      tags:
        - Media
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SignedPlaybackTokenRequest'
      responses:
        '200':
          description: Signed playback token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SignedPlaybackTokenResponse'
        '400':
          description: Missing playbackId
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessError'
        '401':
          description: Missing or invalid authentication
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessError'
        '403':
          description: |
            Course not published, enrollment required, or insufficient
            access level
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessError'
        '404':
          description: Video or course not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessError'
        '405':
          description: Method not allowed (only POST)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessError'
        '422':
          description: Video not associated with a course
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessError'
        '500':
          description: Server configuration missing or signing error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessError'

  # ---------------------------------------------------------------------------
  # Send Email
  # ---------------------------------------------------------------------------
  /send-email:
    post:
      operationId: sendEmail
      summary: Send a transactional email
      description: |
        Sends an email via Resend. Supports two modes:

        - **Template-based**: provide `template_slug` and `template_data` to
          render a DB-stored template.
        - **Pre-rendered**: provide `subject` and `html` directly.

        Non-admin users can only send to their own email address using
        approved templates. Admins can send to any recipient.

        **Rate limits:**
        - Regular users: 50 emails per hour
        - Admins: 500 emails per hour
      tags:
        - Email
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SendEmailRequest'
      responses:
        '200':
          description: Email sent successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SendEmailResponse'
        '400':
          description: |
            Missing recipients, invalid email address, subject too long
            (>200 chars), or body too large (>250KB)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessError'
        '401':
          description: Missing or invalid authentication
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessError'
        '403':
          description: |
            Non-admin attempting to send to external recipients,
            use CC/BCC, or use a disallowed template
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessError'
        '404':
          description: Template slug not found or inactive
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessError'
        '429':
          description: Email rate limit exceeded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessError'
        '500':
          description: Email service not configured or send failure
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessError'
        '503':
          description: Rate limit service unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessError'

  # ---------------------------------------------------------------------------
  # Delete Account
  # ---------------------------------------------------------------------------
  /delete-account:
    post:
      operationId: deleteAccount
      summary: Permanently delete user account
      description: |
        Permanently deletes the authenticated user's account and all associated
        data. This is an **irreversible** action that requires password
        re-authentication and explicit confirmation.

        An audit log entry is created before deletion. Profile data is marked
        with `deleted_at` and the auth user is removed.

        **Rate limit:** 3 requests per minute per IP.
      tags:
        - Users
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DeleteAccountRequest'
      responses:
        '200':
          description: Account deleted successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeleteAccountResponse'
        '400':
          description: Missing confirmation or password
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeleteAccountResponse'
        '401':
          description: Invalid password or authentication token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeleteAccountResponse'
        '429':
          description: Rate limit exceeded (3 per minute)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Deletion failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeleteAccountResponse'

  # ---------------------------------------------------------------------------
  # Redeem Promo Code
  # ---------------------------------------------------------------------------
  /redeem-promo-code:
    post:
      operationId: redeemPromoCode
      summary: Redeem a promotional code
      description: |
        Redeems a promotional code for the authenticated user. Supports
        `free_access`, `percentage_discount`, `fixed_discount`, and
        `trial_extension` code types.

        Uses atomic usage slot claiming via the `increment_promo_code_usage`
        RPC to prevent race conditions. Each user can redeem a code only once.
      tags:
        - Users
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RedeemPromoCodeRequest'
      responses:
        '200':
          description: Code redeemed successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RedeemPromoCodeResponse'
        '400':
          description: |
            Missing code, invalid code, code already redeemed by this user,
            or code has reached its usage limit
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RedeemPromoCodeResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RedeemPromoCodeResponse'
        '500':
          description: Failed to process or update profile
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RedeemPromoCodeResponse'

  # ---------------------------------------------------------------------------
  # Get Activity Analytics
  # ---------------------------------------------------------------------------
  /get-activity-analytics:
    post:
      operationId: getActivityAnalytics
      summary: Get user activity analytics (admin)
      description: |
        Fetches user activity analytics for the admin Activity dashboard.
        Requires **admin role** in `user_roles` table.

        Supports multiple metric groups fetched in parallel:
        - `growth`: DAU, WAU, MAU, registrations, retention cohorts, churn risk
        - `engagement`: lessons, completions, quiz attempts, time spent
        - `userList`: paginated user activity list with search/filter
        - `userDetail`: single user detail with heatmap and timeline
        - `scoreAnalytics`: rank distribution, pillar breakdown, top scorers, KPIs

        Accepts parameters via POST body (preferred) or GET query string.
      tags:
        - Admin
      security:
        - bearerAuth: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ActivityAnalyticsRequest'
      responses:
        '200':
          description: Analytics data
          headers:
            Cache-Control:
              schema:
                type: string
                example: "private, max-age=300"
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ActivityAnalyticsResponse'
        '400':
          description: Missing startDate or endDate
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessError'
        '401':
          description: Missing or invalid authentication token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessError'
        '403':
          description: Admin access required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessError'
        '500':
          description: Server configuration error or unexpected failure
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessError'

  # ---------------------------------------------------------------------------
  # Get Admin Users
  # ---------------------------------------------------------------------------
  /get-admin-users:
    get:
      operationId: getAdminUsers
      summary: List all users with enriched data (admin)
      description: |
        Fetches all users with enriched profile and auth data for the admin
        dashboard. Uses `admin_users_view` via the `get_admin_users_data` RPC
        and batch-fetches auth.users for email and last sign-in data.

        Requires **admin role** in `user_roles` table.
      tags:
        - Admin
      security:
        - bearerAuth: []
      responses:
        '200':
          description: User list with requesting user info
          headers:
            Cache-Control:
              schema:
                type: string
                example: "private, max-age=60"
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminUsersResponse'
        '401':
          description: Missing or invalid authentication token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessError'
        '403':
          description: Admin access required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessError'
        '500':
          description: Failed to fetch users or verify permissions
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessError'

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: Supabase JWT obtained via Supabase Auth

  schemas:
    # -----------------------------------------------------------------------
    # Common
    # -----------------------------------------------------------------------
    Error:
      type: object
      required:
        - error
      properties:
        error:
          type: string
          description: Human-readable error message

    SuccessError:
      type: object
      required:
        - success
      properties:
        success:
          type: boolean
        error:
          type: object
          properties:
            message:
              type: string

    RateLimitError:
      type: object
      properties:
        error:
          type: string
          example: Rate limit exceeded
        window:
          type: string
          description: Rate limit window (e.g. "minute", "hour")
        retryAfter:
          type: integer
          description: Seconds until the window resets

    # -----------------------------------------------------------------------
    # Agent
    # -----------------------------------------------------------------------
    AgentChatRequest:
      type: object
      required:
        - messages
      properties:
        model:
          type: string
          description: Agent model identifier
          default: openclaw
          example: openclaw
        messages:
          type: array
          items:
            $ref: '#/components/schemas/ChatMessage'
          description: Conversation messages
        stream:
          type: boolean
          description: Whether to stream the response via SSE
          default: true
        metadata:
          type: object
          description: Additional context for the agent
          properties:
            sessionKey:
              type: string
              description: Session ID for conversation continuity
            userLocale:
              type: string
              description: "User's locale (e.g. \"es\", \"en\")"
              example: en
            persona:
              type: string
              description: Agent persona to use
            courseContext:
              type: object
              description: Current course context for learning assistance
            graphContext:
              type: object
              description: Dojo Graph context for knowledge queries

    ChatMessage:
      type: object
      required:
        - role
        - content
      properties:
        role:
          type: string
          enum:
            - user
            - assistant
            - system
        content:
          type: string

    AgentSession:
      type: object
      properties:
        id:
          type: string
        user_id:
          type: string
          format: uuid
        agent_id:
          type: string
        primary_channel:
          type: string
          example: web
        channel_metadata:
          type: object
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        metadata:
          type: object

    AgentMessage:
      type: object
      properties:
        id:
          type: string
        session_id:
          type: string
        role:
          type: string
          enum:
            - user
            - assistant
        content:
          type: string
        metadata:
          type: object
        channel:
          type: string
        channel_metadata:
          type: object
        created_at:
          type: string
          format: date-time

    # -----------------------------------------------------------------------
    # Text-to-Speech
    # -----------------------------------------------------------------------
    TextToSpeechRequest:
      type: object
      required:
        - text
      properties:
        text:
          type: string
          description: Text to synthesize (max 5,000 characters)
          maxLength: 5000
        locale:
          type: string
          description: "\"en\" (default) or \"es\" for Spanish"
          enum:
            - en
            - es
          default: en

    # -----------------------------------------------------------------------
    # B2B Lead
    # -----------------------------------------------------------------------
    B2BLeadRequest:
      type: object
      required:
        - leadType
        - companyName
        - contactName
        - contactMethod
      properties:
        leadType:
          type: string
          enum:
            - hire
            - train
            - both
          description: Type of service requested
        companyName:
          type: string
          description: Company or organization name
        contactName:
          type: string
          description: Primary contact person
        contactMethod:
          type: string
          enum:
            - email
            - phone
          description: Preferred contact method
        contactEmail:
          type: string
          format: email
          description: Required when contactMethod is "email"
        contactPhone:
          type: string
          description: Required when contactMethod is "phone"
        roleInCompany:
          type: string
          description: "Contact person's role"
        userId:
          type: string
          format: uuid
          description: Optional; must match authenticated user
        formData:
          type: object
          additionalProperties: true
          description: Additional form fields as key-value pairs
        trainContactName:
          type: string
          description: Separate contact for training (when leadType is "both")
        trainContactEmail:
          type: string
          format: email
          description: Training contact email
        trainContactPhone:
          type: string
          description: Training contact phone
        trainContactMethod:
          type: string
          enum:
            - email
            - phone
          description: Contact method for training contact
        trainCompanyName:
          type: string
          description: Company name for training (if different)

    B2BLeadResponse:
      type: object
      properties:
        success:
          type: boolean
        leadId:
          type: string
          format: uuid
          description: Primary lead record ID
        notificationSent:
          type: boolean
          description: Whether the internal sales notification was sent
        userEmailSent:
          type: boolean
          description: Whether the user confirmation email was sent
        deduplicated:
          type: boolean
          description: Whether the request was deduplicated (same user/type within 5 min)

    # -----------------------------------------------------------------------
    # Certificate
    # -----------------------------------------------------------------------
    CertificateResponse:
      type: object
      properties:
        certificateUrl:
          type: string
          format: uri
          description: Public URL to the generated PDF
        certificateId:
          type: string
          description: Certificate record ID

    # -----------------------------------------------------------------------
    # User Courses
    # -----------------------------------------------------------------------
    UserCoursesResponse:
      type: object
      properties:
        courses:
          type: array
          items:
            $ref: '#/components/schemas/UserCourseSummary'
        fetchedAt:
          type: string
          format: date-time
          description: Timestamp of the response

    UserCourseSummary:
      type: object
      properties:
        id:
          type: string
          format: uuid
        slug:
          type: string
        title:
          type: string
        instructor:
          type: string
          nullable: true
        thumbnailUrl:
          type: string
          format: uri
          nullable: true
        progressPercentage:
          type: integer
          minimum: 0
          maximum: 100
        lastAccessed:
          type: string
          format: date-time
          nullable: true
        continueUrl:
          type: string
          description: "Frontend path to continue the course (e.g. /app/courses/{slug})"

    # -----------------------------------------------------------------------
    # Track Progress
    # -----------------------------------------------------------------------
    TrackProgressRequest:
      type: object
      required:
        - courseId
        - moduleId
        - progress
        - completed
      properties:
        courseId:
          type: string
          format: uuid
          description: Course ID
        moduleId:
          type: string
          format: uuid
          description: Module ID
        progress:
          type: number
          minimum: 0
          maximum: 100
          description: Module progress percentage
        completed:
          type: boolean
          description: Whether the module is completed

    TrackProgressResponse:
      type: object
      properties:
        success:
          type: boolean
        overallProgress:
          type: number
          description: Recalculated average progress across all modules in the course

    # -----------------------------------------------------------------------
    # Role Switch
    # -----------------------------------------------------------------------
    RoleSwitchRequest:
      type: object
      required:
        - toRole
      properties:
        toRole:
          type: string
          enum:
            - learning
            - working
            - founding
          description: Target role to switch to
        reason:
          type: string
          maxLength: 500
          description: Optional reason for switching roles
        metadata:
          type: object
          additionalProperties: true
          description: Optional metadata to attach to the transition
        preserveCurrentData:
          type: boolean
          default: false
          description: Whether to preserve current role data

    RoleSwitchResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        transitionId:
          type: string
          description: Unique ID of the role transition
        fromRole:
          type: string
          nullable: true
          enum:
            - learning
            - working
            - founding
          description: Previous role (null for first-time users)
        toRole:
          type: string
          enum:
            - learning
            - working
            - founding
        message:
          type: string
          example: Role switched successfully
        duration:
          type: integer
          description: Time taken for the switch in milliseconds
        analytics:
          type: object
          properties:
            tracked:
              type: boolean
            eventId:
              type: string

    RoleSwitchErrorResponse:
      type: object
      properties:
        success:
          type: boolean
          example: false
        error:
          type: string
        message:
          type: string
          example: Failed to switch role

    # -----------------------------------------------------------------------
    # Checkout Session
    # -----------------------------------------------------------------------
    CreateCheckoutSessionRequest:
      type: object
      description: |
        At least one of `planType`, `priceId`, or `planTier: "blueprint"` is required.
      properties:
        planType:
          type: string
          enum:
            - monthly
            - annual
          description: Subscription billing period
        planTier:
          type: string
          enum:
            - pro
            - vip
            - blueprint
          description: Subscription tier or one-time product
        priceId:
          type: string
          pattern: "^price_[a-zA-Z0-9]+$"
          maxLength: 100
          description: Explicit Stripe price ID (overrides planType/planTier resolution)
        success_url:
          type: string
          format: uri
          maxLength: 500
          description: Redirect URL after successful checkout
        successUrl:
          type: string
          format: uri
          maxLength: 500
          description: Alias for success_url
        cancel_url:
          type: string
          format: uri
          maxLength: 500
          description: Redirect URL on checkout cancellation
        cancelUrl:
          type: string
          format: uri
          maxLength: 500
          description: Alias for cancel_url

    CreateCheckoutSessionResponse:
      type: object
      properties:
        sessionId:
          type: string
          description: Stripe Checkout Session ID
        clientSecret:
          type: string
          description: Client secret for embedded checkout
        url:
          type: string
          format: uri
          nullable: true
          description: Hosted checkout URL (if not using embedded mode)

    # -----------------------------------------------------------------------
    # Customer Portal
    # -----------------------------------------------------------------------
    CustomerPortalSessionResponse:
      type: object
      properties:
        url:
          type: string
          format: uri
          description: Stripe Customer Portal URL

    # -----------------------------------------------------------------------
    # Signed Playback Token
    # -----------------------------------------------------------------------
    SignedPlaybackTokenRequest:
      type: object
      required:
        - playbackId
      properties:
        playbackId:
          type: string
          description: Mux playback ID
        videoAssetId:
          type: string
          format: uuid
          description: Optional video asset ID for access control validation
        expirationSeconds:
          type: integer
          minimum: 1
          maximum: 7200
          default: 7200
          description: Token lifetime in seconds (max 2 hours)
        type:
          type: string
          enum:
            - video
            - thumbnail
            - storyboard
          default: video
          description: Type of token to generate

    SignedPlaybackTokenResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        data:
          type: object
          properties:
            token:
              type: string
              description: Signed JWT token for Mux playback
            playbackId:
              type: string
              description: The Mux playback ID the token was signed for
            expiresAt:
              type: integer
              description: Unix timestamp when the token expires
            expiresAtISO:
              type: string
              format: date-time
              description: ISO 8601 expiration timestamp

    # -----------------------------------------------------------------------
    # Send Email
    # -----------------------------------------------------------------------
    SendEmailRequest:
      type: object
      required:
        - to
      properties:
        to:
          type: array
          items:
            type: string
            format: email
          minItems: 1
          description: Recipient email addresses
        subject:
          type: string
          maxLength: 200
          description: Email subject (required if not using template_slug)
        html:
          type: string
          maxLength: 250000
          description: HTML email body (required if not using template_slug)
        template_slug:
          type: string
          description: DB template slug (alternative to subject+html)
        template_data:
          type: object
          additionalProperties:
            type: string
          description: Template variable substitutions
        cc:
          type: array
          items:
            type: string
            format: email
          description: CC recipients (admin only)
        bcc:
          type: array
          items:
            type: string
            format: email
          description: BCC recipients (admin only)
        metadata:
          type: object
          additionalProperties: true
          description: Metadata for logging and tagging

    SendEmailResponse:
      type: object
      properties:
        success:
          type: boolean
        id:
          type: string
          description: Resend email ID

    # -----------------------------------------------------------------------
    # Delete Account
    # -----------------------------------------------------------------------
    DeleteAccountRequest:
      type: object
      required:
        - password
        - confirmDeletion
      properties:
        password:
          type: string
          description: "User's current password for re-authentication"
        confirmDeletion:
          type: boolean
          description: Must be true to confirm the irreversible deletion

    DeleteAccountResponse:
      type: object
      properties:
        success:
          type: boolean
        message:
          type: string
          description: Present on success
        error:
          type: string
          description: Present on failure

    # -----------------------------------------------------------------------
    # Redeem Promo Code
    # -----------------------------------------------------------------------
    RedeemPromoCodeRequest:
      type: object
      required:
        - code_id
        - code
      properties:
        code_id:
          type: string
          format: uuid
          description: Promo code record ID
        code:
          type: string
          description: The promotional code string

    RedeemPromoCodeResponse:
      type: object
      properties:
        success:
          type: boolean
        code_type:
          type: string
          description: |
            Type of promo code: "free_access", "percentage_discount",
            "fixed_discount", or "trial_extension"
          enum:
            - free_access
            - percentage_discount
            - fixed_discount
            - trial_extension
        action_config:
          type: object
          additionalProperties: true
          description: Configuration applied by the code (e.g. access_level, role)
        redirect_url:
          type: string
          nullable: true
          description: Internal redirect path after redemption (e.g. a welcome page)
        error:
          type: string
          description: Error message on failure

    # -----------------------------------------------------------------------
    # Activity Analytics
    # -----------------------------------------------------------------------
    ActivityAnalyticsRequest:
      type: object
      required:
        - startDate
        - endDate
      properties:
        startDate:
          type: string
          format: date
          description: Start of the analytics period (ISO date)
        endDate:
          type: string
          format: date
          description: End of the analytics period (ISO date)
        metrics:
          type: string
          default: "growth,engagement"
          description: |
            Comma-separated list of metric groups to fetch:
            `growth`, `engagement`, `userList`, `userDetail`, `scoreAnalytics`
        userId:
          type: string
          format: uuid
          description: Required when metrics includes "userDetail"
        search:
          type: string
          description: Search string for userList filtering
        activityLevel:
          type: string
          enum:
            - high
            - medium
            - low
          description: Activity level filter for userList
        limit:
          type: integer
          minimum: 1
          maximum: 100
          default: 20
          description: Pagination limit for userList
        offset:
          type: integer
          minimum: 0
          default: 0
          description: Pagination offset for userList

    ActivityAnalyticsResponse:
      type: object
      properties:
        success:
          type: boolean
        growth:
          $ref: '#/components/schemas/GrowthMetrics'
        engagement:
          $ref: '#/components/schemas/EngagementMetrics'
        userList:
          type: object
          properties:
            users:
              type: array
              items:
                $ref: '#/components/schemas/UserActivityListItem'
            total:
              type: integer
        userDetail:
          $ref: '#/components/schemas/UserActivityDetail'
        scoreAnalytics:
          $ref: '#/components/schemas/ScoreAnalyticsData'
        error:
          type: string

    GrowthMetrics:
      type: object
      properties:
        dau:
          type: integer
          description: Daily active users
        wau:
          type: integer
          description: Weekly active users
        mau:
          type: integer
          description: Monthly active users
        newRegistrations:
          type: integer
        retentionRate:
          type: number
          description: Day-7 retention rate of most recent cohort
        dauTimeSeries:
          type: array
          items:
            type: object
            properties:
              date:
                type: string
                format: date
              user_count:
                type: integer
        registrationTimeSeries:
          type: array
          items:
            type: object
            properties:
              date:
                type: string
                format: date
              registration_count:
                type: integer
        retentionCohorts:
          type: array
          items:
            type: object
            properties:
              cohort_week:
                type: string
              cohort_size:
                type: integer
              day_1_retention:
                type: number
              day_7_retention:
                type: number
              day_14_retention:
                type: number
              day_30_retention:
                type: number
        churnRiskUsers:
          type: array
          items:
            type: object
            properties:
              user_id:
                type: string
                format: uuid
              full_name:
                type: string
                nullable: true
              avatar_url:
                type: string
                nullable: true
              dojo_score:
                type: number
              last_active_at:
                type: string
                format: date-time
              days_since_active:
                type: integer
              total_activities:
                type: integer

    EngagementMetrics:
      type: object
      properties:
        totalLessonsCompleted:
          type: integer
        totalCourseCompletions:
          type: integer
        totalQuizAttempts:
          type: integer
        totalVideoWatches:
          type: integer
        avgTimeSpentMinutes:
          type: number
        avgDojoScore:
          type: number
        totalActivities:
          type: integer
        activityBreakdown:
          type: array
          items:
            type: object
            properties:
              activity_type:
                type: string
              count:
                type: integer
              percentage:
                type: number

    UserActivityListItem:
      type: object
      properties:
        user_id:
          type: string
          format: uuid
        full_name:
          type: string
          nullable: true
        username:
          type: string
          nullable: true
        avatar_url:
          type: string
          nullable: true
        dojo_score:
          type: number
        activity_level:
          type: string
        total_sessions:
          type: integer
        total_activities:
          type: integer
        total_time_minutes:
          type: number
        last_active_at:
          type: string
          format: date-time
          nullable: true

    UserActivityDetail:
      type: object
      properties:
        user_id:
          type: string
          format: uuid
        full_name:
          type: string
          nullable: true
        username:
          type: string
          nullable: true
        avatar_url:
          type: string
          nullable: true
        dojo_score:
          type: number
        total_activities:
          type: integer
        total_sessions:
          type: integer
        total_time_minutes:
          type: number
        streak_days:
          type: integer
        first_activity_at:
          type: string
          format: date-time
          nullable: true
        last_activity_at:
          type: string
          format: date-time
          nullable: true
        heatmap:
          type: array
          items:
            type: object
            properties:
              date:
                type: string
                format: date
              activity_count:
                type: integer
              intensity_score:
                type: number
        timeline:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
              activity_type:
                type: string
              entity_type:
                type: string
                nullable: true
              entity_id:
                type: string
                nullable: true
              metadata:
                type: object
              points_earned:
                type: number
              created_at:
                type: string
                format: date-time

    ScoreAnalyticsData:
      type: object
      properties:
        scoreDistribution:
          type: array
          items:
            type: object
            properties:
              rank_name:
                type: string
              rank_order:
                type: integer
              user_count:
                type: integer
              percentage:
                type: number
        pillarBreakdown:
          type: array
          items:
            type: object
            properties:
              pillar:
                type: string
              total_points:
                type: number
              user_count:
                type: integer
              percentage:
                type: number
        topScorers:
          type: array
          items:
            type: object
            properties:
              user_id:
                type: string
                format: uuid
              full_name:
                type: string
                nullable: true
              username:
                type: string
                nullable: true
              avatar_url:
                type: string
                nullable: true
              total_score:
                type: number
              rank_name:
                type: string
              rank_position:
                type: integer
        kpiStats:
          type: object
          properties:
            total_scored_users:
              type: integer
            average_score:
              type: number
            champion_plus_count:
              type: integer
            median_score:
              type: number

    # -----------------------------------------------------------------------
    # Admin Users
    # -----------------------------------------------------------------------
    AdminUsersResponse:
      type: object
      properties:
        success:
          type: boolean
        users:
          type: array
          items:
            $ref: '#/components/schemas/AdminUser'
        requesting_user:
          $ref: '#/components/schemas/RequestingUser'
        error:
          type: string

    AdminUser:
      type: object
      properties:
        id:
          type: string
          format: uuid
        full_name:
          type: string
          nullable: true
        username:
          type: string
          nullable: true
        email:
          type: string
          format: email
          nullable: true
        avatar_url:
          type: string
          nullable: true
        discord_user_id:
          type: string
          nullable: true
        discord_connected_at:
          type: string
          format: date-time
          nullable: true
        github_user_id:
          type: string
          nullable: true
        github_url:
          type: string
          nullable: true
        github_connected_at:
          type: string
          format: date-time
          nullable: true
        onboarding_completed_at:
          type: string
          format: date-time
          nullable: true
        access_level:
          type: string
          nullable: true
          description: "Subscription tier: free, pro, or vip"
        dojo_score:
          type: number
          nullable: true
        stripe_customer_id:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        last_sign_in_at:
          type: string
          format: date-time
          nullable: true
        roles:
          type: array
          items:
            type: string
          description: "User roles (e.g. [\"admin\", \"moderator\"])"
        enrolled_courses_count:
          type: integer

    RequestingUser:
      type: object
      properties:
        id:
          type: string
          format: uuid
        email:
          type: string
          format: email
          nullable: true
        roles:
          type: array
          items:
            type: string
