openapi: 3.0.3
info:
  title: EVOL API v2
  description: |
    REST + WebSocket API for EVOL dating app v2.
    
    ## Architecture
    - **REST API**: Profile, Match, Gems, Media uploads
    - **Socket.IO**: All chat operations, presence, typing
    - **Firebase RTDB**: Message storage backend
    
    ## Key Features
    - On-demand matching with daily quota (3/day)
    - Real-time chat with text, image, and voice messages
    - Time-limited chatrooms with gem-based extensions
    - Presence system (online/typing status)
    
    ## Authentication
    Uses **Firebase Authentication** with the following methods:
    - Google Sign-In
    - Apple Sign-In
    - Phone Number (SMS OTP)
    
    After Firebase auth, client receives a Firebase ID token. 
    Include this token in all API requests:
    ```
    Authorization: Bearer <firebase_id_token>
    ```
    
    ## Chat & Presence
    All chat operations use Socket.IO at `/socket.io`.
    See WebSocket documentation for details.
  version: 2.0.0
  contact:
    name: EVOL Team

servers:
  - url: https://api.evol.app/v2
    description: Production
  - url: http://localhost:8080/v2
    description: Development

tags:
  - name: Auth
    description: |
      Authentication via Firebase.
      
      <details>
      <summary><small>📋 Auth Flow</small></summary>
      
      **Login Flow (handled by Firebase SDK on client):**
      1. User selects login method (Google / Apple / Phone)
      2. Firebase SDK handles OAuth or SMS verification
      3. Firebase returns ID token on success
      4. Client calls `POST /auth/login` with Firebase ID token
      5. Server verifies token, creates/updates user, returns session
      
      **Supported Methods:**
      - Google Sign-In
      - Apple Sign-In  
      - Phone Number (SMS OTP)
      
      **Token Usage:**
      ```
      Authorization: Bearer <firebase_id_token>
      ```
      
      </details>
  - name: Options
    description: Load UI options for dropdowns and selectors
  - name: Onboarding
    description: |
      User registration and profile setup flow.
      
      <details>
      <summary><small>📋 Call Flow</small></summary>
      
      1. Load all options in parallel (`GET /options/*`)
      2. `PUT /onboarding/basic-info` - name, gender, ethnicity, DOB, height, education, interests
      3. `PUT /onboarding/personality` - 5 sets of questions, slider 1-10
      4. `PUT /onboarding/values` - choose 1 answer per question
      5. `PUT /onboarding/preferences` - perfect date + looking for genders
      6. `POST /onboarding/photo` - upload **1 profile photo** (also used for verification)
      7. `POST /onboarding/complete` → proceed to Ideal Partner
      
      *Note: The single photo serves as both profile photo and verification photo.*
      
      </details>
  - name: Ideal Partner
    description: |
      AI-powered ideal partner generation and customization.
      
      <details>
      <summary><small>📋 Generation Flow</small></summary>
      
      1. `POST /ideal-partner/generate` → returns `generationId` (UUID)
      2. Poll `GET /ideal-partner/generate/{id}` until `generated: true`
      3. `GET /ideal-partner/generate/{id}` → view generated profile
      4. User views 5 screens (Type → Interested → Personality → Value → Basic Info)
      5. Decision:
         - "I love this" → `PUT /ideal-partner` with `generationId` → accept & go to Match
         - "Change New" (same params) → `POST /ideal-partner/regenerate` → new variation
         - "Edit" (change params) → `POST /ideal-partner/refine` → Refine Flow
      
      </details>
      
      <details>
      <summary><small>✏️ Refine Flow (Async)</small></summary>
      
      1. Load options (`GET /options/*`)
      2. `POST /ideal-partner/refine` with preferences → returns new `generationId`
      3. Poll `GET /ideal-partner/generate/{id}` until complete
      4. View result, compare with previous generations
      5. `PUT /ideal-partner` with preferred `generationId` → accept
      
      **Refine Options (min 7 interests):**
      - Reference photo, Ethnicity, Education
      - Height range, Age range, Gender preference
      - Interests (7+), Personality, Values, Perfect date
      
      *Only 1 generation can be accepted. User can change anytime with PUT.*
      
      *Note: All generations are saved in backend but not exposed via list API.*
      
      </details>
  - name: Profile
    description: |
      User profile management after onboarding.
      
      <details>
      <summary><small>📋 Edit Profile Flow</small></summary>
      
      1. `GET /me` → get current profile
      2. Load options if needed (`GET /options/*`)
      3. `PATCH /me` - update name, ethnicity, height, education, interests, occupation, religion
      4. Manage photos (can add up to 6 total):
         - `POST /me/photos` - add additional photos
         - `DELETE /me/photos/{id}` - remove (cannot delete verified photo)
         - `PUT /me/photos` - reorder
      5. `GET /me` → confirm changes
      
      *Note: Onboarding uploads 1 verified photo. Users can add up to 5 more photos after onboarding.*
      
      </details>
  - name: Match
    description: |
      Match discovery and interactions.
      
      <details>
      <summary><small>📋 Matching Algorithm</small></summary>
      
      **Score Formula:**
      ```
      Total = (Personality × 35%) + (Values × 25%) + (Interests × 15%) + (Face × 25%)
      ```
      
      **Components:**
      1. **Personality (35%)** - Big Five trait similarity with ideal partner
      2. **Values (25%)** - Traditionalism & conformity alignment
      3. **Interests (15%)** - Shared interests overlap
      4. **Face (25%)** - AWS Rekognition similarity to ideal avatar
      
      **Match Indicators:**
      - 🟢 Green: 66-100% (good match)
      - 🟡 Yellow: 35-65% (moderate match)
      - 🔴 Red: 0-34% (low match)
      
      See RFC-003 for full algorithm details.
      
      </details>
      
      <details>
      <summary><small>📋 Call Flow</small></summary>
      
      1. `GET /discovery` - Get current match stack
      2. `GET /discovery/quota` - Check remaining quota
      3. `POST /discovery/next` - Request new match (uses 1 quota)
      4. For each match card:
         - `POST /discovery/{matchId}/like` - Express interest
         - `POST /discovery/{matchId}/pass` - Skip
      5. If quota exhausted: `POST /gems/purchase/quota`
      
      </details>
  - name: Conversations
    description: Chat room metadata (messages via Socket.IO)
  - name: Gems
    description: Virtual currency and purchases
  - name: Media
    description: File uploads for chat

paths:
  # ============================================
  # AUTH (Firebase Authentication)
  # ============================================
  /auth/login:
    post:
      tags: [Auth]
      summary: Login with Firebase token
      description: |
        Exchange Firebase ID token for API session.
        
        **Flow:**
        1. Client authenticates with Firebase (Google/Apple/Phone)
        2. Firebase returns ID token
        3. Client sends token to this endpoint
        4. Server verifies token with Firebase Admin SDK
        5. Server creates/updates user record
        6. Returns user profile and onboarding status
        
        **Note:** Firebase ID tokens expire after 1 hour. 
        Client should refresh tokens automatically via Firebase SDK.
      operationId: login
      security: []  # No auth required for login
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LoginRequest'
      responses:
        '200':
          description: Login successful
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LoginResponse'
        '401':
          description: Invalid or expired Firebase token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthError'

  /auth/logout:
    post:
      tags: [Auth]
      summary: Logout current session
      description: Invalidate current session on server side
      operationId: logout
      responses:
        '204':
          description: Logged out successfully

  /auth/me:
    get:
      tags: [Auth]
      summary: Get current auth status
      description: Returns current user's auth status and basic info
      operationId: getAuthStatus
      responses:
        '200':
          description: Auth status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthStatus'
        '401':
          description: Not authenticated

  # ============================================
  # OPTIONS (for UI dropdowns)
  # ============================================
  /options/genders:
    get:
      tags: [Options]
      summary: Get gender options
      operationId: getGenderOptions
      responses:
        '200':
          description: Gender options
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OptionsResponse'
              example:
                options:
                  - id: "gender_male"
                    label: "Male"
                    order: 1
                  - id: "gender_female"
                    label: "Female"
                    order: 2
                  - id: "gender_non_binary"
                    label: "Non-binary"
                    order: 3

  /options/ethnicities:
    get:
      tags: [Options]
      summary: Get ethnicity options
      operationId: getEthnicityOptions
      responses:
        '200':
          description: Ethnicity options
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OptionsResponse'
              example:
                options:
                  - id: "ethnicity_asian"
                    label: "Asian"
                    order: 1
                  - id: "ethnicity_black"
                    label: "Black / African"
                    order: 2
                  - id: "ethnicity_hispanic"
                    label: "Hispanic / Latino"
                    order: 3
                  - id: "ethnicity_middle_eastern"
                    label: "Middle Eastern"
                    order: 4
                  - id: "ethnicity_native_american"
                    label: "Native American"
                    order: 5
                  - id: "ethnicity_pacific_islander"
                    label: "Pacific Islander"
                    order: 6
                  - id: "ethnicity_white"
                    label: "White / Caucasian"
                    order: 7
                  - id: "ethnicity_mixed"
                    label: "Mixed / Multiple"
                    order: 8
                  - id: "ethnicity_other"
                    label: "Other"
                    order: 9
                  - id: "ethnicity_prefer_not_to_say"
                    label: "Prefer not to say"
                    order: 10

  /options/education-levels:
    get:
      tags: [Options]
      summary: Get education level options
      operationId: getEducationOptions
      responses:
        '200':
          description: Education level options
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OptionsResponse'
              example:
                options:
                  - id: "education_high_school"
                    label: "High School"
                    order: 1
                  - id: "education_vocational"
                    label: "Vocational / Trade School"
                    order: 2
                  - id: "education_associate"
                    label: "Associate Degree"
                    order: 3
                  - id: "education_bachelors"
                    label: "Bachelor's Degree"
                    order: 4
                  - id: "education_masters"
                    label: "Master's Degree"
                    order: 5
                  - id: "education_doctorate"
                    label: "Doctorate / PhD"
                    order: 6
                  - id: "education_professional"
                    label: "Professional Degree (MD, JD, etc.)"
                    order: 7
                  - id: "education_other"
                    label: "Other"
                    order: 8

  /options/interests:
    get:
      tags: [Options]
      summary: Get interest options
      description: Returns searchable list of interests for type-ahead selection (min 2, max 5 selections)
      operationId: getInterestOptions
      parameters:
        - name: q
          in: query
          description: Search query for filtering interests
          schema:
            type: string
      responses:
        '200':
          description: Interest options
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OptionsResponse'
              example:
                options:
                  - id: "interest_art"
                    label: "Art"
                    order: 1
                  - id: "interest_board_games"
                    label: "Board Games"
                    order: 2
                  - id: "interest_cooking"
                    label: "Cooking"
                    order: 3
                  - id: "interest_dancing"
                    label: "Dancing"
                    order: 4
                  - id: "interest_fitness"
                    label: "Fitness"
                    order: 5
                  - id: "interest_gaming"
                    label: "Gaming"
                    order: 6
                  - id: "interest_hiking"
                    label: "Hiking"
                    order: 7
                  - id: "interest_movies"
                    label: "Movies"
                    order: 8
                  - id: "interest_music"
                    label: "Music"
                    order: 9
                  - id: "interest_photography"
                    label: "Photography"
                    order: 10
                  - id: "interest_reading"
                    label: "Reading"
                    order: 11
                  - id: "interest_sports"
                    label: "Sports"
                    order: 12
                  - id: "interest_travel"
                    label: "Travel"
                    order: 13
                  - id: "interest_yoga"
                    label: "Yoga"
                    order: 14

  /options/perfect-dates:
    get:
      tags: [Options]
      summary: Get perfect date options
      operationId: getPerfectDateOptions
      responses:
        '200':
          description: Perfect date options
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OptionsResponse'
              example:
                options:
                  - id: "date_spontaneous_roadtrip"
                    label: "Spontaneous Road Trip"
                    description: "Adventure and excitement on the open road"
                    order: 1
                  - id: "date_cozy_time"
                    label: "Cozy Time at Home"
                    description: "Relaxing together with movies and comfort food"
                    order: 2
                  - id: "date_fine_dining"
                    label: "Fine Dining Experience"
                    description: "Elegant restaurant with great conversation"
                    order: 3
                  - id: "date_outdoor_adventure"
                    label: "Outdoor Adventure"
                    description: "Hiking, camping, or exploring nature together"
                    order: 4
                  - id: "date_cultural_outing"
                    label: "Cultural Outing"
                    description: "Museums, galleries, or live performances"
                    order: 5
                  - id: "date_active_fun"
                    label: "Active Fun"
                    description: "Sports, games, or physical activities together"
                    order: 6

  /options/looking-for:
    get:
      tags: [Options]
      summary: Get "looking for" gender options
      description: |
        Returns gender options for "looking for" selection.
        User can select **multiple** options (at least 1 required).
      operationId: getLookingForOptions
      responses:
        '200':
          description: Looking for options (multi-select)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OptionsResponse'
              example:
                options:
                  - id: "looking_for_male"
                    label: "Male"
                    order: 1
                  - id: "looking_for_female"
                    label: "Female"
                    order: 2
                  - id: "looking_for_non_binary"
                    label: "Non-binary"
                    order: 3

  /options/religions:
    get:
      tags: [Options]
      summary: Get religion options
      description: Optional field for profile
      operationId: getReligionOptions
      responses:
        '200':
          description: Religion options
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OptionsResponse'
              example:
                options:
                  - id: "religion_christianity"
                    label: "Christianity"
                    order: 1
                  - id: "religion_islam"
                    label: "Islam"
                    order: 2
                  - id: "religion_buddhism"
                    label: "Buddhism"
                    order: 3
                  - id: "religion_hinduism"
                    label: "Hinduism"
                    order: 4
                  - id: "religion_judaism"
                    label: "Judaism"
                    order: 5
                  - id: "religion_sikhism"
                    label: "Sikhism"
                    order: 6
                  - id: "religion_atheist"
                    label: "Atheist / Agnostic"
                    order: 7
                  - id: "religion_spiritual"
                    label: "Spiritual but not religious"
                    order: 8
                  - id: "religion_other"
                    label: "Other"
                    order: 9
                  - id: "religion_prefer_not_to_say"
                    label: "Prefer not to say"
                    order: 10

  /options/personality-items:
    get:
      tags: [Options]
      summary: Get personality quiz question sets
      description: |
        Returns 5 sets of personality questions (Big Five traits).
        Each set contains k questions. User answers each on 1-10 scale.
        User must interact with slider (drag) to proceed - cannot skip.
      operationId: getPersonalityItems
      responses:
        '200':
          description: Array of question sets
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PersonalityItemsResponse'
              example:
                questionSets:
                  - id: "extraversion"
                    label: "Extraversion"
                    description: "How outgoing and social you are"
                    questions:
                      - id: "ext_1"
                        text: "I am the life of the party"
                        reversed: false
                      - id: "ext_2"
                        text: "I don't talk a lot"
                        reversed: true
                  - id: "conscientiousness"
                    label: "Conscientiousness"
                    description: "How organized and dependable you are"
                    questions:
                      - id: "con_1"
                        text: "I am always prepared"
                        reversed: false

  /options/values-items:
    get:
      tags: [Options]
      summary: Get values assessment questions
      description: |
        Returns array of values questions. Each question has multiple choice answers.
        User must select exactly ONE answer per question.
      operationId: getValuesItems
      responses:
        '200':
          description: Array of values questions
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValuesItemsResponse'
              example:
                questions:
                  - id: "val_1"
                    text: "When making important decisions, I prefer to..."
                    answers:
                      - id: "val_1_a"
                        text: "Follow established traditions and customs"
                        score: 1
                      - id: "val_1_b"
                        text: "Consider both tradition and new approaches"
                        score: 5
                      - id: "val_1_c"
                        text: "Challenge conventions and try new methods"
                        score: 10
                  - id: "val_2"
                    text: "In a group setting, I typically..."
                    answers:
                      - id: "val_2_a"
                        text: "Go along with what the group decides"
                        score: 1
                      - id: "val_2_b"
                        text: "Share my opinion but accept group consensus"
                        score: 5
                      - id: "val_2_c"
                        text: "Stand firm on my own views regardless of others"
                        score: 10

  # ============================================
  # ONBOARDING
  # ============================================
  /onboarding/status:
    get:
      tags: [Onboarding]
      summary: Get onboarding progress
      description: Returns current onboarding step and completion status
      operationId: getOnboardingStatus
      responses:
        '200':
          description: Onboarding status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OnboardingStatus'

  /onboarding/basic:
    put:
      tags: [Onboarding]
      summary: "Step 1: Submit basic info"
      description: Name, DOB, gender, ethnicity, height, education
      operationId: submitBasicInfo
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BasicInfoRequest'
      responses:
        '200':
          description: Basic info saved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OnboardingStatus'

  /onboarding/interests:
    put:
      tags: [Onboarding]
      summary: "Step 2: Submit interests"
      description: Select 2-5 interests from the options list
      operationId: submitInterests
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InterestsRequest'
      responses:
        '200':
          description: Interests saved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OnboardingStatus'
        '400':
          description: Validation error (must have 2-5 interests)

  /onboarding/personality:
    put:
      tags: [Onboarding]
      summary: "Step 3: Submit personality quiz"
      description: |
        Submit answers for all personality questions from all 5 question sets.
        
        - Get questions from `GET /options/personality-items`
        - Each answer is **1-10 scale**
        - User must drag slider for each question (cannot skip)
        - Must answer ALL questions from ALL 5 sets
      operationId: submitPersonality
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PersonalityAnswersRequest'
            example:
              answers:
                - questionId: "ext_1"
                  value: 7
                - questionId: "ext_2"
                  value: 3
                - questionId: "con_1"
                  value: 8
                - questionId: "con_2"
                  value: 6
      responses:
        '200':
          description: Personality saved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OnboardingStatus'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PersonalityValidationError'

  /onboarding/values:
    put:
      tags: [Onboarding]
      summary: "Step 4: Submit values assessment"
      description: |
        Submit answers for all values questions.
        
        - Get questions from `GET /options/values-items`
        - Each question has multiple choice answers
        - User must select exactly ONE answer per question
        - Must answer ALL questions
      operationId: submitValues
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ValuesAnswersRequest'
            example:
              answers:
                - questionId: "val_1"
                  answerId: "val_1_b"
                - questionId: "val_2"
                  answerId: "val_2_c"
                - questionId: "val_3"
                  answerId: "val_3_a"
      responses:
        '200':
          description: Values saved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OnboardingStatus'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValuesValidationError'

  /onboarding/preferences:
    put:
      tags: [Onboarding]
      summary: "Step 5: Submit preferences"
      description: Perfect date choice and looking for genders
      operationId: submitPreferences
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PreferencesRequest'
      responses:
        '200':
          description: Preferences saved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OnboardingStatus'

  /onboarding/photo:
    post:
      tags: [Onboarding]
      summary: "Step 6: Upload profile photo"
      description: |
        Upload **1 profile photo** that serves dual purpose:
        1. **Profile display** - This becomes the user's profile photo
        2. **Identity verification** - Same photo used for AWS Rekognition verification
        
        **AWS Rekognition Integration:**
        1. **DetectFaces** - Verify photo quality:
           - Exactly one face detected
           - Face is frontal (not turned)
           - Good lighting and sharpness
           - No sunglasses or obstructions
        
        2. **IndexFaces** - On success, index face for matching:
           - Extracts face embedding
           - Stores in AWS Face Collection
           - Returns `faceId` for future face searches
        
        The indexed face is used for appearance matching against other users' ideal partner avatars.
        
        **Validation:**
        - Must show full face clearly
        - Must be a frontal photo (not side profile)
        - Good lighting required
        - No sunglasses or face obstructions
      operationId: submitProfilePhoto
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - photo
              properties:
                photo:
                  type: string
                  format: binary
                  description: Frontal face photo (used for both profile and verification)
      responses:
        '200':
          description: Photo uploaded, verified, and face indexed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PhotoVerificationResult'
        '400':
          description: Verification failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VerificationError'

  # ============================================
  # IDEAL PARTNER (AI-powered matching profile)
  # ============================================
  /ideal-partner/generate:
    post:
      tags: [Ideal Partner]
      summary: Start generation (async)
      description: |
        Start generating ideal partner type. Returns immediately with a generation ID.
        
        UI should poll `GET /ideal-partner/generate/{generationId}` until `generated: true`.
        
        Generation uses user's existing profile data (personality, values, interests).
      operationId: startGenerateIdealPartner
      responses:
        '202':
          description: Generation started
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenerationStartResponse'
        '400':
          description: User must complete onboarding first

  /ideal-partner/generate/{generationId}:
    get:
      tags: [Ideal Partner]
      summary: Poll generation status
      description: |
        Poll this endpoint to check if generation is complete.
        
        When `generated: true`, call `GET /ideal-partner` to fetch the result.
      operationId: pollGenerationStatus
      parameters:
        - name: generationId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          example: "550e8400-e29b-41d4-a716-446655440000"
      responses:
        '200':
          description: Generation status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenerationStatusResponse'
        '404':
          description: Generation ID not found

  /ideal-partner/regenerate:
    post:
      tags: [Ideal Partner]
      summary: Regenerate with same params (async)
      description: |
        Regenerate ideal partner using the **same parameters** as the current/last generation.
        Creates a new variation without changing any settings.
        
        Use this when user wants to see a different result but keep the same preferences.
        
        For changing parameters, use `POST /ideal-partner/refine` instead.
      operationId: regenerateIdealPartner
      responses:
        '202':
          description: Regeneration started
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenerationStartResponse'

  /ideal-partner:
    get:
      tags: [Ideal Partner]
      summary: Get accepted ideal partner type
      description: |
        Returns the user's ideal partner profile with both:
        - `recommended`: Original AI-generated (immutable, used for matching)
        - `current`: User's edited version (if any edits made)
      operationId: getIdealPartner
      responses:
        '200':
          description: Ideal partner profile
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IdealPartnerProfile'
        '404':
          description: No ideal partner type generated yet

    put:
      tags: [Ideal Partner]
      summary: Accept a specific generation
      description: |
        Accept a specific generated ideal partner by its `generationId`.
        
        - Only **1** ideal partner can be accepted at a time
        - User can change anytime by calling PUT with a different `generationId`
        - The accepted generation becomes the active ideal partner for matching
        
        **Flow:**
        1. Generate/refine creates multiple versions (each with unique generationId)
        2. User views different generations
        3. User calls PUT with preferred generationId to accept it
      operationId: acceptIdealPartnerGeneration
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - generationId
              properties:
                generationId:
                  type: string
                  format: uuid
                  description: The generation ID to accept as the active ideal partner
                  example: "550e8400-e29b-41d4-a716-446655440000"
      responses:
        '200':
          description: Ideal partner accepted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IdealPartnerProfile'
        '404':
          description: Generation ID not found
        '400':
          description: Generation not yet complete

    delete:
      tags: [Ideal Partner]
      summary: Delete ideal partner type
      description: Remove the ideal partner profile (will use standard matching)
      operationId: deleteIdealPartner
      responses:
        '204':
          description: Ideal partner profile deleted

  /ideal-partner/refine:
    post:
      tags: [Ideal Partner]
      summary: Refine ideal partner with new params (async)
      description: |
        Refine the ideal partner with **new parameters**. This is an **async process**.
        
        Returns a `generationId` to poll for completion.
        
        Use this when user wants to change their ideal partner preferences.
        For same params with new variation, use `POST /ideal-partner/regenerate` instead.
        
        **Refinement Options:**
        - Reference photo URL (optional) - avatar based on photo
        - Face description (optional) - text describing ideal face/appearance
        - Ethnicity, Education
        - Height range, Age range
        - Gender preference (men/women/others)
        - Interests (at least 7)
        - Personality, Values, Perfect date
      operationId: refineIdealPartner
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdealPartnerRefineRequest'
      responses:
        '202':
          description: Refinement started
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenerationStartResponse'
        '400':
          description: Validation error (e.g., less than 7 interests)

  /options/personality-traits:
    get:
      tags: [Options]
      summary: Get personality trait pair options
      description: |
        Returns personality trait **pairs** for ideal partner editing.
        User must choose ONE trait from each pair.
        
        "Tell us what you value in your type. Choose either one in each pair!"
      operationId: getPersonalityTraitOptions
      responses:
        '200':
          description: Personality trait pairs
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PersonalityTraitPairsResponse'
              example:
                pairs:
                  - id: "pair_openness"
                    left:
                      id: "trait_imaginative"
                      label: "Imaginative"
                    right:
                      id: "trait_conventional"
                      label: "Conventional"
                  - id: "pair_conscientiousness"
                    left:
                      id: "trait_organized"
                      label: "Organized"
                    right:
                      id: "trait_spontaneous"
                      label: "Spontaneous"
                  - id: "pair_extraversion"
                    left:
                      id: "trait_outgoing"
                      label: "Outgoing"
                    right:
                      id: "trait_reserved"
                      label: "Reserved"
                  - id: "pair_warmth"
                    left:
                      id: "trait_warm"
                      label: "Warm"
                    right:
                      id: "trait_independent"
                      label: "Independent"
                  - id: "pair_sensitivity"
                    left:
                      id: "trait_sensitive"
                      label: "Sensitive"
                    right:
                      id: "trait_calm"
                      label: "Calm"
                  - id: "pair_traditionalism"
                    left:
                      id: "trait_traditional"
                      label: "Traditional"
                    right:
                      id: "trait_progressive"
                      label: "Progressive"

  # ============================================
  # PROFILE
  # ============================================
  /me:
    get:
      tags: [Profile]
      summary: Get current user profile
      description: Returns full profile with photos, preferences, stats
      operationId: getMyProfile
      responses:
        '200':
          description: User profile
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MyProfile'

    patch:
      tags: [Profile]
      summary: Update profile fields
      description: Partial update of profile fields
      operationId: updateMyProfile
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProfileUpdateRequest'
      responses:
        '200':
          description: Updated profile
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MyProfile'

  /me/photos:
    get:
      tags: [Profile]
      summary: Get my photos
      operationId: getMyPhotos
      responses:
        '200':
          description: Photo list
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Photo'

    post:
      tags: [Profile]
      summary: Add a photo
      operationId: addPhoto
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                photo:
                  type: string
                  format: binary
                caption:
                  type: string
                  maxLength: 200
      responses:
        '201':
          description: Photo added
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Photo'

    put:
      tags: [Profile]
      summary: Reorder photos
      description: Batch reorder photos by providing ordered list of photo IDs
      operationId: reorderPhotos
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                photoIds:
                  type: array
                  items:
                    type: string
                  description: Ordered list of photo IDs (first = cover)
      responses:
        '200':
          description: Photos reordered
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Photo'

  /me/photos/{photoId}:
    delete:
      tags: [Profile]
      summary: Delete a photo
      operationId: deletePhoto
      parameters:
        - name: photoId
          in: path
          required: true
          schema:
            type: string
      responses:
        '204':
          description: Photo deleted

  /me/preferences:
    get:
      tags: [Profile]
      summary: Get match preferences
      operationId: getPreferences
      responses:
        '200':
          description: Preferences
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Preferences'

    put:
      tags: [Profile]
      summary: Update match preferences
      operationId: updatePreferences
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PreferencesUpdateRequest'
      responses:
        '200':
          description: Updated preferences
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Preferences'

  /users/{userId}:
    get:
      tags: [Profile]
      summary: Get user profile
      description: View another user's public profile (only if matched or in discovery)
      operationId: getUserProfile
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: User profile
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserProfile'
        '403':
          description: Not allowed to view this profile

  # ============================================
  # DISCOVERY (MATCHING)
  # ============================================
  /discovery:
    get:
      tags: [Match]
      summary: Get discovery stack
      description: |
        Returns current matches for today with full profiles.
        Matches are calculated on-demand when requested.
        Daily quota: 3 matches (can purchase more with gems).
      operationId: getMatchStack
      responses:
        '200':
          description: Match stack
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MatchStack'

  /discovery/next:
    post:
      tags: [Match]
      summary: Request next match
      description: |
        Calculates and returns a new match on-demand.
        Consumes 1 quota. Returns 402 if no quota available.
        
        **Matching Process:**
        1. Search candidates using AWS Rekognition `SearchFacesByImage` (ideal avatar)
        2. Calculate personality/values/interests scores
        3. Combine weighted scores (see RFC-003)
        4. Generate AI explanation for top match
        5. Return match card with full breakdown
      operationId: requestNextMatch
      responses:
        '200':
          description: New match
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MatchCard'
        '402':
          description: No quota available
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/QuotaError'
        '404':
          description: No more potential matches available

  /discovery/{matchId}/like:
    post:
      tags: [Match]
      summary: Like a match
      description: Express interest in a match. If mutual, creates a chat room.
      operationId: likeMatch
      parameters:
        - name: matchId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Like recorded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LikeResult'

  /discovery/{matchId}/pass:
    post:
      tags: [Match]
      summary: Pass on a match
      description: Skip this match. They won't appear again.
      operationId: passMatch
      parameters:
        - name: matchId
          in: path
          required: true
          schema:
            type: string
      responses:
        '204':
          description: Pass recorded

  /discovery/quota:
    get:
      tags: [Match]
      summary: Get match quota status
      description: Returns remaining daily quota and reset time
      operationId: getMatchQuota
      responses:
        '200':
          description: Quota status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MatchQuota'

  # ============================================
  # CONVERSATIONS (Metadata only - messages via Socket.IO)
  # ============================================
  /conversations:
    get:
      tags: [Conversations]
      summary: Get conversation list
      description: |
        Returns all chat rooms with metadata.
        **Note:** Messages are fetched via Socket.IO, not REST.
      operationId: getConversations
      parameters:
        - name: cursor
          in: query
          schema:
            type: string
          description: Pagination cursor
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            maximum: 50
      responses:
        '200':
          description: Conversation list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConversationList'

  /conversations/{roomId}:
    get:
      tags: [Conversations]
      summary: Get conversation details
      description: |
        Returns room info, partner profile, and expiration status.
        **Note:** Use Socket.IO to fetch messages and send messages.
      operationId: getConversation
      parameters:
        - name: roomId
          in: path
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: Conversation details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Conversation'

  # ============================================
  # MEDIA UPLOADS
  # ============================================
  /media/upload:
    post:
      tags: [Media]
      summary: Upload media file
      description: |
        Upload image or voice file for chat messages.
        Returns URL to use in Socket.IO message event.
      operationId: uploadMedia
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - file
                - type
              properties:
                file:
                  type: string
                  format: binary
                type:
                  type: string
                  enum: [image, voice]
      responses:
        '200':
          description: Upload successful
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MediaUploadResult'

  # ============================================
  # GEMS & PURCHASES
  # ============================================
  /gems:
    get:
      tags: [Gems]
      summary: Get gem balance
      operationId: getGemBalance
      responses:
        '200':
          description: Gem balance
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GemBalance'

  /gems/products:
    get:
      tags: [Gems]
      summary: Get purchasable products
      description: Returns available gem packages and in-app purchases
      operationId: getProducts
      responses:
        '200':
          description: Product list
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Product'

  /gems/purchase/quota:
    post:
      tags: [Gems]
      summary: Purchase additional match quota
      description: Spend gems to get extra daily matches
      operationId: purchaseQuota
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                quantity:
                  type: integer
                  minimum: 1
                  default: 1
                  description: Number of extra matches to purchase
      responses:
        '200':
          description: Quota purchased
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PurchaseResult'
        '402':
          description: Insufficient gems

  /gems/purchase/room-extend:
    post:
      tags: [Gems]
      summary: Extend chat room expiration
      description: Spend gems to extend an expiring chat room
      operationId: purchaseRoomExtend
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - roomId
              properties:
                roomId:
                  type: integer
                  format: int64
      responses:
        '200':
          description: Room extended
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExtendRoomResult'
        '402':
          description: Insufficient gems
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InsufficientGemsError'

  /gems/history:
    get:
      tags: [Gems]
      summary: Get gem transaction history
      operationId: getGemHistory
      parameters:
        - name: cursor
          in: query
          schema:
            type: string
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
      responses:
        '200':
          description: Transaction history
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GemHistoryList'

  # ============================================
  # REPORTS & BLOCKS
  # ============================================
  /users/{userId}/block:
    post:
      tags: [Profile]
      summary: Block a user
      operationId: blockUser
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            type: string
      responses:
        '204':
          description: User blocked

    delete:
      tags: [Profile]
      summary: Unblock a user
      operationId: unblockUser
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            type: string
      responses:
        '204':
          description: User unblocked

  /users/{userId}/report:
    post:
      tags: [Profile]
      summary: Report a user
      operationId: reportUser
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReportRequest'
      responses:
        '204':
          description: Report submitted

# ============================================
# COMPONENTS
# ============================================
components:
  schemas:
    # --- Auth ---
    LoginRequest:
      type: object
      required:
        - firebaseToken
      properties:
        firebaseToken:
          type: string
          description: Firebase ID token from client-side authentication
        deviceInfo:
          type: object
          description: Optional device information for analytics
          properties:
            platform:
              type: string
              enum: [ios, android]
            deviceId:
              type: string
            appVersion:
              type: string
            osVersion:
              type: string

    LoginResponse:
      type: object
      properties:
        user:
          $ref: '#/components/schemas/AuthUser'
        isNewUser:
          type: boolean
          description: True if this is a first-time login (new account created)
        onboardingCompleted:
          type: boolean
          description: Whether user has completed onboarding
        onboardingStatus:
          $ref: '#/components/schemas/OnboardingStatus'
          description: Current onboarding progress (if not completed)

    AuthUser:
      type: object
      properties:
        id:
          type: string
          format: uuid
          description: Internal user ID
        firebaseUid:
          type: string
          description: Firebase user UID
        authProvider:
          type: string
          enum: [google, apple, phone]
          description: Authentication method used
        email:
          type: string
          format: email
          description: Email (from Google/Apple, null for phone auth)
        phoneNumber:
          type: string
          description: Phone number (for phone auth, null for social auth)
        name:
          type: string
          description: Display name (may be from social provider or set during onboarding)
        photoUrl:
          type: string
          format: uri
          description: Profile photo URL (if onboarding completed)
        createdAt:
          type: string
          format: date-time
        lastLoginAt:
          type: string
          format: date-time

    AuthStatus:
      type: object
      properties:
        authenticated:
          type: boolean
        user:
          $ref: '#/components/schemas/AuthUser'
        onboardingCompleted:
          type: boolean
        tokenExpiresAt:
          type: string
          format: date-time
          description: When the current Firebase token expires

    AuthError:
      type: object
      properties:
        error:
          type: string
          enum:
            - invalid_token
            - expired_token
            - revoked_token
            - user_disabled
            - user_not_found
        message:
          type: string
          description: Human-readable error message

    # --- Options ---
    OptionsResponse:
      type: object
      properties:
        options:
          type: array
          items:
            $ref: '#/components/schemas/Option'

    Option:
      type: object
      properties:
        id:
          type: string
          description: Stable option ID (e.g., "gender_male", "ethnicity_asian")
        label:
          type: string
          description: Display label
        description:
          type: string
          description: Optional description text
        order:
          type: integer
          description: Display order

    PersonalityItemsResponse:
      type: object
      properties:
        questionSets:
          type: array
          items:
            $ref: '#/components/schemas/PersonalityQuestionSet'
          minItems: 5
          maxItems: 5
          description: 5 sets of questions (Big Five traits)

    PersonalityQuestionSet:
      type: object
      properties:
        id:
          type: string
          description: Set ID (extraversion, conscientiousness, openness, agreeableness, neuroticism)
        label:
          type: string
          description: Display label for the trait
        description:
          type: string
          description: Brief description of what this trait measures
        questions:
          type: array
          items:
            $ref: '#/components/schemas/PersonalityQuestion'
          description: Array of questions in this set

    PersonalityQuestion:
      type: object
      properties:
        id:
          type: string
          description: Question ID (e.g., "ext_1", "con_3")
        text:
          type: string
          description: Question text to display
        reversed:
          type: boolean
          description: If true, score is inverted (11 - answer) when calculating trait score

    ValuesItemsResponse:
      type: object
      properties:
        questions:
          type: array
          items:
            $ref: '#/components/schemas/ValuesQuestion'
          description: Array of values questions

    ValuesQuestion:
      type: object
      properties:
        id:
          type: string
          description: Question ID (e.g., "val_1")
        text:
          type: string
          description: Question text to display
        answers:
          type: array
          items:
            $ref: '#/components/schemas/ValuesAnswer'
          description: Array of possible answers (user picks one)

    ValuesAnswer:
      type: object
      properties:
        id:
          type: string
          description: Answer ID (e.g., "val_1_a")
        text:
          type: string
          description: Answer text to display
        score:
          type: integer
          description: Score value for this answer (used for matching)

    # --- Onboarding ---
    OnboardingStatus:
      type: object
      properties:
        completed:
          type: boolean
        currentStep:
          type: integer
          description: "1=basic, 2=interests, 3=personality, 4=values, 5=preferences, 6=photo"
        steps:
          type: object
          properties:
            basic:
              type: boolean
              description: Name, DOB, gender, ethnicity, height, education
            interests:
              type: boolean
              description: 2-5 interests selected
            personality:
              type: boolean
              description: All Big Five questions answered
            values:
              type: boolean
              description: All values questions answered
            preferences:
              type: boolean
              description: Perfect date + looking for genders
            photo:
              type: boolean
              description: Profile photo uploaded and verified (includes face indexing)

    BasicInfoRequest:
      type: object
      required:
        - name
        - birthDate
        - genderId
        - ethnicityId
        - height
        - educationId
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 50
        birthDate:
          type: string
          format: date
        genderId:
          type: string
          description: Option ID from /options/genders
        ethnicityId:
          type: string
          description: Option ID from /options/ethnicities
        height:
          type: integer
          minimum: 140
          maximum: 220
          description: Height in cm
        educationId:
          type: string
          description: Option ID from /options/education-levels

    InterestsRequest:
      type: object
      required:
        - interestIds
      properties:
        interestIds:
          type: array
          items:
            type: string
          minItems: 2
          maxItems: 5
          description: Option IDs from /options/interests

    PersonalityAnswersRequest:
      type: object
      required:
        - answers
      properties:
        answers:
          type: array
          items:
            $ref: '#/components/schemas/PersonalityAnswer'
          description: Must answer all questions from all 5 sets

    PersonalityAnswer:
      type: object
      required:
        - questionId
        - value
      properties:
        questionId:
          type: string
          description: Question ID (e.g., "ext_1", "con_3")
        value:
          type: integer
          minimum: 1
          maximum: 10
          description: Answer value (1-10 scale, user must drag slider)

    ValuesAnswersRequest:
      type: object
      required:
        - answers
      properties:
        answers:
          type: array
          items:
            $ref: '#/components/schemas/ValuesAnswerSubmission'
          description: Must answer all questions (one answer per question)

    ValuesAnswerSubmission:
      type: object
      required:
        - questionId
        - answerId
      properties:
        questionId:
          type: string
          description: Question ID (e.g., "val_1")
        answerId:
          type: string
          description: Selected answer ID (e.g., "val_1_b")

    ValuesValidationError:
      type: object
      properties:
        error:
          type: string
          enum:
            - missing_answers
            - invalid_question_id
            - invalid_answer_id
            - answer_not_for_question
        message:
          type: string
          description: Human-readable error message
        missingQuestionIds:
          type: array
          items:
            type: string
          description: List of question IDs that were not answered
        invalidQuestionIds:
          type: array
          items:
            type: string
          description: List of invalid question IDs submitted
        invalidAnswerIds:
          type: array
          items:
            type: string
          description: List of invalid answer IDs submitted

    PreferencesRequest:
      type: object
      required:
        - perfectDateId
        - lookingForIds
      properties:
        perfectDateId:
          type: string
          description: Option ID from /options/perfect-dates
        lookingForIds:
          type: array
          items:
            type: string
          minItems: 1
          description: Option IDs from /options/looking-for

    PhotoVerificationResult:
      type: object
      description: Result of profile photo upload with verification
      properties:
        success:
          type: boolean
          description: Overall success (photo saved + verified + indexed)
        photo:
          $ref: '#/components/schemas/Photo'
          description: The uploaded profile photo
        verification:
          type: object
          description: Verification details
          properties:
            verified:
              type: boolean
              description: Whether photo passed verification checks
            faceIndexed:
              type: boolean
              description: Whether face was successfully indexed for matching
            faceId:
              type: string
              description: AWS Rekognition Face ID (internal)
            faceQuality:
              type: object
              description: Face quality metrics from AWS Rekognition
              properties:
                brightness:
                  type: number
                  format: float
                  description: Image brightness (0-100)
                sharpness:
                  type: number
                  format: float
                  description: Image sharpness (0-100)
                pose:
                  type: object
                  properties:
                    pitch:
                      type: number
                      format: float
                    roll:
                      type: number
                      format: float
                    yaw:
                      type: number
                      format: float
        onboardingStatus:
          $ref: '#/components/schemas/OnboardingStatus'

    # --- Ideal Partner ---
    GenerationStartResponse:
      type: object
      properties:
        generationId:
          type: string
          format: uuid
          description: UUID to poll for status
          example: "550e8400-e29b-41d4-a716-446655440000"
        message:
          type: string
          example: "Cora is Creating Your Ideal Type..."

    GenerationStatusResponse:
      type: object
      properties:
        generationId:
          type: string
          format: uuid
        generated:
          type: boolean
          description: True when generation is complete
        progress:
          type: integer
          minimum: 0
          maximum: 100
          description: Progress percentage (optional, for UI animation)
        message:
          type: string
          description: Status message to display
          example: "Where science sparks love. Just a sec!"
        error:
          type: string
          description: Error message if generation failed

    IdealPartnerProfile:
      type: object
      properties:
        id:
          type: string
          description: Profile ID
        archetype:
          $ref: '#/components/schemas/IdealPartnerArchetype'
        recommended:
          $ref: '#/components/schemas/IdealPartnerData'
          description: Original AI-generated data (immutable, used for matching)
        current:
          $ref: '#/components/schemas/IdealPartnerData'
          description: User's edited version (initially same as recommended)
        generatedAt:
          type: string
          format: date-time
        acceptedAt:
          type: string
          format: date-time
          description: When user accepted (null if not accepted)
        lastEditedAt:
          type: string
          format: date-time
          description: When user last edited (null if never edited)

    IdealPartnerArchetype:
      type: object
      description: |
        Screen 1: "Type" - The fun archetype name and description.
        Shows "Why he will match you?" with AI-generated explanation.
      properties:
        name:
          type: string
          description: Archetype name
          example: "Golden Retriever Boyfriend"
        whyMatch:
          type: string
          description: |
            "Why he will match you?" - AI-generated explanation
          example: "He's the ultimate loyal, enthusiastic partner who's always excited to see you. Pure sunshine energy that makes you feel adored and secure."
        avatar:
          $ref: '#/components/schemas/IdealPartnerAvatar'

    IdealPartnerAvatar:
      type: object
      description: AI-generated avatar with refinement history
      properties:
        imageUrl:
          type: string
          format: uri
          description: Current avatar image URL
        isRefined:
          type: boolean
          description: Whether user has refined the avatar
          default: false
        refinementSummary:
          type: string
          description: Summary of refinements applied (if any)
          example: "Ponytail hair style, wear glasses"
        conversationId:
          type: string
          description: ID of the conversation that created this avatar (for reference)
        createdAt:
          type: string
          format: date-time
        refinedAt:
          type: string
          format: date-time
          description: When avatar was last refined (null if never)

    IdealPartnerData:
      type: object
      description: |
        Ideal partner attributes (used for both recommended and current).
        
        Screen 5: "Basic Info" shows avatar + demographics:
        - Ethnicity (e.g., "Asian")
        - Age range (e.g., "20-25 yrs old")
        - Height range (e.g., "150-170cm")
        - Education (e.g., "Bachelor Degree")
        
        With buttons: "Change New" (regenerate) and "I love this" (accept)
      properties:
        gender:
          $ref: '#/components/schemas/Option'
          description: Based on user's "looking for" preference
        ethnicity:
          $ref: '#/components/schemas/Option'
          description: Same as user's ethnicity
        ageRange:
          type: object
          description: |
            Age range for ideal partner.
            Female users: same to +3 years from user's age
            Male users: same to -3 years from user's age
          properties:
            min:
              type: integer
              description: Minimum age
            max:
              type: integer
              description: Maximum age
          example:
            min: 20
            max: 25
        heightRange:
          type: object
          description: |
            Height range for ideal partner.
            Female users: +10 to +15cm from user's height
            Male users: -10 to -15cm from user's height
          properties:
            min:
              type: integer
              description: Minimum height in cm
            max:
              type: integer
              description: Maximum height in cm
          example:
            min: 150
            max: 170
        education:
          $ref: '#/components/schemas/Option'
          description: Same as user's education level
        interests:
          type: array
          items:
            $ref: '#/components/schemas/Option'
          minItems: 3
          maxItems: 3
          description: |
            Screen 2: "Interested" - 3 similar interests (from same category, not exact match).
            UI shows: "He loves [Interest1]" "And also [Interest2]" "[Interest3]"
          example:
            - id: "interest_pottery"
              label: "Pottery"
            - id: "interest_art_collecting"
              label: "Art Collecting"
            - id: "interest_painting"
              label: "Painting"
        personality:
          $ref: '#/components/schemas/IdealPartnerPersonality'
        values:
          $ref: '#/components/schemas/IdealPartnerValues'
        perfectDate:
          $ref: '#/components/schemas/Option'
          description: Same as user's perfect date choice

    IdealPartnerPersonality:
      type: object
      description: |
        Screen 3: "Personality" - Radar chart with Big Five scores.
        Shows "Your common is both [Trait1] & [Trait2]"
      properties:
        scores:
          type: object
          description: Numerical scores (1-10) for radar chart and matching algorithm
          properties:
            extraversion:
              type: integer
              minimum: 1
              maximum: 10
            conscientiousness:
              type: integer
              minimum: 1
              maximum: 10
            openness:
              type: integer
              minimum: 1
              maximum: 10
            agreeableness:
              type: integer
              minimum: 1
              maximum: 10
            neuroticism:
              type: integer
              minimum: 1
              maximum: 10
        commonTraits:
          type: array
          items:
            type: string
          description: |
            Common personality traits with user.
            UI shows: "Your common is both [Trait1] & [Trait2]"
          example: ["Caring", "Openness"]
        traits:
          type: array
          items:
            $ref: '#/components/schemas/PersonalityTrait'
          description: Display trait tags derived from scores (for edit screen)
          example:
            - id: "trait_caring"
              label: "Caring"
              selected: true
            - id: "trait_openness"
              label: "Openness"
              selected: true

    PersonalityTrait:
      type: object
      properties:
        id:
          type: string
        label:
          type: string
        selected:
          type: boolean
          description: Whether this trait is active

    PersonalityTraitPairsResponse:
      type: object
      properties:
        pairs:
          type: array
          items:
            $ref: '#/components/schemas/PersonalityTraitPair'

    PersonalityTraitPair:
      type: object
      description: A pair of opposite traits - user picks one
      properties:
        id:
          type: string
          description: Pair identifier
        left:
          $ref: '#/components/schemas/Option'
          description: Left option in the pair
        right:
          $ref: '#/components/schemas/Option'
          description: Right option in the pair

    IdealPartnerValues:
      type: object
      description: |
        Screen 4: "Value" - Shows user's values reflected back.
        UI: "He wants same things as yours which are" + list of value labels.
        Values are SAME as user's own values (copied from user profile).
      properties:
        displayLabels:
          type: array
          items:
            type: string
          description: |
            Labels to display on screen.
            Includes traditionalism label, conformity label, and perfect date.
          example: ["Traditional", "Autonomous", "Cozy Time"]
        traditionalism:
          type: object
          properties:
            score:
              type: integer
              minimum: 1
              maximum: 10
            label:
              type: string
              description: '"Traditional" or "Progressive"'
        conformity:
          type: object
          properties:
            score:
              type: integer
              minimum: 1
              maximum: 10
            label:
              type: string
              description: '"Compliant" or "Autonomous"'

    IdealPartnerRefineRequest:
      type: object
      description: |
        Request to refine ideal partner with new parameters.
        All fields are optional - only include fields to change.
        At least one field must be provided.
      properties:
        referencePhotoUrl:
          type: string
          format: uri
          description: |
            Optional reference photo URL. If provided, avatar generation will be based on this photo.
            Upload photo first via `POST /media/upload` then use the returned URL here.
          example: "https://storage.evol.app/uploads/ref-photo-123.jpg"
        faceDescription:
          type: string
          maxLength: 500
          description: |
            Text description of the ideal partner's face/appearance.
            Used for avatar generation.
          example: "Warm smile, short curly hair, friendly eyes, clean shaven"
        ethnicityId:
          type: string
          description: Ethnicity option ID from /options/ethnicities
          example: "ethnicity_asian"
        educationId:
          type: string
          description: Education option ID from /options/education-levels
          example: "education_bachelors"
        heightRange:
          type: object
          description: Height preference range in cm
          properties:
            min:
              type: integer
              minimum: 140
              maximum: 220
              description: Minimum height in cm
            max:
              type: integer
              minimum: 140
              maximum: 220
              description: Maximum height in cm
          example:
            min: 165
            max: 180
        ageRange:
          type: object
          description: Age preference range
          properties:
            min:
              type: integer
              minimum: 18
              maximum: 100
              description: Minimum age
            max:
              type: integer
              minimum: 18
              maximum: 100
              description: Maximum age
          example:
            min: 25
            max: 35
        genderPreferenceId:
          type: string
          description: |
            Gender preference option ID from /options/genders.
            Options: gender_male (men), gender_female (women), gender_non_binary (others)
          example: "gender_male"
        interestIds:
          type: array
          items:
            type: string
          minItems: 7
          description: |
            Interest option IDs from /options/interests.
            **Must select at least 7 interests.**
          example:
            - "interest_pottery"
            - "interest_art"
            - "interest_cooking"
            - "interest_hiking"
            - "interest_reading"
            - "interest_music"
            - "interest_travel"
        personality:
          type: object
          description: Personality scores (Big Five, 1-10 scale)
          properties:
            extraversion:
              type: integer
              minimum: 1
              maximum: 10
            conscientiousness:
              type: integer
              minimum: 1
              maximum: 10
            openness:
              type: integer
              minimum: 1
              maximum: 10
            agreeableness:
              type: integer
              minimum: 1
              maximum: 10
            neuroticism:
              type: integer
              minimum: 1
              maximum: 10
        values:
          type: object
          description: Values scores (1-10 scale)
          properties:
            traditionalism:
              type: integer
              minimum: 1
              maximum: 10
              description: 1=Progressive, 10=Traditional
            conformity:
              type: integer
              minimum: 1
              maximum: 10
              description: 1=Autonomous, 10=Compliant
        perfectDateId:
          type: string
          description: Perfect date option ID from /options/perfect-dates
          example: "date_cozy_time"

    VerificationError:
      type: object
      properties:
        error:
          type: string
          enum:
            - no_face_detected
            - multiple_faces
            - face_not_frontal
            - poor_lighting
            - blurry_photo
            - sunglasses_detected
            - face_obstructed
        message:
          type: string
          description: Human-readable error message

    PersonalityValidationError:
      type: object
      properties:
        error:
          type: string
          enum:
            - missing_answers
            - invalid_question_id
            - value_out_of_range
        message:
          type: string
          description: Human-readable error message
        missingQuestionIds:
          type: array
          items:
            type: string
          description: List of question IDs that were not answered
        invalidQuestionIds:
          type: array
          items:
            type: string
          description: List of invalid question IDs submitted

    # --- Profile ---
    MyProfile:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        email:
          type: string
        phoneNumber:
          type: string
        birthDate:
          type: string
          format: date
        age:
          type: integer
        gender:
          $ref: '#/components/schemas/Option'
        ethnicity:
          $ref: '#/components/schemas/Option'
        height:
          type: integer
          description: Height in cm
        education:
          $ref: '#/components/schemas/Option'
        interests:
          type: array
          items:
            $ref: '#/components/schemas/Option'
        personality:
          $ref: '#/components/schemas/PersonalityScores'
        values:
          $ref: '#/components/schemas/ValuesScores'
        perfectDate:
          $ref: '#/components/schemas/Option'
        lookingFor:
          type: array
          items:
            $ref: '#/components/schemas/Option'
        photos:
          type: array
          items:
            $ref: '#/components/schemas/Photo'
          maxItems: 6
          description: |
            User's photos (max 6). First photo (index 0) is the verified profile photo from onboarding.
            Additional photos can be added after onboarding via POST /me/photos.
        occupation:
          type: string
          description: Optional field
        religion:
          $ref: '#/components/schemas/Option'
          description: Optional field
        verified:
          type: boolean
          description: Photo has been verified via AWS Rekognition
        faceIndexed:
          type: boolean
          description: Face is indexed for matching (AWS Rekognition)
        createdAt:
          type: string
          format: date-time
        # Stats
        gemBalance:
          type: integer
        matchQuotaRemaining:
          type: integer
        matchQuotaResetAt:
          type: string
          format: date-time

    PersonalityScores:
      type: object
      description: Calculated personality trait scores (normalized 0-100)
      properties:
        extraversion:
          type: number
          format: float
          description: Extraversion score (0-100)
        conscientiousness:
          type: number
          format: float
          description: Conscientiousness score (0-100)
        openness:
          type: number
          format: float
          description: Openness score (0-100)
        agreeableness:
          type: number
          format: float
          description: Agreeableness score (0-100)
        neuroticism:
          type: number
          format: float
          description: Neuroticism score (0-100)

    ValuesScores:
      type: object
      description: Calculated values scores from assessment answers
      properties:
        scores:
          type: object
          additionalProperties:
            type: number
          description: Map of value dimension to score
          example:
            traditionalism: 65.0
            conformity: 42.5

    ProfileUpdateRequest:
      type: object
      properties:
        name:
          type: string
        ethnicityId:
          type: string
        height:
          type: integer
        educationId:
          type: string
        interestIds:
          type: array
          items:
            type: string
          minItems: 2
          maxItems: 5
        occupation:
          type: string
          description: Optional - free text
        religionId:
          type: string
          description: Optional - from /options/religions

    PreferencesUpdateRequest:
      type: object
      properties:
        perfectDateId:
          type: string
        lookingForIds:
          type: array
          items:
            type: string
          minItems: 1
        minAge:
          type: integer
          minimum: 18
          maximum: 100
        maxAge:
          type: integer
          minimum: 18
          maximum: 100

    UserProfile:
      type: object
      description: Public profile visible to other users
      properties:
        id:
          type: string
        name:
          type: string
        age:
          type: integer
        gender:
          $ref: '#/components/schemas/Option'
        ethnicity:
          $ref: '#/components/schemas/Option'
        height:
          type: integer
          description: Height in cm
        education:
          $ref: '#/components/schemas/Option'
        interests:
          type: array
          items:
            $ref: '#/components/schemas/Option'
        personality:
          $ref: '#/components/schemas/PersonalityScores'
          description: For radar chart display
        values:
          $ref: '#/components/schemas/ValuesScores'
        photos:
          type: array
          items:
            $ref: '#/components/schemas/Photo'
          maxItems: 5
        occupation:
          type: string
          description: Optional field
        religion:
          $ref: '#/components/schemas/Option'
          description: Optional field
        verified:
          type: boolean

    Photo:
      type: object
      properties:
        id:
          type: string
        url:
          type: string
          format: uri
        caption:
          type: string
        index:
          type: integer
          description: Order index (0 = cover photo, which is the verified photo)
        isVerified:
          type: boolean
          description: True for the verified photo from onboarding (cannot be deleted)
          default: false

    Preferences:
      type: object
      properties:
        perfectDate:
          $ref: '#/components/schemas/Option'
        lookingFor:
          type: array
          items:
            $ref: '#/components/schemas/Option'
        minAge:
          type: integer
        maxAge:
          type: integer

    # --- Match ---
    MatchStack:
      type: object
      properties:
        matches:
          type: array
          items:
            $ref: '#/components/schemas/MatchCard'
        quota:
          $ref: '#/components/schemas/MatchQuota'

    MatchCard:
      type: object
      properties:
        matchId:
          type: string
          description: Unique match/recommendation ID
        user:
          $ref: '#/components/schemas/MatchUserProfile'
        matching:
          $ref: '#/components/schemas/MatchingInfo'
        createdAt:
          type: string
          format: date-time

    MatchUserProfile:
      type: object
      description: User profile data shown in match card
      properties:
        id:
          type: string
        name:
          type: string
        age:
          type: integer
        photos:
          type: array
          items:
            $ref: '#/components/schemas/Photo'
          maxItems: 5
          description: Up to 5 photos
        education:
          $ref: '#/components/schemas/Option'
        height:
          type: integer
          description: Height in cm
        interests:
          type: array
          items:
            $ref: '#/components/schemas/Option'
        personality:
          $ref: '#/components/schemas/PersonalityScores'
          description: For radar chart display
        values:
          $ref: '#/components/schemas/ValuesScores'
        occupation:
          type: string
          description: Optional - shown if provided
        religion:
          $ref: '#/components/schemas/Option'
          description: Optional - shown if provided
        verified:
          type: boolean

    MatchingInfo:
      type: object
      description: |
        Matching analysis with user's ideal type.
        
        **Score Formula:**
        ```
        Total = (Personality × 35%) + (Values × 25%) + (Interests × 15%) + (Face × 25%)
        ```
        
        See RFC-003 for detailed algorithm.
      properties:
        similarityPercent:
          type: number
          format: float
          minimum: 0
          maximum: 100
          description: Overall similarity with user's ideal type (0-100%)
          example: 78.5
        indicator:
          type: string
          enum: [green, yellow, red]
          description: |
            Visual match indicator:
            - green: 66-100% (good match)
            - yellow: 35-65% (moderate match)
            - red: 0-34% (low match)
        scores:
          $ref: '#/components/schemas/MatchScoreBreakdown'
          description: Detailed score breakdown by component
        whyTheyMatch:
          type: string
          description: AI-generated writeup describing what this person and your ideal type share
          example: "Like your ideal Golden Retriever type, Alex shares a warm, caring personality with high agreeableness. You both value quality time and share interests in cooking and outdoor activities."
        sharedTraits:
          type: array
          items:
            type: string
          description: List of shared traits/interests with ideal type
          example: ["High agreeableness", "Cooking", "Outdoor activities", "Traditional values"]
    
    MatchScoreBreakdown:
      type: object
      description: |
        Detailed match score breakdown.
        Weights: Personality 35%, Values 25%, Interests 15%, Face 25%
      properties:
        personality:
          type: object
          properties:
            score:
              type: number
              format: float
              description: Personality match score (0-100)
              example: 88.0
            weight:
              type: number
              format: float
              description: Weight in final score
              example: 0.35
            details:
              type: object
              description: Per-trait differences
              properties:
                extraversion:
                  type: object
                  properties:
                    ideal:
                      type: integer
                    actual:
                      type: integer
                    diff:
                      type: integer
                conscientiousness:
                  type: object
                  properties:
                    ideal:
                      type: integer
                    actual:
                      type: integer
                    diff:
                      type: integer
                openness:
                  type: object
                  properties:
                    ideal:
                      type: integer
                    actual:
                      type: integer
                    diff:
                      type: integer
                agreeableness:
                  type: object
                  properties:
                    ideal:
                      type: integer
                    actual:
                      type: integer
                    diff:
                      type: integer
                neuroticism:
                  type: object
                  properties:
                    ideal:
                      type: integer
                    actual:
                      type: integer
                    diff:
                      type: integer
        values:
          type: object
          properties:
            score:
              type: number
              format: float
              description: Values match score (0-100)
              example: 90.0
            weight:
              type: number
              format: float
              example: 0.25
            details:
              type: object
              properties:
                traditionalism:
                  type: object
                  properties:
                    ideal:
                      type: integer
                    actual:
                      type: integer
                    diff:
                      type: integer
                conformity:
                  type: object
                  properties:
                    ideal:
                      type: integer
                    actual:
                      type: integer
                    diff:
                      type: integer
        interests:
          type: object
          properties:
            score:
              type: number
              format: float
              description: Interest overlap score (0-100)
              example: 50.0
            weight:
              type: number
              format: float
              example: 0.15
            details:
              type: object
              properties:
                idealInterests:
                  type: array
                  items:
                    type: string
                  example: ["hiking", "cooking", "travel"]
                actualInterests:
                  type: array
                  items:
                    type: string
                  example: ["hiking", "photography", "cooking", "yoga"]
                shared:
                  type: array
                  items:
                    type: string
                  example: ["hiking", "cooking"]
                overlapCount:
                  type: integer
                  example: 2
        face:
          type: object
          description: |
            Face similarity from AWS Rekognition.
            Compares candidate's face against user's ideal partner avatar.
          properties:
            score:
              type: number
              format: float
              description: Face similarity score (0-100)
              example: 75.0
            weight:
              type: number
              format: float
              example: 0.25
            available:
              type: boolean
              description: False if candidate has no indexed face or no ideal avatar
              example: true
            note:
              type: string
              description: Explanation if face score unavailable
              example: null

    MatchQuota:
      type: object
      properties:
        used:
          type: integer
          description: Matches used today
        remaining:
          type: integer
          description: Matches remaining today
        total:
          type: integer
          description: Total daily quota (default 3)
        resetAt:
          type: string
          format: date-time
          description: When quota resets (midnight HKT)
        extraPurchased:
          type: integer
          description: Extra matches purchased today

    LikeResult:
      type: object
      properties:
        matched:
          type: boolean
          description: True if mutual match
        room:
          $ref: '#/components/schemas/Conversation'
          description: Chat room if matched (null otherwise)

    QuotaError:
      type: object
      properties:
        error:
          type: string
          example: "quota_exceeded"
        message:
          type: string
          example: "Daily match quota exceeded"
        quota:
          $ref: '#/components/schemas/MatchQuota'
        gemCost:
          type: integer
          description: Gems required for 1 extra match

    # --- Conversations ---
    ConversationList:
      type: object
      properties:
        conversations:
          type: array
          items:
            $ref: '#/components/schemas/ConversationPreview'
        nextCursor:
          type: string
        hasMore:
          type: boolean

    ConversationPreview:
      type: object
      properties:
        roomId:
          type: integer
          format: int64
        partner:
          $ref: '#/components/schemas/ConversationPartner'
        lastMessage:
          $ref: '#/components/schemas/MessagePreview'
        unreadCount:
          type: integer
        expiresAt:
          type: string
          format: date-time
        isExpired:
          type: boolean
        canExtend:
          type: boolean
        extendCost:
          type: integer
          description: Gems to extend
        createdAt:
          type: string
          format: date-time
        matchedAt:
          type: string
          format: date-time

    ConversationPartner:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        coverPhoto:
          type: string
          format: uri

    Conversation:
      type: object
      properties:
        roomId:
          type: integer
          format: int64
        partner:
          $ref: '#/components/schemas/UserProfile'
        expiresAt:
          type: string
          format: date-time
        isExpired:
          type: boolean
        canExtend:
          type: boolean
        extendCost:
          type: integer
          description: Gems required to extend
        createdAt:
          type: string
          format: date-time
        matchedAt:
          type: string
          format: date-time

    MessagePreview:
      type: object
      properties:
        id:
          type: string
        type:
          type: string
          enum: [text, image, voice, system]
        preview:
          type: string
          description: "Text preview or '[Image]' / '[Voice message]'"
        sentAt:
          type: string
          format: date-time
        isFromMe:
          type: boolean

    # --- Media ---
    MediaUploadResult:
      type: object
      properties:
        url:
          type: string
          format: uri
        thumbnailUrl:
          type: string
          format: uri
          description: Only for images
        duration:
          type: integer
          description: Only for voice (auto-detected in seconds)

    ExtendRoomResult:
      type: object
      properties:
        roomId:
          type: integer
          format: int64
        newExpiresAt:
          type: string
          format: date-time
        gemsSpent:
          type: integer
        gemBalance:
          type: integer

    # --- Gems ---
    GemBalance:
      type: object
      properties:
        balance:
          type: integer
        lastUpdated:
          type: string
          format: date-time

    Product:
      type: object
      properties:
        id:
          type: string
        type:
          type: string
          enum: [gem-pack, match-quota, room-extend]
        name:
          type: string
        description:
          type: string
        gemCost:
          type: integer
          description: Cost in gems (for in-app items)
        price:
          type: number
          format: float
          description: Real money price (for gem packs)
        currency:
          type: string
        quantity:
          type: integer
          description: What you get (gems for packs, matches for quota)

    PurchaseResult:
      type: object
      properties:
        success:
          type: boolean
        gemsSpent:
          type: integer
        gemBalance:
          type: integer
        quotaAdded:
          type: integer

    GemHistoryList:
      type: object
      properties:
        transactions:
          type: array
          items:
            $ref: '#/components/schemas/GemTransaction'
        nextCursor:
          type: string
        hasMore:
          type: boolean

    GemTransaction:
      type: object
      properties:
        id:
          type: string
        type:
          type: string
          enum: [purchase, spend, reward, refund]
        amount:
          type: integer
          description: Positive for credit, negative for debit
        description:
          type: string
        createdAt:
          type: string
          format: date-time

    InsufficientGemsError:
      type: object
      properties:
        error:
          type: string
          example: "insufficient_gems"
        message:
          type: string
        required:
          type: integer
        balance:
          type: integer

    # --- Reports ---
    ReportRequest:
      type: object
      required:
        - reason
      properties:
        reason:
          type: string
          enum:
            - inappropriate-photos
            - harassment
            - spam
            - fake-profile
            - underage
            - other
        details:
          type: string
          maxLength: 1000
        messageIds:
          type: array
          items:
            type: string
          description: Optional message IDs as evidence

  securitySchemes:
    firebaseAuth:
      type: http
      scheme: bearer
      bearerFormat: Firebase ID Token
      description: |
        Firebase ID token obtained from client-side authentication.
        
        **How to obtain:**
        1. Authenticate with Firebase SDK (Google/Apple/Phone)
        2. Get ID token: `await FirebaseAuth.instance.currentUser.getIdToken()`
        3. Include in request header: `Authorization: Bearer <token>`
        
        **Token lifetime:** 1 hour (auto-refresh via Firebase SDK)

security:
  - firebaseAuth: []
