openapi: 3.1.0
info:
  title: CourseProfiler Public API
  version: 0.1.0
  summary: Composable public API for CRSProf, USRProf, estimation, and race-plan PDF workflows.
  description: |
    Public composable API for CRSProf/USRProf race-plan workflows.
    Endpoints are artifact/job based so external clients and AI agents can build race-plan workflows without browser automation.
    AI agents should ask the user for explicit permission before making POST, DELETE, or other state-changing requests. Read-only GET requests may be used for discovery and status checks.
servers:
  - url: https://courseprofiler.com
    description: Production origin
  - url: http://localhost:8080
    description: Local backend

security:
  - ApiKeyAuth: []
  - BearerAuth: []
  - {}

tags:
  - name: Jobs
  - name: Artifacts
  - name: Uploads
  - name: RacePlans
  - name: CRSProf
  - name: USRProf
  - name: Estimates
  - name: Exports
  - name: Metadata

paths:
  /api/jobs/{job_id}:
    get:
      tags: [Jobs]
      summary: Get async job status
      operationId: getJob
      x-openai-isConsequential: false
      parameters:
        - $ref: '#/components/parameters/JobId'
      responses:
        '200':
          description: Job status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Job'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/artifacts/{artifact_id}:
    get:
      tags: [Artifacts]
      summary: Get artifact metadata and a private pre-signed download URL
      operationId: getArtifact
      x-openai-isConsequential: false
      parameters:
        - $ref: '#/components/parameters/ArtifactId'
      responses:
        '200':
          description: Artifact metadata
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Artifact'
        '404':
          $ref: '#/components/responses/NotFound'
        '410':
          description: Artifact expired
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
    delete:
      tags: [Artifacts]
      summary: Delete an artifact before its normal expiry
      operationId: deleteArtifact
      x-openai-isConsequential: true
      parameters:
        - $ref: '#/components/parameters/ArtifactId'
      responses:
        '204':
          description: Artifact deleted or already unavailable
        '404':
          $ref: '#/components/responses/NotFound'

  /api/artifact-uploads:
    post:
      tags: [Uploads]
      summary: Create a direct upload session for a local file
      description: |
        Upload bridge for clients and AI assistants with local/sandbox files. Creates a short-lived pre-signed upload target. Complete the session to register an opaque source_file artifact ID.
      operationId: createArtifactUpload
      x-openai-isConsequential: true
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ArtifactUploadCreateRequest'
      responses:
        '201':
          description: Upload session created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ArtifactUploadSession'
        '400':
          $ref: '#/components/responses/BadRequest'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '429':
          $ref: '#/components/responses/RateLimited'

  /api/artifact-uploads/{upload_id}:
    get:
      tags: [Uploads]
      summary: Get upload session status
      operationId: getArtifactUpload
      x-openai-isConsequential: false
      parameters:
        - $ref: '#/components/parameters/UploadId'
      responses:
        '200':
          description: Upload session status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ArtifactUploadSession'
        '404':
          $ref: '#/components/responses/NotFound'
        '410':
          description: Upload session expired
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
    delete:
      tags: [Uploads]
      summary: Cancel an incomplete upload session
      operationId: cancelArtifactUpload
      x-courseprofiler-status: proposed
      x-openai-isConsequential: true
      parameters:
        - $ref: '#/components/parameters/UploadId'
      responses:
        '204':
          description: Upload session cancelled or already unavailable
        '404':
          $ref: '#/components/responses/NotFound'

  /api/artifact-uploads/{upload_id}/complete:
    post:
      tags: [Uploads]
      summary: Complete an upload session and register a source_file artifact
      operationId: completeArtifactUpload
      x-openai-isConsequential: true
      parameters:
        - $ref: '#/components/parameters/UploadId'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ArtifactUploadCompleteRequest'
      responses:
        '201':
          description: Uploaded file registered as an artifact
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Artifact'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '410':
          description: Upload session expired
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '429':
          $ref: '#/components/responses/RateLimited'

  /api/race-plans:
    post:
      tags: [RacePlans]
      summary: Orchestrate course import, segmentation, runner import, estimation, and optional PDF export
      description: |
        Optional convenience workflow for AI agents and simpler API clients. Creates one top-level async job that composes the CRSProf import, segment generation, USRProf import, estimate, and race-plan PDF export capabilities. Atomic endpoints remain the canonical API surface.

        Personalized race plans require runner input. CourseProfiler does not create meaningful pacing estimates from a course alone. Before calling this endpoint, make sure the user provides one runner input: a USRProf artifact ID, a public HTTPS URL to a USRProf file, local GPX/FIT/CRSProf evidence files uploaded through /api/artifact-uploads then imported with /api/usrprof/imports, public HTTPS URLs to runner evidence files, or a USRProf profile created through CourseProfiler's Strava import feature. Strava import requires browser-based user authentication and consent; direct the user to authenticate in the CourseProfiler web app, then use the resulting USRProf artifact/profile when available. If no runner profile or evidence is available, import/analyze the course only and ask the user for runner evidence before creating a race plan.
      operationId: createRacePlan
      x-openai-isConsequential: true
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RacePlanCreateRequest'
      responses:
        '202':
          $ref: '#/components/responses/AcceptedJob'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'

  /api/crsprof/imports:
    post:
      tags: [CRSProf]
      summary: Import or create a CRSProf artifact
      description: |
        Async operation. Creates a CRSProf artifact from GPX, FIT, CRSProf, public HTTPS URL, existing import/share ID, or raw CRSProf JSON.
      operationId: createCrsprofImport
      x-openai-isConsequential: true
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CrsprofImportRequest'
      responses:
        '202':
          $ref: '#/components/responses/AcceptedJob'
        '400':
          $ref: '#/components/responses/BadRequest'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '429':
          $ref: '#/components/responses/RateLimited'

  /api/crsprof/segments:
    post:
      tags: [CRSProf]
      summary: Generate or update CRSProf segments
      description: Async when generation or optimization is requested. Produces an updated CRSProf artifact.
      operationId: createCrsprofSegments
      x-openai-isConsequential: true
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CrsprofSegmentsRequest'
      responses:
        '202':
          $ref: '#/components/responses/AcceptedJob'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'

  /api/crsprof/waypoints:
    post:
      tags: [CRSProf]
      summary: Enrich a CRSProf artifact with waypoint/resource data
      description: |
        Async operation. Applies structured waypoint/resource/cutoff data or parsed aid-station table text to an existing CRSProf artifact and returns an enriched CRSProf artifact. Use this before segment generation and race-plan estimation when official aid stations, water/food/crew resources, cutoffs, or split separators are available.
      operationId: createCrsprofWaypointEnrichment
      x-openai-isConsequential: true
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CrsprofWaypointsRequest'
      responses:
        '202':
          $ref: '#/components/responses/AcceptedJob'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'

  /api/crsprof/validate:
    post:
      tags: [CRSProf]
      summary: Validate a CRSProf artifact or payload
      operationId: validateCrsprof
      x-openai-isConsequential: true
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CrsprofValidateRequest'
      responses:
        '200':
          description: Validation result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationResult'
        '400':
          $ref: '#/components/responses/BadRequest'

  /api/usrprof/imports:
    post:
      tags: [USRProf]
      summary: Import or create a USRProf artifact
      operationId: createUsrprofImport
      x-openai-isConsequential: true
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UsrprofImportRequest'
      responses:
        '202':
          $ref: '#/components/responses/AcceptedJob'
        '400':
          $ref: '#/components/responses/BadRequest'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '429':
          $ref: '#/components/responses/RateLimited'

  /api/usrprof/validate:
    post:
      tags: [USRProf]
      summary: Validate a USRProf artifact or payload
      operationId: validateUsrprof
      x-openai-isConsequential: true
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UsrprofValidateRequest'
      responses:
        '200':
          description: Validation result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationResult'
        '400':
          $ref: '#/components/responses/BadRequest'

  /api/estimates:
    post:
      tags: [Estimates]
      summary: Compatibility alias for segment-evidence estimation
      description: Backward-compatible alias for POST /api/estimates/segment-evidence. New clients should use the model-specific endpoint names.
      operationId: createEstimate
      x-openai-isConsequential: true
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EstimateRequest'
      responses:
        '202':
          $ref: '#/components/responses/AcceptedJob'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'

  /api/estimates/segment-evidence:
    post:
      tags: [Estimates]
      summary: Estimate plan times using the segment-evidence model
      description: Async operation. Uses CRSProf and USRProf artifacts to produce an updated planned CRSProf artifact and optional summary JSON artifact.
      operationId: createSegmentEvidenceEstimate
      x-openai-isConsequential: true
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EstimateRequest'
      responses:
        '202':
          $ref: '#/components/responses/AcceptedJob'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'

  /api/estimates/segment-evidence/selected-segments:
    post:
      tags: [Estimates]
      summary: Estimate selected target segments using the segment-evidence model
      description: Responsive operation for interactive UI use. Accepts selected segment targets plus either raw USRProf JSON or a USRProf artifact ID, and returns estimates/rejections with diagnostics directly.
      operationId: estimateSegmentEvidenceSelectedSegments
      x-openai-isConsequential: true
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SelectedSegmentEvidenceEstimateRequest'
      responses:
        '200':
          description: Selected target estimates and rejected targets
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SelectedSegmentEvidenceEstimateResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'

  /api/estimates/fatigue-form:
    post:
      tags: [Estimates]
      summary: Estimate plan times using the fatigue/form model
      description: Async operation. Uses CRSProf options.estimation fatigue/form settings to produce an updated planned CRSProf artifact.
      operationId: createFatigueFormEstimate
      x-openai-isConsequential: true
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FatigueFormEstimateRequest'
      responses:
        '202':
          $ref: '#/components/responses/AcceptedJob'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'

  /api/estimates/fatigue-form/selected-segments:
    post:
      tags: [Estimates]
      summary: Estimate selected target segments using the fatigue/form model
      description: Responsive operation for interactive UI use. Runs the fatigue/form model over the full CRSProf to preserve fatigue progression, then returns a CRSProf-shaped payload with newly estimated plan overlays only on the selected segments.
      operationId: estimateFatigueFormSelectedSegments
      x-openai-isConsequential: true
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SelectedFatigueFormEstimateRequest'
      responses:
        '200':
          description: CRSProf-shaped payload with selected segment plan overlays updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CrsprofDocument'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'

  /api/exports/race-plan-pdf:
    post:
      tags: [Exports]
      summary: Export a prepared CRSProf artifact as a race-plan PDF
      description: Async operation. Produces a PDF artifact matching the current browser hi-fi PDF.
      operationId: createRacePlanPdfExport
      x-openai-isConsequential: true
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RacePlanPdfExportRequest'
      responses:
        '202':
          $ref: '#/components/responses/AcceptedJob'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'

  /api/crsprof/schemas:
    get:
      tags: [Metadata]
      summary: List CRSProf schemas
      operationId: listCrsprofSchemas
      x-openai-isConsequential: false
      responses:
        '200':
          description: Schema list
          content:
            application/json:
              schema:
                type: object
  /api/crsprof/schemas/{version}:
    get:
      tags: [Metadata]
      summary: Get CRSProf schema metadata
      operationId: getCrsprofSchemaMetadata
      x-openai-isConsequential: false
      parameters:
        - name: version
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Schema metadata
          content:
            application/json:
              schema:
                type: object
  /api/crsprof/schemas/{version}/json-schema:
    get:
      tags: [Metadata]
      summary: Get CRSProf JSON Schema
      operationId: getCrsprofJsonSchema
      x-openai-isConsequential: false
      parameters:
        - name: version
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: JSON Schema
          content:
            application/json:
              schema:
                type: object
  /api/crsprof/interpretations:
    get:
      tags: [Metadata]
      summary: List CRSProf semantic interpretations
      operationId: listCrsprofInterpretations
      x-openai-isConsequential: false
      responses:
        '200':
          description: Interpretation list
          content:
            application/json:
              schema:
                type: object
  /api/crsprof/interpretations/{profile}:
    get:
      tags: [Metadata]
      summary: Get CRSProf semantic interpretation
      operationId: getCrsprofInterpretation
      x-openai-isConsequential: false
      parameters:
        - name: profile
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Interpretation metadata
          content:
            application/json:
              schema:
                type: object

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: Optional API key. Invalid keys return 401; absent keys use unauthenticated public quotas.
    BearerAuth:
      type: http
      scheme: bearer
      description: Optional bearer API key. Invalid tokens return 401; absent tokens use unauthenticated public quotas.

  parameters:
    JobId:
      name: job_id
      in: path
      required: true
      schema:
        type: string
        pattern: '^job_[A-Za-z0-9_-]+$'
    ArtifactId:
      name: artifact_id
      in: path
      required: true
      schema:
        type: string
        pattern: '^art_[A-Za-z0-9_-]+$'
    UploadId:
      name: upload_id
      in: path
      required: true
      schema:
        type: string
        pattern: '^upl_[A-Za-z0-9_-]+$'

  responses:
    AcceptedJob:
      description: Async job accepted
      headers:
        Location:
          description: URL of the job status resource
          schema:
            type: string
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Job'
    BadRequest:
      description: Invalid request
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    PayloadTooLarge:
      description: Payload too large
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    RateLimited:
      description: Rate limit exceeded
      headers:
        Retry-After:
          schema:
            type: integer
          description: Seconds before retry
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'

  schemas:
    Job:
      type: object
      required: [job_id, status, status_url, created_at, expires_at, result_artifacts, warnings, errors]
      properties:
        job_id:
          type: string
          examples: [job_01JABCDEF]
        status:
          $ref: '#/components/schemas/JobStatus'
        status_url:
          type: string
          examples: [/api/jobs/job_01JABCDEF]
        created_at:
          type: string
          format: date-time
        started_at:
          type: string
          format: date-time
        completed_at:
          type: string
          format: date-time
        expires_at:
          type: string
          format: date-time
        result_artifacts:
          type: array
          items:
            $ref: '#/components/schemas/Artifact'
        warnings:
          type: array
          items:
            $ref: '#/components/schemas/ApiProblem'
        errors:
          type: array
          items:
            $ref: '#/components/schemas/ApiProblem'
        progress:
          type: object
          additionalProperties: true

    JobStatus:
      type: string
      enum: [queued, running, succeeded, failed, expired, cancelled]

    Artifact:
      type: object
      required: [artifact_id, type, content_type, created_at, expires_at]
      properties:
        artifact_id:
          type: string
          examples: [art_01JABCDEF]
        type:
          $ref: '#/components/schemas/ArtifactType'
        content_type:
          type: string
          examples: [application/vnd.courseprof+json, application/vnd.usrprof+json, application/pdf]
        file_name:
          type: string
        size_bytes:
          type: integer
          minimum: 0
        download_url:
          type: string
          format: uri
          description: Private pre-signed URL. May be omitted for expired artifacts.
        download_url_expires_at:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time
        expires_at:
          type: string
          format: date-time
        metadata:
          type: object
          additionalProperties: true
        warnings:
          type: array
          items:
            $ref: '#/components/schemas/ApiProblem'

    ArtifactType:
      type: string
      enum: [source_file, crsprof, usrprof, pdf, json, diagnostics]

    ArtifactUploadCreateRequest:
      type: object
      required: [file_name, content_type, size_bytes, purpose]
      additionalProperties: false
      properties:
        file_name:
          type: string
          minLength: 1
          maxLength: 255
          examples: [course.gpx]
        content_type:
          type: string
          examples: [application/gpx+xml, application/octet-stream, application/vnd.courseprof+json]
        size_bytes:
          type: integer
          minimum: 1
          maximum: 26214400
        purpose:
          $ref: '#/components/schemas/ArtifactUploadPurpose'
          description: Use `course_source` for course files, `runner_evidence` for activity/evidence files, and `usrprof_source` for local `.usrprof` profile files.
        checksum:
          $ref: '#/components/schemas/ArtifactUploadChecksum'
        metadata:
          type: object
          additionalProperties: true

    ArtifactUploadCompleteRequest:
      type: object
      additionalProperties: false
      properties:
        checksum:
          $ref: '#/components/schemas/ArtifactUploadChecksum'

    ArtifactUploadChecksum:
      type: object
      required: [algorithm, value]
      additionalProperties: false
      properties:
        algorithm:
          type: string
          enum: [sha256]
        value:
          type: string
          pattern: '^[a-fA-F0-9]{64}$'

    ArtifactUploadPurpose:
      type: string
      description: Upload purpose. Use `usrprof_source` when uploading an existing local `.usrprof` runner profile; completed `source_file` artifacts can then be passed through `runner.sources` or `/api/usrprof/imports`.
      enum: [course_source, runner_evidence, usrprof_source, generic_source]

    ArtifactUploadSession:
      type: object
      required: [upload_id, status, expires_at]
      properties:
        upload_id:
          type: string
          examples: [upl_01JABCDEF]
        status:
          $ref: '#/components/schemas/ArtifactUploadStatus'
        upload:
          $ref: '#/components/schemas/ArtifactUploadTarget'
        complete_url:
          type: string
          examples: [/api/artifact-uploads/upl_01JABCDEF/complete]
        artifact_id:
          type: string
          description: Present after completion.
        created_at:
          type: string
          format: date-time
        expires_at:
          type: string
          format: date-time
        warnings:
          type: array
          items:
            $ref: '#/components/schemas/ApiProblem'

    ArtifactUploadStatus:
      type: string
      enum: [waiting_for_upload, uploaded, completed, failed, expired, cancelled]

    ArtifactUploadTarget:
      type: object
      required: [method, url, expires_at, max_size_bytes]
      properties:
        method:
          type: string
          enum: [PUT]
        url:
          type: string
          format: uri
          description: Private pre-signed direct-upload URL.
        headers:
          type: object
          description: Headers that must be replayed exactly on the direct PUT upload request. Missing or changed headers can invalidate the pre-signed URL.
          additionalProperties:
            type: string
        expires_at:
          type: string
          format: date-time
        max_size_bytes:
          type: integer
          minimum: 1

    ApiProblem:
      type: object
      required: [code, message, severity]
      properties:
        code:
          type: string
          examples: [missing_elevation, payload_too_large, rate_limited]
        message:
          type: string
        severity:
          type: string
          enum: [info, warning, error]
        field:
          type: string
        details:
          type: object
          additionalProperties: true

    ErrorEnvelope:
      type: object
      required: [errors]
      properties:
        errors:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/ApiProblem'
        warnings:
          type: array
          items:
            $ref: '#/components/schemas/ApiProblem'
        request_id:
          type: string

    InputSource:
      type: object
      required: [kind]
      discriminator:
        propertyName: kind
      oneOf:
        - $ref: '#/components/schemas/ArtifactInputSource'
        - $ref: '#/components/schemas/UrlInputSource'
        - $ref: '#/components/schemas/RawJsonInputSource'
        - $ref: '#/components/schemas/ImportIdInputSource'

    ArtifactInputSource:
      type: object
      required: [kind, artifact_id]
      properties:
        kind:
          type: string
          const: artifact
        artifact_id:
          type: string

    UrlInputSource:
      type: object
      required: [kind, url]
      properties:
        kind:
          type: string
          const: url
        url:
          type: string
          format: uri

    RawJsonInputSource:
      type: object
      required: [kind, payload]
      properties:
        kind:
          type: string
          const: raw_json
        payload:
          type: object
          additionalProperties: true

    ImportIdInputSource:
      type: object
      required: [kind, import_id]
      properties:
        kind:
          type: string
          const: import_id
        import_id:
          type: string

    CrsprofImportOptions:
      type: object
      properties:
        mode:
          type: string
          enum: [profile, execution, preserve]
        include_segments:
          type: boolean
        analysis_options:
          type: object
          additionalProperties: true

    CrsprofSegmentOptions:
      type: object
      additionalProperties: false
      properties:
        algorithm:
          type: string
          enum: [GREEDY, SLOPE_BASED, PACE_BASED]
        flat_grade:
          type: number
          description: Flat grade threshold in percent.
        slope_change_threshold:
          type: number
          description: Slope-change threshold in percent.
        flat_max_delta:
          type: number
          description: Optional maximum elevation delta for flat segments, in meters.
        minimum_distance:
          type: number
          description: Minimum segment distance in meters.
        flexible_minimum_distance:
          type: boolean
        flexible_flat_max_delta:
          type: boolean
        instant_mode:
          type: boolean
        include_routes:
          type: boolean

    UsrprofImportOptions:
      type: object
      additionalProperties: false
      properties:
        file_name:
          type: string

    EstimateOptions:
      type: object
      additionalProperties: false
      properties:
        strategy:
          type: string
          enum: [segment_evidence]
          default: segment_evidence
        gap_algorithm:
          type: string
          enum: [POLYNOMIAL, CONSTANT_POWER, MINETTI]
        flat_grade:
          type: number
          description: Optional flat-grade threshold passed to segment-evidence locomotion planning.

    EstimateOutputOptions:
      type: object
      properties:
        include_summary_artifact:
          type: boolean
          default: true

    RacePlanPdfOptions:
      type: object
      properties:
        title:
          type: string
        subtitle:
          type: string
        unit_system:
          type: string
          enum: [metric, imperial]
        file_name:
          type: string

    RacePlanCreateRequest:
      type: object
      required: [course, runner]
      properties:
        course:
          $ref: '#/components/schemas/RacePlanCourseInput'
        runner:
          $ref: '#/components/schemas/RacePlanRunnerInput'
        estimate:
          $ref: '#/components/schemas/RacePlanEstimateInput'
        pdf:
          $ref: '#/components/schemas/RacePlanPdfInput'

    RacePlanCourseInput:
      type: object
      description: Exactly one of source or crsprof_artifact_id should be supplied.
      oneOf:
        - required: [source]
        - required: [crsprof_artifact_id]
      properties:
        source:
          $ref: '#/components/schemas/InputSource'
        crsprof_artifact_id:
          type: string
        import_options:
          $ref: '#/components/schemas/CrsprofImportOptions'
        waypoints:
          description: Optional waypoint/resource enrichment to apply before segment generation. Use this for official aid stations, water/food/crew resources, cutoffs, and course points.
          oneOf:
            - $ref: '#/components/schemas/StructuredWaypointEnrichment'
            - $ref: '#/components/schemas/TableWaypointEnrichment'
        segments:
          $ref: '#/components/schemas/RacePlanSegmentsInput'

    RacePlanSegmentsInput:
      type: object
      properties:
        mode:
          type: string
          enum: [generate, skip]
          default: generate
        options:
          $ref: '#/components/schemas/CrsprofSegmentOptions'

    RacePlanRunnerInput:
      type: object
      description: Exactly one of usrprof_artifact_id or sources should be supplied. Use usrprof_artifact_id for an existing USRProf artifact/profile. Use sources for public HTTPS runner evidence/profile URLs or source_file artifacts created via /api/artifact-uploads. Strava-based profiles must be created after browser-based user authentication in the CourseProfiler web app, then supplied as a USRProf artifact/profile.
      oneOf:
        - required: [usrprof_artifact_id]
        - required: [sources]
      properties:
        usrprof_artifact_id:
          type: string
        sources:
          type: array
          minItems: 1
          items:
            oneOf:
              - $ref: '#/components/schemas/ArtifactInputSource'
              - $ref: '#/components/schemas/UrlInputSource'
        import_options:
          $ref: '#/components/schemas/UsrprofImportOptions'

    RacePlanEstimateInput:
      type: object
      properties:
        options:
          $ref: '#/components/schemas/EstimateOptions'
        output:
          $ref: '#/components/schemas/EstimateOutputOptions'

    RacePlanPdfInput:
      type: object
      properties:
        enabled:
          type: boolean
          default: true
        options:
          $ref: '#/components/schemas/RacePlanPdfOptions'

    CrsprofImportRequest:
      type: object
      required: [source]
      properties:
        source:
          $ref: '#/components/schemas/InputSource'
        options:
          type: object
          properties:
            mode:
              type: string
              enum: [profile, execution, preserve]
            include_segments:
              type: boolean
            analysis_options:
              type: object
              additionalProperties: true

    CrsprofSegmentsRequest:
      type: object
      required: [crsprof_artifact_id]
      properties:
        crsprof_artifact_id:
          type: string
        mode:
          type: string
          enum: [generate]
          default: generate
        options:
          type: object
          additionalProperties: false
          properties:
            algorithm:
              type: string
              enum: [GREEDY, SLOPE_BASED, PACE_BASED]
            flat_grade:
              type: number
              description: Flat grade threshold in percent.
            slope_change_threshold:
              type: number
              description: Slope-change threshold in percent.
            flat_max_delta:
              type: number
              description: Optional maximum elevation delta for flat segments, in meters.
            minimum_distance:
              type: number
              description: Minimum segment distance in meters.
            flexible_minimum_distance:
              type: boolean
            flexible_flat_max_delta:
              type: boolean
            instant_mode:
              type: boolean
            include_routes:
              type: boolean

    CrsprofWaypointsRequest:
      type: object
      required: [course, waypoints]
      properties:
        course:
          type: object
          required: [crsprof_artifact_id]
          properties:
            crsprof_artifact_id:
              type: string
              description: Existing CRSProf artifact to enrich.
        waypoints:
          oneOf:
            - $ref: '#/components/schemas/StructuredWaypointEnrichment'
            - $ref: '#/components/schemas/TableWaypointEnrichment'

    StructuredWaypointEnrichment:
      type: object
      required: [items]
      properties:
        mode:
          type: string
          enum: [structured]
          default: structured
        distance_unit:
          type: string
          enum: [m, km, mi]
          default: km
        items:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/WaypointItem'

    TableWaypointEnrichment:
      type: object
      required: [mode]
      properties:
        mode:
          type: string
          enum: [table]
        text:
          type: string
          description: Copied official aid-station/course-point table text. Provide either text or table_url.
        table_url:
          type: string
          format: uri
          description: Public HTTPS URL for an official aid-station/course-point table, HTML page, or PDF. Provide either text or table_url.
        source_ref:
          type: string
          description: Optional source URL/page/table reference.
        start_time:
          type: string
          description: Optional race start time used when parsing clock-based cutoffs.

    WaypointItem:
      type: object
      required: [name, distance]
      properties:
        name:
          type: string
        distance:
          type: number
          minimum: 0
        distance_unit:
          type: string
          enum: [m, km, mi]
        kind:
          type: string
          enum: [start, finish, aid, checkpoint, water, crew, other]
        resources:
          type: array
          items:
            type: string
            enum: [water, water_source, drinks, fruit, food, toilet, medic, chrono, crew, pacer, car, bus, ambulance, shuttle, drop_bag, withdrawal, binoculars, hot_meal, tent, showers, changing_room, physiotherapy, rest_area, start_flag, finish_flag]
          uniqueItems: true
        cutoff:
          type: string
          pattern: '^\\d{1,3}:\\d{2}$'
          description: Total elapsed cutoff in HH:MM. HH may be greater than 24.
        cutoff_kind:
          type: string
          enum: [enter, leave]
        split_separator:
          type: boolean
          description: Reserved for explicit split separator behavior; current enrichment preserves existing CourseProfiler semantics.
        notes:
          type: string
        source_text:
          type: string
        source_ref:
          type: string

    CrsprofValidateRequest:
      type: object
      oneOf:
        - required: [crsprof_artifact_id]
        - required: [crsprof]
      properties:
        crsprof_artifact_id:
          type: string
        crsprof:
          type: object
          additionalProperties: true

    UsrprofImportRequest:
      type: object
      required: [sources]
      properties:
        sources:
          type: array
          minItems: 1
          items:
            oneOf:
              - $ref: '#/components/schemas/ArtifactInputSource'
              - $ref: '#/components/schemas/UrlInputSource'
          description: First implementation slice supports CRSProf/USRProf artifact IDs and public HTTPS URLs for supported evidence files.
        options:
          type: object
          additionalProperties: false
          properties:
            file_name:
              type: string

    UsrprofValidateRequest:
      type: object
      oneOf:
        - required: [usrprof_artifact_id]
        - required: [usrprof]
      properties:
        usrprof_artifact_id:
          type: string
        usrprof:
          type: object
          additionalProperties: true

    EstimateRequest:
      type: object
      required: [crsprof_artifact_id, usrprof_artifact_id]
      properties:
        crsprof_artifact_id:
          type: string
        usrprof_artifact_id:
          type: string
        options:
          type: object
          additionalProperties: false
          properties:
            strategy:
              type: string
              enum: [segment_evidence]
              default: segment_evidence
            gap_algorithm:
              type: string
              enum: [POLYNOMIAL, CONSTANT_POWER, MINETTI]
            flat_grade:
              type: number
              description: Optional flat-grade threshold passed to segment-evidence locomotion planning.
        output:
          type: object
          properties:
            include_summary_artifact:
              type: boolean
              default: true

    FatigueFormEstimateRequest:
      type: object
      oneOf:
        - required: [crsprof_artifact_id]
        - required: [crsprof]
      properties:
        crsprof_artifact_id:
          type: string
          description: CRSProf artifact with options.estimation fatigue/form settings.
        crsprof:
          type: object
          additionalProperties: true
          description: Inline CRSProf payload with options.estimation fatigue/form settings.
        options:
          type: object
          additionalProperties: true
          description: Optional CRSProf options object to merge before estimation.
        output:
          type: object
          properties:
            include_summary_artifact:
              type: boolean
              default: false

    SelectedSegmentEvidenceEstimateRequest:
      type: object
      required: [segments]
      oneOf:
        - required: [usrprof_artifact_id]
        - required: [usrprof]
      properties:
        usrprof_artifact_id:
          type: string
          description: USRProf artifact to use as evidence source.
        usrprof:
          type: object
          additionalProperties: true
          description: Inline USRProf payload to use as evidence source.
        segments:
          type: array
          items:
            type: object
            additionalProperties: true
          description: Ordered selected targets with segment_index, geometry, horizon, optional start_state, and estimate flag.
        options:
          type: object
          additionalProperties: true
          description: Optional analysis settings such as gap_algorithm and flat_grade under options.analysis.

    SelectedFatigueFormEstimateRequest:
      type: object
      required: [segments]
      oneOf:
        - required: [crsprof_artifact_id]
        - required: [crsprof]
      properties:
        crsprof_artifact_id:
          type: string
          description: CRSProf artifact with options.estimation fatigue/form settings.
        crsprof:
          type: object
          additionalProperties: true
          description: Inline CRSProf payload with options.estimation fatigue/form settings.
        segments:
          type: array
          minItems: 1
          items:
            type: object
            properties:
              segment_index:
                type: integer
                minimum: 0
              index:
                type: integer
                minimum: 0
            additionalProperties: true
          description: Selected targets by canonical segment_index or index. The model runs over the full course; only selected segment plan overlays are returned as newly updated.
        options:
          type: object
          additionalProperties: true
          description: Optional CRSProf options object to merge before estimation.

    SelectedSegmentEvidenceEstimateResponse:
      type: object
      required: [estimates, rejected]
      properties:
        estimates:
          type: array
          items:
            type: object
            additionalProperties: true
        rejected:
          type: array
          items:
            type: object
            additionalProperties: true

    RacePlanPdfExportRequest:
      type: object
      required: [crsprof_artifact_id]
      properties:
        crsprof_artifact_id:
          type: string
        options:
          type: object
          properties:
            title:
              type: string
            subtitle:
              type: string
            unit_system:
              type: string
              enum: [metric, imperial]
            file_name:
              type: string

    ValidationResult:
      type: object
      required: [valid, warnings, errors]
      properties:
        valid:
          type: boolean
        artifact:
          $ref: '#/components/schemas/Artifact'
        warnings:
          type: array
          items:
            $ref: '#/components/schemas/ApiProblem'
        errors:
          type: array
          items:
            $ref: '#/components/schemas/ApiProblem'

x-courseprofiler-operational-defaults:
  artifact_retention: PT24H
  presigned_download_url_ttl: PT15M
  public_url_fetch_timeout: PT10S
  public_url_max_redirects: 4
  unauthenticated_concurrent_jobs_per_ip: 2
  unauthenticated_queued_jobs_per_ip: 10
  unauthenticated_requests_per_minute_per_ip: 60
  unauthenticated_requests_per_hour_per_ip: 1000
  authenticated_requests_per_minute_per_api_key: 1200
  authenticated_requests_per_hour_per_api_key: 10000
  upload_size_limit_bytes: 26214400
  json_body_size_limit_bytes: 10485760
