openapi: 3.0.3
info:
  title: ScrambleSync API
  version: "1.0.0"
  description: |
    The ScrambleSync REST API lets your systems read and manage tournament data —
    contacts & tags, tournaments, teams, the live leaderboard, registrations,
    invitations, webhooks, and more — over OAuth 2.0.

    ## Quickstart

    **1. Create an API client.** In the dashboard go to **Settings → API access**
    (`/settings/api`), name a client, choose its scopes, and copy the **client secret**
    (shown once).

    **2. Get an access token** (OAuth 2.0 *client credentials*). Tokens are opaque,
    last 1 hour, and are revocable.
    ```bash
    curl -s -X POST https://scramblesync.com/api/v1/oauth/token \
      -u "$CLIENT_ID:$CLIENT_SECRET" \
      -H "Content-Type: application/json" \
      -d '{"grant_type":"client_credentials"}'
    # → { "access_token": "sat_…", "token_type": "Bearer", "expires_in": 3600, "scope": "contacts:read …" }
    ```

    **3. Call the API** with the token as a Bearer credential:
    ```bash
    curl -s https://scramblesync.com/api/v1/contacts?limit=50 \
      -H "Authorization: Bearer $ACCESS_TOKEN"
    ```
    ```js
    const r = await fetch("https://scramblesync.com/api/v1/contacts?limit=50", {
      headers: { Authorization: `Bearer ${accessToken}` },
    });
    const { data, page } = await r.json();
    ```

    ## Scopes

    A token only carries the scopes its client was granted (and that you request).

    | Scope | Grants |
    |-------|--------|
    | `contacts:read`       | Read contacts & tags |
    | `contacts:write`      | Create, edit, import & delete contacts & tags |
    | `registrations:read`  | Read & export registrations |
    | `registrations:write` | Refund & cancel registrations (issues Stripe refunds) |
    | `invitations:read`    | Read invitations & delivery stats |
    | `invitations:write`   | Create & schedule invitations |
    | `tournaments:read`    | Read tournaments, teams & leaderboard |
    | `tournaments:write`   | Edit event details and change event status (Draft / Open / Live / Final / Archived) |
    | `webhooks:read`       | Read webhook endpoints & deliveries |
    | `webhooks:write`      | Create, edit, delete & replay webhooks |
    | `teams:write`         | Move or remove teams, assign flights (live day) |
    | `checkin:write`       | Check teams & players in |
    | `messages:write`      | Send broadcast messages |
    | `scores:write`        | Correct team & player hole scores |
    | `sidegames:write`     | Add, edit & remove side games / contests |
    | `players:write`       | Edit player details and withdraw players |
    | `brackets:read`       | Read match-play brackets and match results |
    | `brackets:write`      | Record match-play results & advance brackets |
    | `raffles:read`        | Read raffles, prizes & ticket counts |
    | `raffles:write`       | Draw raffle winners |
    | `auctions:read`       | Read auction items & current bids |
    | `announcements:read`  | Read announcements & delivery status |
    | `announcements:write` | Create & send announcements |
    | `rsvps:read`          | Read RSVP responses |
    | `series:read`         | Read series / season standings |
    | `scorecards:read`     | Read per-team hole-by-hole scorecards |
    | `flights:read`        | Read per-flight standings |
    | `audit:read`          | Read the tournament audit log |

    Request a subset at token time with a space-delimited `scope` parameter; omit it to
    get all of the client's scopes. A call missing the required scope returns
    `403 insufficient_scope`.

    ## Conventions

    - **Base URL:** `https://scramblesync.com/api/v1`
    - **Tenancy:** every client belongs to one organization; tokens only ever see that
      org's data.
    - **Pagination:** list endpoints are cursor-paginated. Responses are
      `{ "data": [...], "page": { "next_cursor": "…"|null, "has_more": true|false } }`.
      Pass `?cursor=<next_cursor>&limit=<1–200>` for the next page.
    - **Errors:** non-2xx responses share one shape —
      `{ "error": "message", "code": "snake_case", "request_id": "uuid" }`. Every
      response carries an `X-Request-Id` header (quote it in support requests).
      Common codes: `invalid_token` (401), `insufficient_scope` (403), `not_found`
      (404), `validation_error` (400), `conflict` (409), `rate_limited` (429),
      `bad_request` (400), `forbidden` (403), `internal_error` (500),
      `service_unavailable` (503).
    - **Idempotency:** `POST` requests accept an `Idempotency-Key` header (≤128 chars).
      Replaying the same key returns the original result; reusing a key with a different
      body returns `409 conflict`.
    - **Rate limiting:** 600 requests/minute per client (keyed by `client_id`). Over the
      limit returns `429` with a `Retry-After` header (seconds).
    - **CSV exports:** certain GET endpoints accept `?format=csv` and return `text/csv`
      with `Content-Disposition: attachment`. If the result was truncated an
      `X-Truncated: true` header is added.

    Token authentication failures on the token endpoint use the OAuth 2.0 error shape
    (`{ "error": "invalid_client", "error_description": "…" }`); all other endpoints use
    the `{ error, code, request_id }` envelope above.

    ## Webhooks

    Outbound webhooks let ScrambleSync push events to your own HTTPS endpoint in real
    time so you don't need to poll the API.

    ### Creating an endpoint

    From the dashboard go to **Settings → API access** (`/settings/api`) and click
    **Add endpoint**, or call `POST /api/v1/webhooks` with scope `webhooks:write`:

    ```bash
    curl -s -X POST https://scramblesync.com/api/v1/webhooks \
      -H "Authorization: Bearer $ACCESS_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"url":"https://your-server.example.com/hooks","event_types":["registration.created","registration.paid"]}'
    # → { "data": { "id": "…", "signing_secret": "whsec_…", … } }
    ```

    The `signing_secret` is returned **once** on creation (and once per rotation). Store it
    securely — it cannot be retrieved later.

    ### Event catalog

    | Event type | Fired when |
    |---|---|
    | `contact.created` | A new contact is added to your org |
    | `registration.created` | A golfer registers for one of your tournaments |
    | `registration.paid` | A registration payment is confirmed by Stripe |
    | `invitation.delivered` | An outbound invitation email is delivered to the recipient |

    ### Payload envelope

    Every POST body has the same shape:

    ```json
    {
      "id": "<event_id>",
      "type": "registration.created",
      "created_at": "2026-06-08T15:30:00.000Z",
      "data": { "…": "…" }
    }
    ```

    Deduplicate on `id` — deliveries are **at-least-once**.

    ### Verifying signatures

    Each delivery includes three headers:

    - `X-ScrambleSync-Signature: t=<unix_seconds>,v1=<hex>` — HMAC-SHA256 of `"<t>.<rawBody>"` with your signing secret
    - `X-ScrambleSync-Event: <event_type>`
    - `X-ScrambleSync-Event-Id: <event_id>`
    - `X-ScrambleSync-Delivery: <delivery_id>`

    **Node.js verification snippet** (mirrors `lib/webhooks/signing.ts`):

    ```js
    import crypto from "node:crypto";

    function verifyWebhook(signingSecret, rawBody, signatureHeader) {
      const parts = Object.fromEntries(
        signatureHeader.split(",").map((kv) => kv.split("="))
      );
      const t = parseInt(parts.t ?? "", 10);
      const v1 = parts.v1 ?? "";
      if (!Number.isFinite(t) || !v1) return false;
      const nowSec = Math.floor(Date.now() / 1000);
      if (Math.abs(nowSec - t) > 300) return false;
      const expected = crypto
        .createHmac("sha256", signingSecret)
        .update(`${t}.${rawBody}`)
        .digest("hex");
      const a = Buffer.from(expected);
      const b = Buffer.from(v1);
      return a.length === b.length && crypto.timingSafeEqual(a, b);
    }
    ```

    ### Retry & backoff

    Failed deliveries (non-2xx, timeout, or DNS error) are retried with exponential
    backoff: **1 min → 5 min → 30 min → 2 h → 6 h → 24 h**, up to **8 attempts** total.
    After the eighth failure the delivery is marked `dead`. You can replay any delivery
    via `POST /api/v1/webhooks/{id}/deliveries/{deliveryId}/replay`.

    An endpoint that accumulates **15 consecutive failures** is automatically disabled.
    Re-enable it from the dashboard or via `PATCH /api/v1/webhooks/{id}` with
    `{"status":"enabled"}`.

    ## Versioning & deprecation

    v1 is **stable**. Additive changes ship in place with no version bump. Breaking
    changes will only ship under a new major-version path (`/api/v2`). Deprecated
    endpoints carry `Deprecation`, `Sunset`, and `Link; rel="sunset"` headers
    (RFC 9745 / RFC 8594).

servers:
  - url: https://scramblesync.com/api/v1
    description: Production

security:
  - oauth2: []

components:
  securitySchemes:
    oauth2:
      type: oauth2
      flows:
        clientCredentials:
          tokenUrl: https://scramblesync.com/api/v1/oauth/token
          scopes:
            contacts:read: Read contacts & tags
            contacts:write: Create, edit, import & delete contacts & tags
            registrations:read: Read & export registrations
            registrations:write: Refund & cancel registrations (issues Stripe refunds)
            invitations:read: Read invitations & delivery stats
            invitations:write: Create & schedule invitations
            tournaments:read: Read tournaments, teams & leaderboard
            tournaments:write: Edit event details and change event status (Draft / Open / Live / Final / Archived)
            webhooks:read: Read webhook endpoints & deliveries
            webhooks:write: Create, edit, delete & replay webhooks
            teams:write: Move or remove teams, assign flights (live day)
            checkin:write: Check teams & players in
            messages:write: Send broadcast messages
            scores:write: Correct team & player hole scores
            sidegames:write: Add, edit & remove side games / contests
            players:write: Edit player details and withdraw players
            brackets:read: Read match-play brackets and match results
            brackets:write: Record match-play results & advance brackets
            raffles:read: Read raffles, prizes & ticket counts
            raffles:write: Draw raffle winners
            auctions:read: Read auction items & current bids
            announcements:read: Read announcements & delivery status
            announcements:write: Create & send announcements
            rsvps:read: Read RSVP responses
            series:read: Read series / season standings
            scorecards:read: Read per-team hole-by-hole scorecards
            flights:read: Read per-flight standings

  parameters:
    limitParam:
      name: limit
      in: query
      description: "Number of items per page (1–200, default 50)"
      schema:
        type: integer
        minimum: 1
        maximum: 200
        default: 50
    cursorParam:
      name: cursor
      in: query
      description: Opaque keyset cursor returned by the previous page's `page.next_cursor`
      schema:
        type: string

  schemas:
    PageInfo:
      type: object
      required: [next_cursor, has_more]
      properties:
        next_cursor:
          type: string
          nullable: true
          description: Pass as `?cursor=` to fetch the next page; null on the last page
        has_more:
          type: boolean

    HoleConfig:
      type: object
      properties:
        id:
          type: string
          format: uuid
        hole_number:
          type: integer
          minimum: 1
          maximum: 18
        par:
          type: integer
          nullable: true
        stroke_index:
          type: integer
          nullable: true
          description: Difficulty rank (1 = hardest, 18 = easiest).
        yardage:
          type: integer
          nullable: true
        contest:
          type: string
          enum: [longest_drive, closest_to_pin, straightest_drive]
          nullable: true

    AuditEvent:
      type: object
      properties:
        id:
          type: string
          format: uuid
        action:
          type: string
          description: Machine-readable action code (e.g. `status_changed`, `score_corrected`).
        actor_email:
          type: string
          nullable: true
          description: Email of the staff member who performed the action.
        target_type:
          type: string
          nullable: true
          description: Type of the affected object (e.g. `tournament`, `team`, `score`).
        target_id:
          type: string
          nullable: true
          description: ID of the affected object.
        metadata:
          type: object
          nullable: true
          description: Action-specific detail (e.g. `{"from":"open","to":"live"}`).
        created_at:
          type: string
          format: date-time

    ErrorResponse:
      type: object
      required: [error, code, request_id]
      properties:
        error:
          type: string
          description: Human-readable error message
        code:
          type: string
          enum:
            - invalid_token
            - insufficient_scope
            - bad_request
            - unauthorized
            - forbidden
            - not_found
            - validation_error
            - conflict
            - rate_limited
            - internal_error
            - service_unavailable
        request_id:
          type: string
          format: uuid

    Contact:
      type: object
      properties:
        id:
          type: string
          format: uuid
        email:
          type: string
          format: email
        first_name:
          type: string
          nullable: true
        last_name:
          type: string
          nullable: true
        title:
          type: string
          nullable: true
        department:
          type: string
          nullable: true
        company:
          type: string
          nullable: true
        phone:
          type: string
          nullable: true
        opted_out:
          type: boolean
        source:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time

    Tag:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        color:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time

    Tournament:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        course_name:
          type: string
          nullable: true
        event_date:
          type: string
          format: date
          nullable: true
        format:
          type: string
          enum: [scramble, best_ball, individual]
          nullable: true
        status:
          type: string
          enum: [setup, open, live, final, archived]
        created_at:
          type: string
          format: date-time

    Team:
      type: object
      properties:
        id:
          type: string
          format: uuid
        team_name:
          type: string
        starting_hole:
          type: integer
          nullable: true
        starting_sequence:
          type: string
          nullable: true
        handicap:
          type: number
          nullable: true
        flight_id:
          type: string
          format: uuid
          nullable: true
        created_at:
          type: string
          format: date-time

    Player:
      type: object
      properties:
        id:
          type: string
          format: uuid
        full_name:
          type: string
          nullable: true
        team_id:
          type: string
          format: uuid
          nullable: true
        team_name:
          type: string
          nullable: true
        checked_in:
          type: boolean

    Registration:
      type: object
      properties:
        id:
          type: string
          format: uuid
        tournament_id:
          type: string
          format: uuid
        status:
          type: string
          enum: [pending, paid, cancelled, refunded]
        created_at:
          type: string
          format: date-time

    Invitation:
      type: object
      properties:
        id:
          type: string
          format: uuid
        tournament_id:
          type: string
          format: uuid
          nullable: true
        subject:
          type: string
          nullable: true
        status:
          type: string
        scheduled_at:
          type: string
          format: date-time
          nullable: true
        sent_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time

    WebhookEndpoint:
      type: object
      properties:
        id:
          type: string
          format: uuid
        url:
          type: string
          format: uri
        description:
          type: string
          nullable: true
        event_types:
          type: array
          items:
            type: string
        status:
          type: string
          enum: [enabled, disabled]
        created_at:
          type: string
          format: date-time

    WebhookDelivery:
      type: object
      properties:
        id:
          type: string
          format: uuid
        endpoint_id:
          type: string
          format: uuid
        event_type:
          type: string
        status:
          type: string
          enum: [pending, delivered, failed, dead]
        http_status:
          type: integer
          nullable: true
        attempt:
          type: integer
        created_at:
          type: string
          format: date-time

paths:

  # ---------------------------------------------------------------------------
  # OAuth
  # ---------------------------------------------------------------------------

  /oauth/token:
    post:
      operationId: issueToken
      summary: Issue an access token
      description: |
        OAuth 2.0 client credentials grant. Authenticate with HTTP Basic
        (`Authorization: Basic base64(client_id:client_secret)`) or pass
        `client_id` / `client_secret` in the JSON/form body.

        Errors from this endpoint use the OAuth 2.0 shape
        `{ "error": "…", "error_description": "…" }` — not the standard API envelope.
      security: []
      tags: [Auth]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [grant_type]
              properties:
                grant_type:
                  type: string
                  enum: [client_credentials]
                client_id:
                  type: string
                  description: Required unless using HTTP Basic authentication
                client_secret:
                  type: string
                  description: Required unless using HTTP Basic authentication
                scope:
                  type: string
                  description: Space-delimited subset of the client's allowed scopes. Omit to receive all allowed scopes.
            example:
              grant_type: client_credentials
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [grant_type]
              properties:
                grant_type:
                  type: string
                  enum: [client_credentials]
                client_id:
                  type: string
                client_secret:
                  type: string
                scope:
                  type: string
      responses:
        "200":
          description: Token issued
          headers:
            Cache-Control:
              schema:
                type: string
                example: no-store
          content:
            application/json:
              schema:
                type: object
                required: [access_token, token_type, expires_in, scope]
                properties:
                  access_token:
                    type: string
                  token_type:
                    type: string
                    enum: [Bearer]
                  expires_in:
                    type: integer
                    example: 3600
                  scope:
                    type: string
        "400":
          description: Unsupported grant type or invalid scope
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    example: unsupported_grant_type
                  error_description:
                    type: string
        "401":
          description: Invalid client credentials
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    example: invalid_client
                  error_description:
                    type: string
        "429":
          description: Rate limited (10 auth requests/min per IP)

  /oauth/revoke:
    post:
      operationId: revokeToken
      summary: Revoke an access token
      description: |
        RFC 7009 token revocation. Always returns `200` (even for unknown tokens).
        Authenticate with HTTP Basic. Pass the token as `token` in an
        `application/x-www-form-urlencoded` body. Revocation is scoped to the
        calling client — a client cannot revoke another client's tokens.
      security: []
      tags: [Auth]
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                token:
                  type: string
      responses:
        "200":
          description: Revocation acknowledged (always returned, even for unknown tokens)
          headers:
            Cache-Control:
              schema:
                type: string
                example: no-store

  # ---------------------------------------------------------------------------
  # Contacts
  # ---------------------------------------------------------------------------

  /contacts:
    get:
      operationId: listContacts
      summary: List contacts
      description: Cursor-paginated list of all contacts in the organization.
      tags: [Contacts]
      security:
        - oauth2: [contacts:read]
      parameters:
        - $ref: "#/components/parameters/limitParam"
        - $ref: "#/components/parameters/cursorParam"
      responses:
        "200":
          description: Paginated contact list
          content:
            application/json:
              schema:
                type: object
                required: [data, page]
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Contact"
                  page:
                    $ref: "#/components/schemas/PageInfo"
        "400":
          description: Invalid cursor
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Invalid or missing token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Insufficient scope
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

    post:
      operationId: createContact
      summary: Create a contact
      description: Creates a new contact. Returns `409 conflict` if the email is already in use.
      tags: [Contacts]
      security:
        - oauth2: [contacts:write]
      parameters:
        - name: Idempotency-Key
          in: header
          schema:
            type: string
            maxLength: 128
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email:
                  type: string
                  format: email
                first_name:
                  type: string
                last_name:
                  type: string
                title:
                  type: string
                department:
                  type: string
                company:
                  type: string
                phone:
                  type: string
      responses:
        "201":
          description: Contact created
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    $ref: "#/components/schemas/Contact"
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Email already in use
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /contacts/export:
    get:
      operationId: exportContacts
      summary: Export contacts as CSV
      description: |
        Returns all contacts as a CSV file (columns: Email, First, Last, Title,
        Department, Company, Phone, Opted out, Source, Created).
        If the export was truncated an `X-Truncated: true` header is set.
      tags: [Contacts]
      security:
        - oauth2: [contacts:read]
      responses:
        "200":
          description: CSV file
          headers:
            Content-Disposition:
              schema:
                type: string
                example: 'attachment; filename="contacts.csv"'
            X-Truncated:
              schema:
                type: string
                example: "true"
          content:
            text/csv:
              schema:
                type: string

  /contacts/bulk:
    post:
      operationId: bulkContacts
      summary: Bulk delete / tag / untag contacts
      description: |
        Apply one action to up to **1 000** contacts at once.

        - `delete` — permanently deletes the contacts
        - `tag` — applies `tag_id` to all listed contacts (requires `tag_id`)
        - `untag` — removes `tag_id` from all listed contacts (requires `tag_id`)
      tags: [Contacts]
      security:
        - oauth2: [contacts:write]
      parameters:
        - name: Idempotency-Key
          in: header
          schema:
            type: string
            maxLength: 128
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [action, ids]
              properties:
                action:
                  type: string
                  enum: [delete, tag, untag]
                ids:
                  type: array
                  items:
                    type: string
                    format: uuid
                  minItems: 1
                  maxItems: 1000
                  description: Contact IDs to act on (max 1 000)
                tag_id:
                  type: string
                  format: uuid
                  description: Required when `action` is `tag` or `untag`
            example:
              action: tag
              ids: ["<uuid1>", "<uuid2>"]
              tag_id: "<tag-uuid>"
      responses:
        "200":
          description: Action applied
          content:
            application/json:
              schema:
                type: object
                required: [ok, count]
                properties:
                  ok:
                    type: boolean
                    example: true
                  count:
                    type: integer
                    description: Number of contacts affected
        "400":
          description: Validation error (invalid action, empty ids, missing tag_id, more than 1000 ids, etc.)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /contacts/import:
    post:
      operationId: importContacts
      summary: Import contacts from CSV
      description: |
        Import contacts from a CSV string. The CSV must have a header row.
        Recognised column names (case-insensitive): `Email`, `First`, `Last`,
        `Title`, `Department`, `Company`, `Phone`.

        - Maximum **5 000 rows** per call; split larger files into batches.
        - `consent: true` is **required** to confirm recipients have agreed to receive email.
        - Existing contacts (matched by email) are updated; new emails are inserted.
        - Duplicate emails within the file are counted in `duplicatesInFile`.
      tags: [Contacts]
      security:
        - oauth2: [contacts:write]
      parameters:
        - name: Idempotency-Key
          in: header
          schema:
            type: string
            maxLength: 128
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [csv, consent]
              properties:
                csv:
                  type: string
                  description: Raw CSV text including header row
                consent:
                  type: boolean
                  enum: [true]
                  description: Must be `true` — confirms contacts have agreed to receive email
            example:
              csv: "Email,First,Last\njane@example.com,Jane,Smith"
              consent: true
      responses:
        "200":
          description: Import result
          content:
            application/json:
              schema:
                type: object
                required: [inserted, updated, skipped, duplicatesInFile, errors]
                properties:
                  inserted:
                    type: integer
                    description: New contacts created
                  updated:
                    type: integer
                    description: Existing contacts updated
                  skipped:
                    type: integer
                    description: Rows skipped due to parse errors
                  duplicatesInFile:
                    type: integer
                    description: Email addresses that appeared more than once in the CSV
                  errors:
                    type: array
                    items:
                      type: object
                      properties:
                        row:
                          type: integer
                        message:
                          type: string
        "400":
          description: Validation error (missing csv, consent not true, CSV exceeds 5 000 rows, no valid rows)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /contacts/{id}:
    get:
      operationId: getContact
      summary: Get a contact
      tags: [Contacts]
      security:
        - oauth2: [contacts:read]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Contact found
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    $ref: "#/components/schemas/Contact"
        "404":
          description: Contact not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

    patch:
      operationId: updateContact
      summary: Update a contact
      description: Partially update a contact. Only supplied fields are changed.
      tags: [Contacts]
      security:
        - oauth2: [contacts:write]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                email:
                  type: string
                  format: email
                first_name:
                  type: string
                last_name:
                  type: string
                title:
                  type: string
                department:
                  type: string
                company:
                  type: string
                phone:
                  type: string
      responses:
        "200":
          description: Updated contact
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    $ref: "#/components/schemas/Contact"
        "404":
          description: Contact not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Email already in use by another contact
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

    delete:
      operationId: deleteContact
      summary: Delete a contact
      tags: [Contacts]
      security:
        - oauth2: [contacts:write]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Contact deleted
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                        format: uuid
                      deleted:
                        type: boolean
                        example: true
        "404":
          description: Contact not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  # ---------------------------------------------------------------------------
  # Tags
  # ---------------------------------------------------------------------------

  /tags:
    get:
      operationId: listTags
      summary: List tags
      tags: [Tags]
      security:
        - oauth2: [contacts:read]
      parameters:
        - $ref: "#/components/parameters/limitParam"
        - $ref: "#/components/parameters/cursorParam"
      responses:
        "200":
          description: Paginated tag list
          content:
            application/json:
              schema:
                type: object
                required: [data, page]
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Tag"
                  page:
                    $ref: "#/components/schemas/PageInfo"

    post:
      operationId: createTag
      summary: Create a tag
      tags: [Tags]
      security:
        - oauth2: [contacts:write]
      parameters:
        - name: Idempotency-Key
          in: header
          schema:
            type: string
            maxLength: 128
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
                color:
                  type: string
                  description: Optional hex color for UI display
      responses:
        "201":
          description: Tag created
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    $ref: "#/components/schemas/Tag"
        "409":
          description: Tag name already in use
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /tags/{id}:
    delete:
      operationId: deleteTag
      summary: Delete a tag
      tags: [Tags]
      security:
        - oauth2: [contacts:write]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Tag deleted
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                        format: uuid
                      deleted:
                        type: boolean
                        example: true
        "404":
          description: Tag not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  # ---------------------------------------------------------------------------
  # Tournaments
  # ---------------------------------------------------------------------------

  /tournaments:
    get:
      operationId: listTournaments
      summary: List tournaments
      tags: [Tournaments]
      security:
        - oauth2: [tournaments:read]
      parameters:
        - $ref: "#/components/parameters/limitParam"
        - $ref: "#/components/parameters/cursorParam"
      responses:
        "200":
          description: Paginated tournament list
          content:
            application/json:
              schema:
                type: object
                required: [data, page]
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Tournament"
                  page:
                    $ref: "#/components/schemas/PageInfo"

  /tournaments/{id}:
    get:
      operationId: getTournament
      summary: Get a tournament
      tags: [Tournaments]
      security:
        - oauth2: [tournaments:read]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Tournament found
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    $ref: "#/components/schemas/Tournament"
        "404":
          description: Tournament not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

    patch:
      operationId: updateTournament
      summary: Update tournament details
      description: |
        Update basic tournament fields. All fields are optional — at least one
        must be supplied. Fields not included are left unchanged.
      tags: [Tournaments]
      security:
        - oauth2: [tournaments:write]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 500
                courseName:
                  type: string
                  maxLength: 500
                eventDate:
                  type: string
                  pattern: '^\d{4}-\d{2}-\d{2}$'
                  description: "Event date in YYYY-MM-DD format"
                format:
                  type: string
                  enum: [scramble, best_ball, individual]
      responses:
        "200":
          description: Fields updated; returns id plus each updated field
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
        "400":
          description: No fields supplied or validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Tournament not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /tournaments/{id}/summary:
    get:
      operationId: getTournamentSummary
      summary: Get tournament summary
      description: |
        Quick digest: status, check-in progress, scores posted, open integrity
        flags, hole count, and whether side games are enabled. Does not recompute
        the full leaderboard — use `/boards` for standings.
      tags: [Tournaments]
      security:
        - oauth2: [tournaments:read]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Summary
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    properties:
                      name:
                        type: string
                      status:
                        type: string
                      teams:
                        type: object
                        properties:
                          total:
                            type: integer
                          checkedIn:
                            type: integer
                      players:
                        type: object
                        properties:
                          total:
                            type: integer
                          checkedIn:
                            type: integer
                      scoresPosted:
                        type: integer
                      openFlags:
                        type: integer
                      holes:
                        type: integer
                      sideGamesEnabled:
                        type: boolean
        "404":
          description: Tournament not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /tournaments/{id}/status:
    post:
      operationId: setTournamentStatus
      summary: Set tournament status
      description: |
        Change the event status. Valid values: `draft` (alias for `setup`), `setup`,
        `open`, `live`, `final`, `archived`.

        By default a manual status change clears the automatic-status flag (same as
        a manual dashboard change). Pass `autoStatus: true` to re-enable the
        automatic lifecycle without disabling it — equivalent to choosing "Automatic"
        in the dashboard status selector.
      tags: [Tournaments]
      security:
        - oauth2: [tournaments:write]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [status]
              properties:
                status:
                  type: string
                  enum: [draft, setup, open, live, final, archived]
                autoStatus:
                  type: boolean
                  description: >
                    When `true`, re-enables the automatic status lifecycle for
                    this event (same as the dashboard "Automatic" option).
                    When `false` or omitted, the manual status change disables
                    automation.
            example:
              status: live
      responses:
        "200":
          description: Status updated
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    required: [status, autoStatus]
                    properties:
                      status:
                        type: string
                      autoStatus:
                        type: boolean
                        description: Whether automatic status management is now enabled.
        "404":
          description: Tournament not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Invalid status transition
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /tournaments/{id}/leaderboard:
    get:
      operationId: getTournamentLeaderboard
      summary: Get leaderboard
      description: Gross leaderboard for the tournament (single board). Use `/boards` for all enabled boards.
      tags: [Tournaments]
      security:
        - oauth2: [tournaments:read]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Leaderboard rows
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items:
                      type: object
        "404":
          description: Tournament not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /tournaments/{id}/boards:
    get:
      operationId: getTournamentBoards
      summary: Get all score boards
      description: All enabled boards (gross, net, stableford) via the shared `computeBoards` engine.
      tags: [Tournaments]
      security:
        - oauth2: [tournaments:read]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: All boards
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items:
                      type: object
        "404":
          description: Tournament not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /tournaments/{id}/players:
    get:
      operationId: listPlayers
      summary: List players
      description: Flat roster of all players in the tournament with team assignment and check-in state.
      tags: [Tournaments]
      security:
        - oauth2: [tournaments:read]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Player list
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Player"
        "404":
          description: Tournament not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /tournaments/{id}/players/{pid}/checkin:
    post:
      operationId: checkInPlayer
      summary: Check in a player
      description: Checks in a single player. No request body. Idempotent.
      tags: [Tournaments]
      security:
        - oauth2: [checkin:write]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: pid
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Player checked in
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    properties:
                      checkedIn:
                        type: boolean
                        example: true
        "404":
          description: Tournament or player not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Check-in conflict
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /tournaments/{id}/players/{pid}/move:
    post:
      operationId: movePlayer
      summary: Move player to another team
      description: Move a player to a different team (substitution / re-pair).
      tags: [Tournaments]
      security:
        - oauth2: [teams:write]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: pid
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [toTeamId]
              properties:
                toTeamId:
                  type: string
                  format: uuid
      responses:
        "200":
          description: Player moved
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    properties:
                      moved:
                        type: boolean
                        example: true
        "404":
          description: Tournament, player, or target team not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Move not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /tournaments/{id}/players/{pid}/flight:
    post:
      operationId: assignPlayerFlight
      summary: Assign player to a flight
      description: Assign a player to a flight (individual-format events), or clear with `flightId:null`.
      tags: [Tournaments]
      security:
        - oauth2: [teams:write]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: pid
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [flightId]
              properties:
                flightId:
                  type: string
                  format: uuid
                  nullable: true
      responses:
        "200":
          description: Flight assigned (or cleared)
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    properties:
                      assigned:
                        type: boolean
                        example: true
        "404":
          description: Tournament, player, or flight not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /tournaments/{id}/players/{pid}/update:
    post:
      operationId: updatePlayer
      summary: Update player details
      description: |
        Edit a player's details (shirt size, cart, handicap, name, email, etc.).
        At least one field must be supplied.
      tags: [Tournaments]
      security:
        - oauth2: [players:write]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: pid
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                shirt_size:
                  type: string
                  maxLength: 20
                cart:
                  type: integer
                rental_clubs:
                  type: string
                  maxLength: 40
                player_type:
                  type: string
                  maxLength: 40
                handicap:
                  type: number
                  minimum: -10
                  maximum: 60
                company:
                  type: string
                  maxLength: 120
                job_title:
                  type: string
                  maxLength: 120
                email:
                  type: string
                  format: email
                  maxLength: 160
                phone:
                  type: string
                  maxLength: 40
                first_name:
                  type: string
                  maxLength: 80
                last_name:
                  type: string
                  maxLength: 80
                full_name:
                  type: string
                  maxLength: 160
                ghin_number:
                  type: string
                  maxLength: 20
      responses:
        "200":
          description: Player updated
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    properties:
                      updated:
                        type: boolean
                        example: true
        "404":
          description: Tournament or player not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Update conflict
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /tournaments/{id}/players/{pid}/withdraw:
    post:
      operationId: withdrawPlayer
      summary: Withdraw a player
      description: |
        Mark a player withdrawn (no-show, DQ, injury) without deleting them.
        Withdrawn players are excluded from individual and best-ball leaderboards.
      tags: [Tournaments]
      security:
        - oauth2: [players:write]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: pid
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                reason:
                  type: string
                  maxLength: 200
      responses:
        "200":
          description: Player withdrawn
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    properties:
                      withdrawn:
                        type: boolean
                        example: true
        "404":
          description: Tournament or player not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Withdrawal conflict
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /tournaments/{id}/teams:
    get:
      operationId: listTeams
      summary: List teams
      tags: [Tournaments]
      security:
        - oauth2: [tournaments:read]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - $ref: "#/components/parameters/limitParam"
        - $ref: "#/components/parameters/cursorParam"
      responses:
        "200":
          description: Paginated team list
          content:
            application/json:
              schema:
                type: object
                required: [data, page]
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Team"
                  page:
                    $ref: "#/components/schemas/PageInfo"
        "404":
          description: Tournament not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

    post:
      operationId: createTeam
      summary: Add a team
      description: |
        Create a new team (live-day add). Generates an access code, auto-picks the
        least-crowded starting hole when none is given, optionally seats a roster
        and assigns a flight.
      tags: [Tournaments]
      security:
        - oauth2: [teams:write]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [teamName]
              properties:
                teamName:
                  type: string
                  minLength: 1
                  maxLength: 60
                startingHole:
                  type: integer
                  minimum: 1
                  maximum: 18
                startingSequence:
                  type: string
                  enum: [A, B, C, D]
                flightId:
                  type: string
                  format: uuid
                  nullable: true
                players:
                  type: array
                  maxItems: 8
                  items:
                    type: object
                    required: [fullName]
                    properties:
                      fullName:
                        type: string
                        minLength: 1
                        maxLength: 120
      responses:
        "200":
          description: Team created
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    properties:
                      teamId:
                        type: string
                        format: uuid
                      startingHole:
                        type: integer
                      seatedPlayers:
                        type: integer
        "404":
          description: Tournament or flight not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Team creation conflict
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /tournaments/{id}/teams/{tid}/checkin:
    post:
      operationId: checkInTeam
      summary: Check in a team
      description: Checks in every not-yet-checked-in player on the team. No request body. Idempotent.
      tags: [Tournaments]
      security:
        - oauth2: [checkin:write]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: tid
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Team checked in
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    properties:
                      checkedIn:
                        type: boolean
                        example: true
        "404":
          description: Tournament or team not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Check-in conflict
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /tournaments/{id}/teams/{tid}/move:
    post:
      operationId: moveTeam
      summary: Move team to a new starting hole
      tags: [Tournaments]
      security:
        - oauth2: [teams:write]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: tid
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [startingHole]
              properties:
                startingHole:
                  type: integer
                  minimum: 1
                  maximum: 18
                startingSequence:
                  type: string
                  enum: [A, B]
                  default: A
      responses:
        "200":
          description: Team moved
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    properties:
                      moved:
                        type: boolean
                        example: true
        "404":
          description: Tournament or team not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Move conflict
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /tournaments/{id}/teams/{tid}/flight:
    post:
      operationId: assignTeamFlight
      summary: Assign team to a flight
      description: Assign a team to a flight, or clear with `flightId:null`.
      tags: [Tournaments]
      security:
        - oauth2: [teams:write]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: tid
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [flightId]
              properties:
                flightId:
                  type: string
                  format: uuid
                  nullable: true
      responses:
        "200":
          description: Flight assigned (or cleared)
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    properties:
                      assigned:
                        type: boolean
                        example: true
        "404":
          description: Tournament, team, or flight not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /tournaments/{id}/teams/{tid}/remove:
    post:
      operationId: removeTeam
      summary: Remove a team
      description: |
        Permanently remove a team. Refuses if the team has signed scorecards.
        **Destructive and irreversible** — scores are snapshot to the audit log
        before deletion.
      tags: [Tournaments]
      security:
        - oauth2: [teams:write]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: tid
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Team removed
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    properties:
                      removed:
                        type: boolean
                        example: true
        "404":
          description: Tournament or team not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Team has signed scorecards and cannot be removed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /tournaments/{id}/teams/{tid}/scorecard:
    get:
      operationId: getTeamScorecard
      summary: Get one team's scorecard
      description: |
        Hole-by-hole gross scores for a single team:
        `{ team_name, holes:[{hole_number, par, gross}], total, thru }`.
        Only holes with posted scores appear, ordered by hole number.
      tags: [Tournaments]
      security:
        - oauth2: [tournaments:read]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: tid
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Team scorecard
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    properties:
                      team_name:
                        type: string
                      holes:
                        type: array
                        items:
                          type: object
                          properties:
                            hole_number:
                              type: integer
                            par:
                              type: integer
                              nullable: true
                            gross:
                              type: integer
                      total:
                        type: integer
                      thru:
                        type: integer
        "404":
          description: Tournament or team not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /tournaments/{id}/scores:
    post:
      operationId: postScore
      summary: Post / correct a hole score
      description: |
        Post or correct a team's (scramble) or a player's (best-ball / individual)
        gross score on a hole. Exactly one of `teamId` or `playerId` must be
        provided. Respects signed-card and per-hole locks.
      tags: [Tournaments]
      security:
        - oauth2: [scores:write]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [holeNumber, gross]
              properties:
                teamId:
                  type: string
                  format: uuid
                  description: Supply for scramble (team-ball) formats
                playerId:
                  type: string
                  format: uuid
                  description: Supply for best-ball / individual formats
                holeNumber:
                  type: integer
                  minimum: 1
                  maximum: 18
                gross:
                  type: integer
                  minimum: 1
                  maximum: 30
      responses:
        "200":
          description: Score saved
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    properties:
                      saved:
                        type: boolean
                        example: true
        "400":
          description: Neither or both of teamId / playerId supplied
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Tournament not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Score locked (signed card or hole lock)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /tournaments/{id}/messages:
    post:
      operationId: sendMessage
      summary: Send a broadcast message
      description: Send a message to all teams or a single team (when `teamId` is provided).
      tags: [Tournaments]
      security:
        - oauth2: [messages:write]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [body]
              properties:
                body:
                  type: string
                  minLength: 1
                  maxLength: 500
                teamId:
                  type: string
                  format: uuid
                  description: Omit to broadcast to all teams
      responses:
        "200":
          description: Message sent
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    properties:
                      sent:
                        type: boolean
                        example: true
        "404":
          description: Tournament or team not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Message conflict
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /tournaments/{id}/checkin:
    get:
      operationId: getCheckinStatus
      summary: Get check-in status
      description: |
        Check-in status per team plus roll-up totals. A team counts as checked in
        when it has players and every one of them has arrived.
      tags: [Tournaments]
      security:
        - oauth2: [tournaments:read]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Check-in status
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    properties:
                      teams:
                        type: array
                        items:
                          type: object
                          properties:
                            id:
                              type: string
                              format: uuid
                            team_name:
                              type: string
                            checked_in:
                              type: boolean
                            players_in:
                              type: integer
                            players_total:
                              type: integer
                      summary:
                        type: object
                        properties:
                          teamsIn:
                            type: integer
                          teamsTotal:
                            type: integer
                          playersIn:
                            type: integer
                          playersTotal:
                            type: integer
        "404":
          description: Tournament not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /tournaments/{id}/flags:
    get:
      operationId: getTournamentFlags
      summary: Get integrity flags
      description: Open integrity flags ordered by severity descending (highest first).
      tags: [Tournaments]
      security:
        - oauth2: [tournaments:read]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Integrity flags
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    properties:
                      count:
                        type: integer
                      flags:
                        type: array
                        items:
                          type: object
                          properties:
                            flag_type:
                              type: string
                            severity:
                              type: integer
                              nullable: true
                            hole:
                              type: integer
                              nullable: true
                            note:
                              type: string
                              nullable: true
                            source:
                              type: string
                              nullable: true
                            team_name:
                              type: string
                              nullable: true
        "404":
          description: Tournament not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /tournaments/{id}/fundraising:
    get:
      operationId: getFundraising
      summary: Get fundraising progress
      description: |
        Fundraising totals: amount raised, goal, and percentage reached.
        Returns a zero state when donations are disabled or nothing has been raised.
        Donor names and per-donor amounts are never exposed.
      tags: [Tournaments]
      security:
        - oauth2: [tournaments:read]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Fundraising progress
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    properties:
                      donationsEnabled:
                        type: boolean
                      raisedCents:
                        type: integer
                      raisedDollars:
                        type: number
                      donationCount:
                        type: integer
                      goalCents:
                        type: integer
                        nullable: true
                      goalDollars:
                        type: number
                        nullable: true
                      pctOfGoal:
                        type: integer
                        nullable: true
        "404":
          description: Tournament not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /tournaments/{id}/sponsorfill:
    get:
      operationId: getSponsorFill
      summary: Get sponsor fill / inventory
      description: Total sponsor count plus per-package sold/available counts (when packages are configured).
      tags: [Tournaments]
      security:
        - oauth2: [tournaments:read]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Sponsor inventory
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    properties:
                      sponsorCount:
                        type: integer
                      packages:
                        type: array
                        items:
                          type: object
                          properties:
                            name:
                              type: string
                            priceCents:
                              type: integer
                            inventory:
                              type: integer
                              nullable: true
                            sold:
                              type: integer
                            available:
                              type: integer
                              nullable: true
        "404":
          description: Tournament not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /tournaments/{id}/sidegames:
    get:
      operationId: listSideGames
      summary: Get side-game standings
      description: Side-game / skins standings for all active games via the shared standings engine.
      tags: [Tournaments]
      security:
        - oauth2: [tournaments:read]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Side-game standings
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items:
                      type: object
        "404":
          description: Tournament not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

    post:
      operationId: addSideGame
      summary: Add a side game
      description: Add a side game / contest (skins, closest-to-pin, longest drive, custom).
      tags: [Tournaments]
      security:
        - oauth2: [sidegames:write]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [kind, name]
              properties:
                kind:
                  type: string
                  enum: [skins, ctp, longest_drive, custom]
                name:
                  type: string
                  minLength: 1
                  maxLength: 100
                holeNumber:
                  type: integer
                  minimum: 1
                  maximum: 18
      responses:
        "200":
          description: Side game added
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    properties:
                      added:
                        type: boolean
                        example: true
        "404":
          description: Tournament not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Side game conflict
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /tournaments/{id}/sidegames/{gid}:
    patch:
      operationId: updateSideGame
      summary: Update a side game
      description: Edit a side game's name and/or hole. At least one field must be provided.
      tags: [Tournaments]
      security:
        - oauth2: [sidegames:write]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: gid
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 100
                holeNumber:
                  type: integer
                  minimum: 1
                  maximum: 18
                  nullable: true
                  description: Pass `null` to clear the hole assignment
      responses:
        "200":
          description: Side game updated
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    properties:
                      updated:
                        type: boolean
                        example: true
        "404":
          description: Tournament or side game not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

    delete:
      operationId: deleteSideGame
      summary: Delete a side game
      tags: [Tournaments]
      security:
        - oauth2: [sidegames:write]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: gid
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Side game deleted
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    properties:
                      deleted:
                        type: boolean
                        example: true
        "404":
          description: Tournament or side game not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /tournaments/{id}/holes/{hole}/contest:
    patch:
      operationId: setHoleContest
      summary: Set or clear a hole contest
      description: |
        Designate or clear a contest on a specific hole (by 1-based number).
        Pass `contest:null` to clear. Valid types: `longest_drive`,
        `closest_to_pin`, `straightest_drive`.
      tags: [Tournaments]
      security:
        - oauth2: [sidegames:write]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: hole
          in: path
          required: true
          schema:
            type: integer
            minimum: 1
            maximum: 18
          description: 1-based hole number
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [contest]
              properties:
                contest:
                  type: string
                  enum: [longest_drive, closest_to_pin, straightest_drive]
                  nullable: true
      responses:
        "200":
          description: Hole contest updated
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    properties:
                      updated:
                        type: boolean
                        example: true
        "400":
          description: Invalid hole number
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Tournament or hole not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /tournaments/{id}/scorecards:
    get:
      operationId: listScorecards
      summary: List all team scorecards
      description: |
        Per-team hole-by-hole scorecards for all teams. Each scorecard includes
        team id, name, handicap, starting hole, gross scores by hole, total gross,
        total net (when handicap is set), and `thru`. Player names and emails are
        not returned.
      tags: [Tournaments]
      security:
        - oauth2: [scorecards:read]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: All team scorecards
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        teamId:
                          type: string
                          format: uuid
                        teamName:
                          type: string
                        handicap:
                          type: number
                          nullable: true
                        startingHole:
                          type: integer
                        holes:
                          type: array
                          items:
                            type: object
                            properties:
                              holeNumber:
                                type: integer
                              par:
                                type: integer
                                nullable: true
                              gross:
                                type: integer
                        totalGross:
                          type: integer
                        totalNet:
                          type: integer
                          nullable: true
                        thru:
                          type: integer
        "404":
          description: Tournament not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /tournaments/{id}/flights:
    get:
      operationId: getFlightStandings
      summary: Get per-flight standings
      description: |
        Gross leaderboard grouped by flight. Teams without a flight appear in a
        trailing `"Unflighted"` group when present.
      tags: [Tournaments]
      security:
        - oauth2: [flights:read]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Per-flight standings
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        flightId:
                          type: string
                          format: uuid
                          nullable: true
                        flightName:
                          type: string
                        standings:
                          type: array
                          items:
                            type: object
                            properties:
                              teamId:
                                type: string
                                format: uuid
                              teamName:
                                type: string
                              total:
                                type: integer
                              thru:
                                type: integer
                              relativeScore:
                                type: integer
        "404":
          description: Tournament not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /tournaments/{id}/flights/{fid}:
    patch:
      operationId: renameFlight
      summary: Rename a flight
      tags: [Tournaments]
      security:
        - oauth2: [teams:write]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: fid
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 60
      responses:
        "200":
          description: Flight renamed
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    properties:
                      renamed:
                        type: boolean
                        example: true
        "404":
          description: Tournament or flight not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /tournaments/{id}/flights/auto-split:
    post:
      operationId: autoSplitFlights
      summary: Auto-split teams or players into flights by handicap
      description: |
        Distribute teams (`scope:"teams"`) or players (`scope:"players"`) across
        the event's existing flights by handicap. No undo is recorded.
      tags: [Tournaments]
      security:
        - oauth2: [teams:write]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                scope:
                  type: string
                  enum: [teams, players]
                  default: teams
      responses:
        "200":
          description: Auto-split complete
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    properties:
                      assigned:
                        type: integer
                        description: Number of teams/players assigned to flights
        "404":
          description: Tournament not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: No flights configured or other conflict
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /tournaments/{id}/brackets:
    get:
      operationId: listBrackets
      summary: List match-play brackets
      description: All brackets for the tournament, each with its full round/match structure.
      tags: [Tournaments]
      security:
        - oauth2: [brackets:read]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Brackets
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                          format: uuid
                        tournamentId:
                          type: string
                          format: uuid
                        name:
                          type: string
                        kind:
                          type: string
                        createdAt:
                          type: string
                          format: date-time
                        rounds:
                          type: array
                          description: Each element is an array of matches in that round
                          items:
                            type: array
                            items:
                              type: object
        "404":
          description: Tournament not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /tournaments/{id}/brackets/matches/{mid}:
    post:
      operationId: recordMatchResult
      summary: Record a bracket match result
      description: |
        Record the winner of a bracket match and advance them to the next round.
        Re-recording the same winner is idempotent (safe to retry).
      tags: [Tournaments]
      security:
        - oauth2: [brackets:write]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: mid
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [winnerId]
              properties:
                winnerId:
                  type: string
                  format: uuid
                  description: Entity id (team or player) of the winner — must be one of the two participants
                scoreText:
                  type: string
                  maxLength: 64
                  description: 'Optional score text, e.g. "2&1" or "1 UP"'
      responses:
        "200":
          description: Match result recorded
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    properties:
                      matchId:
                        type: string
                        format: uuid
                      winnerId:
                        type: string
                        format: uuid
                      winnerName:
                        type: string
                        nullable: true
                      statusText:
                        type: string
                        nullable: true
                      decided:
                        type: boolean
        "400":
          description: Winner is not one of the two participants
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Tournament or match not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /tournaments/{id}/raffles:
    get:
      operationId: listRaffles
      summary: List raffles
      description: All raffle products with ticket counts and drawn winners.
      tags: [Tournaments]
      security:
        - oauth2: [raffles:read]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Raffles
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                          format: uuid
                        tournamentId:
                          type: string
                          format: uuid
                        name:
                          type: string
                        description:
                          type: string
                          nullable: true
                        priceCents:
                          type: integer
                        active:
                          type: boolean
                        ticketsSold:
                          type: integer
                        winnersDrawn:
                          type: integer
                        winners:
                          type: array
                          items:
                            type: object
                            properties:
                              id:
                                type: string
                                format: uuid
                              buyerName:
                                type: string
                              tickets:
                                type: integer
                              drawNumber:
                                type: integer
                              drawnAt:
                                type: string
                                format: date-time
        "404":
          description: Tournament not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /tournaments/{id}/raffles/{rid}/draw:
    post:
      operationId: drawRaffleWinner
      summary: Draw a raffle winner
      description: |
        Draw the next winner for raffle `rid`. If a winner has already been drawn,
        pass `redraw:true` to allow an additional draw. Winner email is never returned.
      tags: [Tournaments]
      security:
        - oauth2: [raffles:write]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: rid
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                redraw:
                  type: boolean
                  default: false
                  description: Pass `true` to draw an additional winner when one has already been drawn
      responses:
        "200":
          description: Winner drawn
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    properties:
                      winner:
                        type: object
                        properties:
                          name:
                            type: string
                          tickets:
                            type: integer
                          drawNumber:
                            type: integer
                      totalDrawn:
                        type: integer
        "404":
          description: Tournament or raffle not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Winner already drawn (pass `redraw:true`) or no eligible tickets remain
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /tournaments/{id}/auctions:
    get:
      operationId: listAuctions
      summary: List auctions
      description: |
        All auctions with items and current high-bid state. Bidder name is masked
        to `"First L."` for open auctions; full name once `winnerLocked`. Bidder
        email and phone are never returned.
      tags: [Tournaments]
      security:
        - oauth2: [auctions:read]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Auctions
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                          format: uuid
                        tournamentId:
                          type: string
                          format: uuid
                        title:
                          type: string
                        status:
                          type: string
                          enum: [draft, open, closed]
                        opensAt:
                          type: string
                          format: date-time
                          nullable: true
                        closesAt:
                          type: string
                          format: date-time
                          nullable: true
                        createdAt:
                          type: string
                          format: date-time
                        items:
                          type: array
                          items:
                            type: object
        "404":
          description: Tournament not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /tournaments/{id}/announcements:
    get:
      operationId: listAnnouncements
      summary: List announcements
      description: All announcements with delivery counts (sent/total). Individual recipient emails are not returned.
      tags: [Tournaments]
      security:
        - oauth2: [announcements:read]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Announcements
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                          format: uuid
                        tournamentId:
                          type: string
                          format: uuid
                        kind:
                          type: string
                        subject:
                          type: string
                        status:
                          type: string
                        scheduledAt:
                          type: string
                          format: date-time
                          nullable: true
                        sentAt:
                          type: string
                          format: date-time
                          nullable: true
                        createdAt:
                          type: string
                          format: date-time
                        delivery:
                          type: object
                          properties:
                            total:
                              type: integer
                            sent:
                              type: integer
        "404":
          description: Tournament not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

    post:
      operationId: createAnnouncement
      summary: Create an announcement
      description: |
        Create an announcement for a tournament. Starts as `draft`; the send cron
        dispatches it when `scheduledAt` is set. Optionally scope to specific contact
        tags via `audienceTagIds`.
      tags: [Tournaments]
      security:
        - oauth2: [announcements:write]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [subject, message]
              properties:
                subject:
                  type: string
                  minLength: 1
                  maxLength: 500
                message:
                  type: string
                  minLength: 1
                  maxLength: 50000
                  description: Plain-text body
                scheduledAt:
                  type: string
                  format: date-time
                  description: When to send; omit to leave as draft
                audienceTagIds:
                  type: array
                  items:
                    type: string
                    format: uuid
                  description: Contact tag IDs to scope recipients; omit for all contacts
      responses:
        "200":
          description: Announcement created
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                        format: uuid
                      subject:
                        type: string
                      status:
                        type: string
                      scheduledAt:
                        type: string
                        format: date-time
                        nullable: true
                      createdAt:
                        type: string
                        format: date-time
        "404":
          description: Tournament not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /tournaments/{id}/rsvps:
    get:
      operationId: getTournamentRsvps
      summary: Get RSVP responses
      description: RSVP counts by status and list of responders (name + status). Raw email is not returned.
      tags: [Tournaments]
      security:
        - oauth2: [rsvps:read]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: RSVPs
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    properties:
                      counts:
                        type: object
                        properties:
                          attending:
                            type: integer
                          declined:
                            type: integer
                          none:
                            type: integer
                          other:
                            type: integer
                          total:
                            type: integer
                      responders:
                        type: array
                        items:
                          type: object
                          properties:
                            id:
                              type: string
                              format: uuid
                            contactId:
                              type: string
                              format: uuid
                              nullable: true
                            name:
                              type: string
                              nullable: true
                            status:
                              type: string
                            respondedAt:
                              type: string
                              format: date-time
                              nullable: true
        "404":
          description: Tournament not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /tournaments/{id}/invitations:
    get:
      operationId: listTournamentInvitations
      summary: List tournament invitations
      tags: [Invitations]
      security:
        - oauth2: [invitations:read]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - $ref: "#/components/parameters/limitParam"
        - $ref: "#/components/parameters/cursorParam"
      responses:
        "200":
          description: Paginated invitation list
          content:
            application/json:
              schema:
                type: object
                required: [data, page]
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Invitation"
                  page:
                    $ref: "#/components/schemas/PageInfo"
        "404":
          description: Tournament not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

    post:
      operationId: createTournamentInvitation
      summary: Create a tournament invitation
      description: Creates an invitation (announcement draft) for the tournament.
      tags: [Invitations]
      security:
        - oauth2: [invitations:write]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: Idempotency-Key
          in: header
          schema:
            type: string
            maxLength: 128
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                subject:
                  type: string
                body_text:
                  type: string
                audience_tag_ids:
                  type: array
                  items:
                    type: string
                    format: uuid
      responses:
        "201":
          description: Invitation created
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    $ref: "#/components/schemas/Invitation"
        "404":
          description: Tournament not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  # ---------------------------------------------------------------------------
  # Registrations
  # ---------------------------------------------------------------------------

  /tournaments/{id}/registrations:
    get:
      operationId: listRegistrations
      summary: List registrations
      description: |
        Cursor-paginated list of registrations for the tournament.
        Pass `?format=csv` to download a CSV export instead.
      tags: [Registrations]
      security:
        - oauth2: [registrations:read]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - $ref: "#/components/parameters/limitParam"
        - $ref: "#/components/parameters/cursorParam"
        - name: format
          in: query
          schema:
            type: string
            enum: [csv]
          description: Pass `csv` to receive a CSV export instead of JSON
      responses:
        "200":
          description: Registrations (JSON or CSV depending on `format`)
          content:
            application/json:
              schema:
                type: object
                required: [data, page]
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Registration"
                  page:
                    $ref: "#/components/schemas/PageInfo"
            text/csv:
              schema:
                type: string
        "404":
          description: Tournament not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /tournaments/{id}/registrations/{rid}/refund:
    post:
      operationId: refundRegistration
      summary: Refund a registration
      description: |
        Issues a Stripe refund and cancels the registration. **Irreversible.**
        Idempotent — a registration already cancelled/refunded returns the current
        state without contacting Stripe again.
      tags: [Registrations]
      security:
        - oauth2: [registrations:write]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: rid
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Refund result
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    properties:
                      refunded:
                        type: boolean
                        description: True if a Stripe refund was issued now or was already issued previously
                      alreadyRefunded:
                        type: boolean
                        description: True when the registration was not in a refundable state (idempotent no-op)
        "404":
          description: Tournament or registration not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Registration cannot be refunded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  # ---------------------------------------------------------------------------
  # Invitations (standalone)
  # ---------------------------------------------------------------------------

  /invitations/{aid}/schedule:
    post:
      operationId: scheduleInvitation
      summary: Schedule an invitation
      description: |
        Materialize recipients and set `scheduled_at` so the send cron dispatches
        the invitation at that time. Omit `scheduled_at` to send immediately.
      tags: [Invitations]
      security:
        - oauth2: [invitations:write]
      parameters:
        - name: aid
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: Idempotency-Key
          in: header
          schema:
            type: string
            maxLength: 128
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                scheduled_at:
                  type: string
                  format: date-time
                  description: When to send; defaults to immediately (now) if omitted
      responses:
        "200":
          description: Invitation scheduled
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                        format: uuid
                      recipientCount:
                        type: integer
                      scheduledAt:
                        type: string
                        format: date-time
        "400":
          description: Missing subject or no eligible recipients
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Invitation not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Invitation already sent or currently sending
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /invitations/{aid}/stats:
    get:
      operationId: getInvitationStats
      summary: Get invitation delivery stats
      description: |
        Delivery counts by status. Pass `?format=csv` to download a CSV of all
        recipients (without emails). If truncated an `X-Truncated: true` header
        is added.
      tags: [Invitations]
      security:
        - oauth2: [invitations:read]
      parameters:
        - name: aid
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: format
          in: query
          schema:
            type: string
            enum: [csv]
          description: Pass `csv` to download a recipient CSV instead of JSON stats
      responses:
        "200":
          description: Delivery stats (JSON or CSV depending on `format`)
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    properties:
                      counts:
                        type: object
                        properties:
                          invited:
                            type: integer
                          pending:
                            type: integer
                          sent:
                            type: integer
                          delivered:
                            type: integer
                          opened:
                            type: integer
                          bounced:
                            type: integer
                          complained:
                            type: integer
                          failed:
                            type: integer
                          suppressed:
                            type: integer
                          attending:
                            type: integer
                          declined:
                            type: integer
            text/csv:
              schema:
                type: string
        "404":
          description: Invitation not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  # ---------------------------------------------------------------------------
  # Series
  # ---------------------------------------------------------------------------

  /series:
    get:
      operationId: listSeries
      summary: List series / seasons
      tags: [Series]
      security:
        - oauth2: [series:read]
      responses:
        "200":
          description: Series list
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                          format: uuid
                        name:
                          type: string
                        seasonYear:
                          type: integer
                          nullable: true
                        pointsMethod:
                          type: string
                        eventCount:
                          type: integer
                        createdAt:
                          type: string
                          format: date-time

  /series/{sid}/standings:
    get:
      operationId: getSeriesStandings
      summary: Get series standings
      description: Cumulative standings for one series, computed live from all series events.
      tags: [Series]
      security:
        - oauth2: [series:read]
      parameters:
        - name: sid
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Series standings
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    properties:
                      standings:
                        type: array
                        items:
                          type: object
                          properties:
                            rank:
                              type: integer
                            entityId:
                              type: string
                              format: uuid
                            name:
                              type: string
                            totalPoints:
                              type: number
                            eventsPlayed:
                              type: integer
                            perEvent:
                              type: array
                              items:
                                type: object
                      events:
                        type: array
                        items:
                          type: object
                          properties:
                            seriesEventId:
                              type: string
                              format: uuid
                            tournamentId:
                              type: string
                              format: uuid
                            tournamentName:
                              type: string
                            tournamentDate:
                              type: string
                              nullable: true
                            sequence:
                              type: integer
                            pointsWeight:
                              type: number
                            status:
                              type: string
        "404":
          description: Series not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /series/{sid}/head-to-head:
    get:
      operationId: getSeriesHeadToHead
      summary: Get series head-to-head records
      description: >
        Head-to-head records between entrants across one series, computed live
        from the same per-event results as the standings. A "win" is a better
        finish than the opponent in a shared event; equal finishes are ties.
        Averages are the per-event scores across the events both entrants played.
      tags: [Series]
      security:
        - oauth2: [series:read]
      parameters:
        - name: sid
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Series head-to-head records
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    properties:
                      pairs:
                        type: array
                        items:
                          type: object
                          properties:
                            entrantA:
                              type: object
                              properties:
                                entityId:
                                  type: string
                                name:
                                  type: string
                            entrantB:
                              type: object
                              properties:
                                entityId:
                                  type: string
                                name:
                                  type: string
                            meetings:
                              type: integer
                            aWins:
                              type: integer
                            bWins:
                              type: integer
                            ties:
                              type: integer
                            aAvg:
                              type: number
                            bAvg:
                              type: number
                      summary:
                        type: array
                        items:
                          type: object
                          properties:
                            entityId:
                              type: string
                            name:
                              type: string
                            wins:
                              type: integer
                            losses:
                              type: integer
                            ties:
                              type: integer
                            meetings:
                              type: integer
        "404":
          description: Series not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  # ---------------------------------------------------------------------------
  # Webhooks
  # ---------------------------------------------------------------------------

  /webhooks:
    get:
      operationId: listWebhookEndpoints
      summary: List webhook endpoints
      tags: [Webhooks]
      security:
        - oauth2: [webhooks:read]
      parameters:
        - $ref: "#/components/parameters/limitParam"
        - $ref: "#/components/parameters/cursorParam"
      responses:
        "200":
          description: Paginated endpoint list
          content:
            application/json:
              schema:
                type: object
                required: [data, page]
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/WebhookEndpoint"
                  page:
                    $ref: "#/components/schemas/PageInfo"

    post:
      operationId: createWebhookEndpoint
      summary: Create a webhook endpoint
      description: |
        The `signing_secret` is returned **once** at creation. Store it securely —
        it cannot be retrieved later.
      tags: [Webhooks]
      security:
        - oauth2: [webhooks:write]
      parameters:
        - name: Idempotency-Key
          in: header
          schema:
            type: string
            maxLength: 128
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url:
                  type: string
                  format: uri
                description:
                  type: string
                event_types:
                  type: array
                  items:
                    type: string
      responses:
        "201":
          description: Endpoint created (`signing_secret` included once)
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    allOf:
                      - $ref: "#/components/schemas/WebhookEndpoint"
                      - type: object
                        properties:
                          signing_secret:
                            type: string
                            description: HMAC signing secret — shown once, store securely
        "400":
          description: Validation error (missing or invalid URL)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Endpoint with this URL already exists
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /webhooks/{id}:
    get:
      operationId: getWebhookEndpoint
      summary: Get a webhook endpoint
      tags: [Webhooks]
      security:
        - oauth2: [webhooks:read]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Endpoint
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    $ref: "#/components/schemas/WebhookEndpoint"
        "404":
          description: Endpoint not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

    patch:
      operationId: updateWebhookEndpoint
      summary: Update a webhook endpoint
      description: |
        Update URL, description, event_types, or status. Pass
        `{"status":"enabled"}` to re-enable a disabled endpoint (also resets the
        consecutive-failure counter).
      tags: [Webhooks]
      security:
        - oauth2: [webhooks:write]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                url:
                  type: string
                  format: uri
                description:
                  type: string
                event_types:
                  type: array
                  items:
                    type: string
                status:
                  type: string
                  enum: [enabled, disabled]
      responses:
        "200":
          description: Endpoint updated
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    $ref: "#/components/schemas/WebhookEndpoint"
        "404":
          description: Endpoint not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

    delete:
      operationId: deleteWebhookEndpoint
      summary: Delete a webhook endpoint
      tags: [Webhooks]
      security:
        - oauth2: [webhooks:write]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Endpoint deleted
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    properties:
                      deleted:
                        type: boolean
                        example: true
        "404":
          description: Endpoint not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /webhooks/{id}/rotate-secret:
    post:
      operationId: rotateWebhookSecret
      summary: Rotate signing secret
      description: |
        Generate a new signing secret. The new secret is returned **once** — store
        it securely. The old secret is immediately invalidated.
      tags: [Webhooks]
      security:
        - oauth2: [webhooks:write]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: Idempotency-Key
          in: header
          schema:
            type: string
            maxLength: 128
      responses:
        "200":
          description: New signing secret
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    properties:
                      signing_secret:
                        type: string
                        description: New HMAC signing secret — shown once, store securely
        "404":
          description: Endpoint not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /webhooks/{id}/deliveries:
    get:
      operationId: listWebhookDeliveries
      summary: List webhook deliveries
      tags: [Webhooks]
      security:
        - oauth2: [webhooks:read]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - $ref: "#/components/parameters/limitParam"
        - $ref: "#/components/parameters/cursorParam"
        - name: status
          in: query
          schema:
            type: string
            enum: [pending, delivered, failed, dead]
          description: Filter by delivery status
      responses:
        "200":
          description: Paginated delivery list
          content:
            application/json:
              schema:
                type: object
                required: [data, page]
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/WebhookDelivery"
                  page:
                    $ref: "#/components/schemas/PageInfo"

  /webhooks/{id}/deliveries/{did}/replay:
    post:
      operationId: replayWebhookDelivery
      summary: Replay a webhook delivery
      description: Re-send a delivery (useful for failed or dead deliveries). Returns `202 Accepted`.
      tags: [Webhooks]
      security:
        - oauth2: [webhooks:write]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: did
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: Idempotency-Key
          in: header
          schema:
            type: string
            maxLength: 128
      responses:
        "202":
          description: Delivery queued for replay
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    $ref: "#/components/schemas/WebhookDelivery"
        "404":
          description: Endpoint or delivery not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  # ── Holes config ────────────────────────────────────────────────────────────

  /tournaments/{id}/holes:
    get:
      operationId: getTournamentHoles
      summary: Get hole configuration
      description: >
        Returns all hole rows for the tournament ordered by hole number: par,
        stroke index (difficulty rank), yardage, and any on-course contest.
      tags: [Tournaments]
      security:
        - oauth2: [tournaments:read]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Hole configuration list
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/HoleConfig"
        "404":
          description: Tournament not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    patch:
      operationId: updateTournamentHoles
      summary: Update hole configuration
      description: >
        Bulk-update hole configuration. Supply an array of entries; each must
        include `holeNumber` (1-based) plus at least one of `par`, `strokeIndex`,
        `yardage`, or `contest`. Holes omitted are untouched. Pass `null` for
        `strokeIndex`, `yardage`, or `contest` to clear that field.
      tags: [Tournaments]
      security:
        - oauth2: [tournaments:write]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [holes]
              properties:
                holes:
                  type: array
                  minItems: 1
                  maxItems: 18
                  items:
                    type: object
                    required: [holeNumber]
                    properties:
                      holeNumber:
                        type: integer
                        minimum: 1
                        maximum: 18
                        description: 1-based hole number.
                      par:
                        type: integer
                        minimum: 1
                        maximum: 8
                      strokeIndex:
                        type: integer
                        minimum: 1
                        maximum: 18
                        nullable: true
                        description: Difficulty rank. Pass `null` to clear.
                      yardage:
                        type: integer
                        minimum: 1
                        maximum: 1000
                        nullable: true
                        description: Yardage. Pass `null` to clear.
                      contest:
                        type: string
                        enum: [longest_drive, closest_to_pin, straightest_drive]
                        nullable: true
                        description: On-course contest. Pass `null` to clear.
            example:
              holes:
                - holeNumber: 1
                  par: 4
                  strokeIndex: 1
                  yardage: 380
                - holeNumber: 7
                  contest: longest_drive
      responses:
        "200":
          description: Holes updated
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    required: [updated]
                    properties:
                      updated:
                        type: integer
                        description: Number of hole entries processed.
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Tournament not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  # ── Team bulk operations ─────────────────────────────────────────────────────

  /tournaments/{id}/teams/generate:
    post:
      operationId: generateTournamentTeams
      summary: Bulk-generate teams
      description: >
        Generate teams in the standard scramble pattern
        (`numHoles` × `teamsPerHole` A/B/C/D starting positions).
        Clears any existing teams first. Refuses if any score has been entered.
      tags: [Tournaments]
      security:
        - oauth2: [teams:write]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                numHoles:
                  type: integer
                  minimum: 1
                  maximum: 18
                  default: 18
                  description: How many starting holes to spread teams across.
                teamsPerHole:
                  type: integer
                  minimum: 1
                  maximum: 4
                  default: 2
                  description: Teams per starting hole (generates A/B/C/D slots).
            example:
              numHoles: 18
              teamsPerHole: 2
      responses:
        "200":
          description: Teams generated
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    required: [created]
                    properties:
                      created:
                        type: integer
                        description: Number of teams created.
                      skipped:
                        type: integer
                        description: Slots skipped due to access-code generation failures (normally 0).
        "404":
          description: Tournament not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Scores already exist — teams cannot be regenerated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /tournaments/{id}/teams/clear:
    post:
      operationId: clearTournamentTeams
      summary: Clear all teams
      description: >
        Remove all teams from the tournament. Refuses if any score has been
        entered. This action is permanent and cannot be undone.
      tags: [Tournaments]
      security:
        - oauth2: [teams:write]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: All teams removed
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    required: [cleared]
                    properties:
                      cleared:
                        type: boolean
        "404":
          description: Tournament not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Scores already exist — teams cannot be cleared
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  # ── Pairings draw ─────────────────────────────────────────────────────────────

  /tournaments/{id}/draw:
    post:
      operationId: assignTournamentDraw
      summary: Auto-assign starting holes (draw)
      description: >
        Auto-assign balanced starting holes and A/B/C/D sequences to all
        existing teams using the pairing algorithm. Refuses if any score
        has been entered. Returns `assigned: 0` when no teams exist yet.
      tags: [Tournaments]
      security:
        - oauth2: [teams:write]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Starting holes assigned
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    required: [assigned]
                    properties:
                      assigned:
                        type: integer
                        description: Number of teams assigned a starting hole.
        "404":
          description: Tournament not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Scores already exist — draw cannot be re-run
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  # ── Scoring config ────────────────────────────────────────────────────────────

  /tournaments/{id}/scoring:
    patch:
      operationId: updateTournamentScoring
      summary: Update scoring configuration
      description: >
        Update the leaderboard and scoring configuration: which boards to
        show, scoring format, best-ball count, custom stableford points, or
        max-score cap. Pass only the fields to change — at least one is
        required. Busts leaderboard caches so the live board updates immediately.
      tags: [Tournaments]
      security:
        - oauth2: [tournaments:write]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                boards:
                  type: array
                  minItems: 1
                  items:
                    type: string
                    enum: [gross, net, stableford]
                  description: Which leaderboard views to enable.
                format:
                  type: string
                  enum: [scramble, best_ball, individual]
                  description: Scoring format.
                bestBallCount:
                  type: integer
                  minimum: 1
                  maximum: 4
                  description: For `best_ball` format — how many scores count per hole.
                maxScoreMode:
                  type: string
                  enum: [none, bogey, double_bogey, triple_bogey, max_strokes]
                  description: Max-score cap mode. `none` disables capping.
                maxScoreValue:
                  type: integer
                  minimum: 1
                  maximum: 30
                  nullable: true
                  description: For `max_strokes` mode — the stroke cap.
            example:
              boards: [gross, net]
              format: scramble
              maxScoreMode: double_bogey
      responses:
        "200":
          description: Scoring configuration updated
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    required: [updated]
                    properties:
                      updated:
                        type: boolean
        "400":
          description: Validation error (no fields supplied)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Tournament not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  # ── Event-days config ─────────────────────────────────────────────────────────

  /tournaments/{id}/event-days:
    patch:
      operationId: updateTournamentEventDays
      summary: Update event-day configuration
      description: >
        Update the multi-day configuration: how many days the event runs
        (1 or 2) and which day is currently active. Setting `eventDays` to 1
        disables all multi-day surfaces. Clamps `activeDay` to 1..`eventDays`.
        Moves teams on now-out-of-range days back to day 1. Busts leaderboard
        and map-score caches.
      tags: [Tournaments]
      security:
        - oauth2: [tournaments:write]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [eventDays]
              properties:
                eventDays:
                  type: integer
                  minimum: 1
                  maximum: 2
                  description: How many days the event runs.
                activeDay:
                  type: integer
                  minimum: 1
                  maximum: 2
                  description: Which day is currently active (1..eventDays). Defaults to 1.
            example:
              eventDays: 2
              activeDay: 2
      responses:
        "200":
          description: Event-day configuration updated
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    required: [eventDays, activeDay]
                    properties:
                      eventDays:
                        type: integer
                      activeDay:
                        type: integer
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Tournament not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  # ── Audit log ──────────────────────────────────────────────────────────────────

  /tournaments/{id}/audit:
    get:
      operationId: getTournamentAuditLog
      summary: Get audit log
      description: >
        Cursor-paginated list of audit events for the tournament, most-recent
        first. Each entry records who changed what and when — status changes,
        team edits, score corrections, check-ins, and more.
      tags: [Tournaments]
      security:
        - oauth2: [audit:read]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - $ref: "#/components/parameters/limitParam"
        - $ref: "#/components/parameters/cursorParam"
      responses:
        "200":
          description: Paginated audit event list
          content:
            application/json:
              schema:
                type: object
                required: [data, page]
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/AuditEvent"
                  page:
                    $ref: "#/components/schemas/PageInfo"
        "404":
          description: Tournament not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

tags:
  - name: Auth
    description: OAuth 2.0 token issuance and revocation
  - name: Contacts
    description: Contact management
  - name: Tags
    description: Contact tags
  - name: Tournaments
    description: Tournament read and write operations
  - name: Registrations
    description: Tournament registration management
  - name: Invitations
    description: Email invitation management
  - name: Series
    description: Series / season standings
  - name: Webhooks
    description: Outbound webhook endpoints and deliveries
