openapi: "3.0.3"

info:
  title: apps.tlehane.com API
  version: "1.0.0"
  description: >
    All applications hosted at apps.tlehane.com via Cloudflare Tunnel → nginx → gunicorn.
    Two apps are documented here: the BPD Incident Dashboard (/dashboard) and
    ChoreTracker (/chores).

servers:
  - url: https://apps.tlehane.com
    description: Production (Cloudflare Tunnel → nginx → gunicorn)

tags:
  # ── BPD Dashboard ────────────────────────────────────────────────────────────
  - name: BPD Auth
    description: Turnstile access gate
  - name: BPD UI
    description: Server-rendered HTML pages
  - name: BPD Export
    description: Data export endpoints
  - name: BPD API
    description: JSON data API

  # ── ChoreTracker ─────────────────────────────────────────────────────────────
  - name: CT Auth
    description: Registration, password login (admin/superuser), Google OAuth, token refresh
  - name: CT Families
    description: Family membership management
  - name: CT Chores
    description: Chore definitions and the assign → complete → approve/reject lifecycle
  - name: CT Rewards
    description: Reward catalog and the redeem → approve/reject lifecycle
  - name: CT Admin
    description: Cross-family admin operations (admin/superuser roles only)

paths:

  # ════════════════════════════════════════════════════════════════════════════
  # BPD Incident Dashboard  (/dashboard/*)
  # ════════════════════════════════════════════════════════════════════════════

  # ─── BPD Auth ────────────────────────────────────────────────────────────────

  /dashboard/gate:
    get:
      operationId: bpdGetGate
      summary: Access gate
      description: Shows the Cloudflare Turnstile challenge. Unauthenticated requests to any other route redirect here.
      tags: [BPD Auth]
      responses:
        "200":
          description: HTML page with embedded Turnstile widget
          content:
            text/html:
              schema:
                type: string

  /dashboard/gate/verify:
    post:
      operationId: bpdPostGateVerify
      summary: Verify Turnstile token
      description: >
        Accepts the Turnstile widget token as a form POST (cf-turnstile-response field),
        validates it server-side via siteverify, and creates a 12-hour signed session
        cookie on success. Form submission only — not a JSON endpoint.
      tags: [BPD Auth]
      responses:
        "302":
          description: Redirect to /incidents on success; redirect back to /gate on failure
          headers:
            Location:
              schema:
                type: string
            Set-Cookie:
              description: Signed session cookie (session=…; HttpOnly; SameSite=Lax)
              schema:
                type: string

  # ─── BPD UI pages ────────────────────────────────────────────────────────────

  /dashboard/incidents:
    get:
      operationId: bpdGetIncidentsPage
      summary: Incidents table
      description: Paginated HTML table of incidents with date-range and type filters.
      tags: [BPD UI]
      parameters:
        - $ref: '#/components/parameters/BPDStartDate'
        - $ref: '#/components/parameters/BPDEndDate'
        - $ref: '#/components/parameters/BPDIncidentType'
        - name: page
          in: query
          description: Page number (1-based)
          schema:
            type: integer
            minimum: 1
            default: 1
      responses:
        "200":
          description: HTML incidents page
          content:
            text/html:
              schema:
                type: string
        "302":
          description: Redirect to /gate if session is not verified

  /dashboard/stats:
    get:
      operationId: bpdGetStatsPage
      summary: Statistics page
      description: HTML page with Chart.js bar and line charts powered by /api/stats.
      tags: [BPD UI]
      responses:
        "200":
          description: HTML stats page
          content:
            text/html:
              schema:
                type: string
        "302":
          description: Redirect to /gate if session is not verified

  /dashboard/map:
    get:
      operationId: bpdGetMapPage
      summary: Incident map
      description: HTML page with a Leaflet.js map and marker clusters powered by /api/map-points.
      tags: [BPD UI]
      responses:
        "200":
          description: HTML map page
          content:
            text/html:
              schema:
                type: string
        "302":
          description: Redirect to /gate if session is not verified

  # ─── BPD Export ──────────────────────────────────────────────────────────────

  /dashboard/export/csv:
    get:
      operationId: bpdExportCsv
      summary: Export incidents as CSV
      description: Streams a CSV download using the same date-range and type filters as the incidents page. Includes latitude/longitude columns.
      tags: [BPD Export]
      parameters:
        - $ref: '#/components/parameters/BPDStartDate'
        - $ref: '#/components/parameters/BPDEndDate'
        - $ref: '#/components/parameters/BPDIncidentType'
      responses:
        "200":
          description: CSV file
          headers:
            Content-Disposition:
              schema:
                type: string
                example: attachment; filename=bpd_incidents_2025-01-01_2025-01-31.csv
          content:
            text/csv:
              schema:
                type: string

  # ─── BPD JSON API ────────────────────────────────────────────────────────────

  /dashboard/api/incidents:
    get:
      operationId: bpdApiListIncidents
      summary: List incidents
      description: >
        Returns a paginated list of incidents. Defaults to the last 30 days.
        Maximum 1,000 records per request.
      tags: [BPD API]
      parameters:
        - $ref: '#/components/parameters/BPDStartDate'
        - $ref: '#/components/parameters/BPDEndDate'
        - $ref: '#/components/parameters/BPDIncidentType'
        - name: limit
          in: query
          description: Records per page (1–1000)
          schema:
            type: integer
            minimum: 1
            maximum: 1000
            default: 100
        - name: offset
          in: query
          description: Pagination offset
          schema:
            type: integer
            minimum: 0
            default: 0
      responses:
        "200":
          description: Paginated incident list
          content:
            application/json:
              schema:
                type: object
                required: [total, limit, offset, data]
                properties:
                  total:
                    type: integer
                    description: Total matching incidents (ignoring limit/offset)
                  limit:
                    type: integer
                  offset:
                    type: integer
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Incident'
              example:
                total: 423
                limit: 100
                offset: 0
                data:
                  - id: 9981
                    incident_date: "2025-04-28"
                    incident_time: "14:32"
                    case_number: "25-12345"
                    location: "100 N COMMERCIAL ST, BELLINGHAM WA 98225"
                    incident_type: "THEFT"
                    description: "Shoplifting"
                    geocode_status: "ok"
                    location_lat: 48.7519
                    location_lon: -122.4787

  /dashboard/api/incident:
    get:
      operationId: bpdApiGetIncident
      summary: Get single incident
      description: Returns one incident by database ID.
      tags: [BPD API]
      parameters:
        - name: id
          in: query
          required: true
          description: Incident database ID
          schema:
            type: integer
      responses:
        "200":
          description: Incident object
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Incident'
        "400":
          description: Missing or non-integer id parameter
        "404":
          description: No incident with that ID

  /dashboard/api/stats:
    get:
      operationId: bpdApiGetStats
      summary: Incident statistics
      description: Returns incident counts grouped by type (top 20) and by calendar day, with optional filters.
      tags: [BPD API]
      parameters:
        - $ref: '#/components/parameters/BPDStartDate'
        - $ref: '#/components/parameters/BPDEndDate'
        - $ref: '#/components/parameters/BPDIncidentType'
      responses:
        "200":
          description: Stats by type and by day
          content:
            application/json:
              schema:
                type: object
                required: [by_type, by_day]
                properties:
                  by_type:
                    type: array
                    description: Top 20 incident types by count, descending
                    items:
                      type: object
                      required: [incident_type, count]
                      properties:
                        incident_type:
                          type: string
                        count:
                          type: integer
                  by_day:
                    type: array
                    description: Daily counts in ascending date order
                    items:
                      type: object
                      required: [date, count]
                      properties:
                        date:
                          type: string
                          format: date
                        count:
                          type: integer

  /dashboard/api/map-points:
    get:
      operationId: bpdApiGetMapPoints
      summary: Geocoded map points
      description: >
        Returns lightweight geocoded incident points for Leaflet marker clusters.
        Only includes incidents with geocode_status=ok. Maximum 15,000 points.
      tags: [BPD API]
      parameters:
        - $ref: '#/components/parameters/BPDStartDate'
        - $ref: '#/components/parameters/BPDEndDate'
        - $ref: '#/components/parameters/BPDIncidentType'
      responses:
        "200":
          description: Array of map points
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/MapPoint'

  /dashboard/api/incident-types:
    get:
      operationId: bpdApiGetIncidentTypes
      summary: Distinct incident types
      description: Returns all distinct incident types ordered by frequency. Restricted to geocoded incidents (geocode_status=ok).
      tags: [BPD API]
      responses:
        "200":
          description: Incident types with counts
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  required: [type, count]
                  properties:
                    type:
                      type: string
                    count:
                      type: integer


  # ════════════════════════════════════════════════════════════════════════════
  # ChoreTracker  (/chores/*)
  # ════════════════════════════════════════════════════════════════════════════

  /chores/api/health:
    get:
      operationId: ctHealth
      summary: Health check
      description: Returns 200 when the app is running. Used by CI deploy verification.
      tags: [CT Auth]
      responses:
        "200":
          description: App is healthy
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: ok

  # ─── CT Auth ─────────────────────────────────────────────────────────────────

  /chores/api/auth/register:
    post:
      operationId: ctRegister
      summary: Register family + first parent
      description: >
        Creates a new Family and its first parent User in one transaction.
        No authentication required.
      tags: [CT Auth]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [family_name, username, password]
              properties:
                family_name:
                  type: string
                  example: Smith Family
                username:
                  type: string
                  example: tyler
                password:
                  type: string
                  format: password
      responses:
        "201":
          description: Family and user created
          content:
            application/json:
              schema:
                type: object
                properties:
                  family:
                    $ref: '#/components/schemas/CTFamily'
                  user:
                    $ref: '#/components/schemas/CTUser'
        "400":
          $ref: '#/components/responses/CTBadRequest'

  /chores/api/auth/login:
    post:
      operationId: ctLogin
      summary: Password login (admin/superuser only)
      description: >
        Issues a JWT access + refresh token pair. Parent and child accounts
        receive 403 — they authenticate via Google OAuth (/api/auth/google).
        Optionally accepts family_id to disambiguate usernames that exist in
        multiple families.
      tags: [CT Auth]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [username, password]
              properties:
                username:
                  type: string
                password:
                  type: string
                  format: password
                family_id:
                  type: integer
                  description: Required only when the same username exists in multiple families
      responses:
        "200":
          description: Tokens issued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CTTokenResponse'
        "400":
          $ref: '#/components/responses/CTBadRequest'
        "401":
          description: Invalid credentials
        "403":
          description: Password login not available for parent/child roles

  /chores/api/auth/google:
    post:
      operationId: ctGoogleLogin
      summary: Google OAuth login
      description: >
        Verifies a Google ID token and issues a JWT pair.
        The Google sub must already be associated with a User record (Phase 5 adds auto-create).
        Parent and child accounts use this endpoint; admin/superuser use /api/auth/login.
      tags: [CT Auth]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [id_token]
              properties:
                id_token:
                  type: string
                  description: Google ID token from the client-side OAuth flow
      responses:
        "200":
          description: Tokens issued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CTTokenResponse'
        "400":
          $ref: '#/components/responses/CTBadRequest'
        "401":
          description: Invalid or expired Google token
        "404":
          description: No account found for this Google identity

  /chores/api/auth/google/callback:
    get:
      operationId: ctGoogleCallback
      summary: Google OAuth Authorization Code callback
      description: >
        Server-side OAuth callback. Exchanges the authorization code for a Google ID token,
        then runs three-path identity resolution:
        (1) sub lookup — existing user with this Google sub;
        (2) invite claim — pending user with matching google_email;
        (3) auto-create — new parent account for an unrecognized Google identity.
        On success, redirects the browser to the token_relay page with
        access=…&refresh=…&role=…&state=… in the URL fragment.
        On failure, redirects to the login page with an error query param.
      tags: [CT Auth]
      parameters:
        - name: code
          in: query
          required: true
          description: OAuth authorization code from Google
          schema:
            type: string
        - name: state
          in: query
          description: CSRF nonce passed through from the initial auth request
          schema:
            type: string
      responses:
        "302":
          description: >
            Redirect to /chores/token_relay#access=…&refresh=…&role=…&state=… on success,
            or /chores/login?error=… on failure.
          headers:
            Location:
              schema:
                type: string

  /chores/api/auth/refresh:
    post:
      operationId: ctRefresh
      summary: Refresh access token
      description: Exchanges a valid refresh token for a new access token.
      tags: [CT Auth]
      security:
        - CTBearerRefresh: []
      responses:
        "200":
          description: New access token
          content:
            application/json:
              schema:
                type: object
                properties:
                  access_token:
                    type: string
        "401":
          description: Missing or invalid refresh token

  /chores/api/auth/me:
    get:
      operationId: ctMe
      summary: Get current user
      description: Returns the authenticated user's profile fields.
      tags: [CT Auth]
      security:
        - CTBearer: []
      responses:
        "200":
          description: Current user
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CTUser'
        "401":
          $ref: '#/components/responses/CTUnauthorized'

  # ─── CT Families ─────────────────────────────────────────────────────────────

  /chores/api/families/{family_id}/members:
    get:
      operationId: ctListMembers
      summary: List family members
      description: Returns all users in the family. Accessible by members of that family or superusers.
      tags: [CT Families]
      security:
        - CTBearer: []
      parameters:
        - $ref: '#/components/parameters/CTFamilyId'
      responses:
        "200":
          description: Member list
          content:
            application/json:
              schema:
                type: object
                properties:
                  members:
                    type: array
                    items:
                      $ref: '#/components/schemas/CTUser'
        "401":
          $ref: '#/components/responses/CTUnauthorized'
        "403":
          $ref: '#/components/responses/CTForbidden'

    post:
      operationId: ctAddMember
      summary: Add member to family
      description: >
        Parents only. Adds a new parent or child account to the family.
        Admin/superuser accounts cannot be created via this endpoint.
      tags: [CT Families]
      security:
        - CTBearer: []
      parameters:
        - $ref: '#/components/parameters/CTFamilyId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [username, password]
              properties:
                username:
                  type: string
                password:
                  type: string
                  format: password
                role:
                  type: string
                  enum: [parent, child]
                  default: child
      responses:
        "201":
          description: User created
          content:
            application/json:
              schema:
                type: object
                properties:
                  user:
                    $ref: '#/components/schemas/CTUser'
        "400":
          $ref: '#/components/responses/CTBadRequest'
        "401":
          $ref: '#/components/responses/CTUnauthorized'
        "403":
          $ref: '#/components/responses/CTForbidden'

  /chores/api/families/{family_id}/invite:
    post:
      operationId: ctInviteMember
      summary: Invite member by Gmail address
      description: >
        Parents only. Creates a pending user record linked to the given Gmail address.
        The invitee signs in with Google and the account is claimed automatically on first login.
        Role defaults to child; pass role=parent to invite another parent.
      tags: [CT Families]
      security:
        - CTBearer: []
      parameters:
        - $ref: '#/components/parameters/CTFamilyId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email:
                  type: string
                  format: email
                  description: Gmail address of the person being invited
                role:
                  type: string
                  enum: [parent, child]
                  default: child
      responses:
        "201":
          description: Invitation created
          content:
            application/json:
              schema:
                type: object
                properties:
                  user:
                    $ref: '#/components/schemas/CTUser'
        "400":
          $ref: '#/components/responses/CTBadRequest'
        "401":
          $ref: '#/components/responses/CTUnauthorized'
        "403":
          $ref: '#/components/responses/CTForbidden'
        "409":
          description: An account with that Gmail address already exists in this family

  /chores/api/families/{family_id}/members/{member_id}:
    patch:
      operationId: ctUpdateMember
      summary: Update member display name
      description: >
        Parents may update any family member's display name.
        Children may update only their own display name.
      tags: [CT Families]
      security:
        - CTBearer: []
      parameters:
        - $ref: '#/components/parameters/CTFamilyId'
        - name: member_id
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [display_name]
              properties:
                display_name:
                  type: string
                  example: Tyler
      responses:
        "200":
          description: Member updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  user:
                    $ref: '#/components/schemas/CTUser'
        "400":
          $ref: '#/components/responses/CTBadRequest'
        "401":
          $ref: '#/components/responses/CTUnauthorized'
        "403":
          $ref: '#/components/responses/CTForbidden'
        "404":
          description: Member not found

    delete:
      operationId: ctRemoveMember
      summary: Remove member from family
      description: >
        Parents only. Removes any family member except themselves.
        Immediately revokes access.
      tags: [CT Families]
      security:
        - CTBearer: []
      parameters:
        - $ref: '#/components/parameters/CTFamilyId'
        - name: member_id
          in: path
          required: true
          schema:
            type: integer
      responses:
        "204":
          description: Member removed
        "400":
          description: Cannot remove yourself
        "401":
          $ref: '#/components/responses/CTUnauthorized'
        "403":
          $ref: '#/components/responses/CTForbidden'
        "404":
          description: Member not found

  # ─── CT Chores ───────────────────────────────────────────────────────────────

  /chores/api/chores/:
    get:
      operationId: ctListChores
      summary: List chores
      description: Returns all chore definitions for the caller's family. Superusers may pass ?family_id= to scope to another family.
      tags: [CT Chores]
      security:
        - CTBearer: []
      parameters:
        - $ref: '#/components/parameters/CTSuperuserFamilyId'
      responses:
        "200":
          description: Chore list
          content:
            application/json:
              schema:
                type: object
                properties:
                  chores:
                    type: array
                    items:
                      $ref: '#/components/schemas/CTChore'
        "401":
          $ref: '#/components/responses/CTUnauthorized'

    post:
      operationId: ctCreateChore
      summary: Create chore
      description: Parents only. Creates a new chore definition for the family.
      tags: [CT Chores]
      security:
        - CTBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [title]
              properties:
                title:
                  type: string
                  example: Wash the dishes
                description:
                  type: string
                  nullable: true
                points:
                  type: integer
                  default: 10
                  description: Points awarded on approval
      responses:
        "201":
          description: Chore created
          content:
            application/json:
              schema:
                type: object
                properties:
                  chore:
                    $ref: '#/components/schemas/CTChore'
        "400":
          $ref: '#/components/responses/CTBadRequest'
        "401":
          $ref: '#/components/responses/CTUnauthorized'
        "403":
          $ref: '#/components/responses/CTForbidden'

  /chores/api/chores/assignments/:
    get:
      operationId: ctListAssignments
      summary: List chore assignments
      description: >
        Returns assignments for the caller's family, ordered by assigned_at desc.
        Children see only their own assignments. Filterable by status.
      tags: [CT Chores]
      security:
        - CTBearer: []
      parameters:
        - $ref: '#/components/parameters/CTSuperuserFamilyId'
        - name: status
          in: query
          description: Filter by assignment status
          schema:
            type: string
            enum: [pending, completed, approved, rejected]
      responses:
        "200":
          description: Assignment list
          content:
            application/json:
              schema:
                type: object
                properties:
                  assignments:
                    type: array
                    items:
                      $ref: '#/components/schemas/CTChoreAssignment'
        "401":
          $ref: '#/components/responses/CTUnauthorized'

  /chores/api/chores/{chore_id}/assign:
    post:
      operationId: ctAssignChore
      summary: Assign chore to child
      description: Parents only. Creates a new pending assignment for the specified child.
      tags: [CT Chores]
      security:
        - CTBearer: []
      parameters:
        - name: chore_id
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [child_id]
              properties:
                child_id:
                  type: integer
                  description: User ID of the child to assign the chore to
      responses:
        "201":
          description: Assignment created
          content:
            application/json:
              schema:
                type: object
                properties:
                  assignment:
                    $ref: '#/components/schemas/CTChoreAssignment'
        "400":
          $ref: '#/components/responses/CTBadRequest'
        "401":
          $ref: '#/components/responses/CTUnauthorized'
        "403":
          $ref: '#/components/responses/CTForbidden'
        "404":
          description: Chore or child not found

  /chores/api/chores/assignments/{assignment_id}/complete:
    post:
      operationId: ctCompleteAssignment
      summary: Mark assignment complete
      description: >
        Child only. Marks their own pending assignment as completed.
        Transitions status: pending → completed.
      tags: [CT Chores]
      security:
        - CTBearer: []
      parameters:
        - $ref: '#/components/parameters/CTAssignmentId'
      responses:
        "200":
          description: Assignment updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  assignment:
                    $ref: '#/components/schemas/CTChoreAssignment'
        "401":
          $ref: '#/components/responses/CTUnauthorized'
        "403":
          $ref: '#/components/responses/CTForbidden'
        "404":
          description: Assignment not found
        "409":
          description: Assignment already resolved (not in pending state)

  /chores/api/chores/assignments/{assignment_id}/approve:
    post:
      operationId: ctResolveAssignment
      summary: Approve or reject completed assignment
      description: >
        Parent only. Resolves a completed assignment.
        action=approve awards points to the child and transitions to approved.
        action=reject transitions to rejected with no point change.
        Transitions status: completed → approved | rejected.
      tags: [CT Chores]
      security:
        - CTBearer: []
      parameters:
        - $ref: '#/components/parameters/CTAssignmentId'
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                action:
                  type: string
                  enum: [approve, reject]
                  default: approve
      responses:
        "200":
          description: Assignment resolved
          content:
            application/json:
              schema:
                type: object
                properties:
                  assignment:
                    $ref: '#/components/schemas/CTChoreAssignment'
                  child_points:
                    type: integer
                    description: Child's updated point balance
        "401":
          $ref: '#/components/responses/CTUnauthorized'
        "403":
          $ref: '#/components/responses/CTForbidden'
        "404":
          description: Assignment not found
        "409":
          description: Assignment not in completed state

  /chores/api/chores/{chore_id}:
    patch:
      operationId: ctUpdateChore
      summary: Update chore
      description: Parents only. Updates the title and/or point value of an existing chore definition.
      tags: [CT Chores]
      security:
        - CTBearer: []
      parameters:
        - name: chore_id
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                title:
                  type: string
                points:
                  type: integer
                  minimum: 0
      responses:
        "200":
          description: Chore updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  chore:
                    $ref: '#/components/schemas/CTChore'
        "400":
          $ref: '#/components/responses/CTBadRequest'
        "401":
          $ref: '#/components/responses/CTUnauthorized'
        "403":
          $ref: '#/components/responses/CTForbidden'
        "404":
          description: Chore not found

    delete:
      operationId: ctDeleteChore
      summary: Delete chore
      description: Parents only. Deletes the chore definition and all its assignments.
      tags: [CT Chores]
      security:
        - CTBearer: []
      parameters:
        - name: chore_id
          in: path
          required: true
          schema:
            type: integer
      responses:
        "204":
          description: Chore deleted
        "401":
          $ref: '#/components/responses/CTUnauthorized'
        "403":
          $ref: '#/components/responses/CTForbidden'
        "404":
          description: Chore not found

  /chores/api/chores/history/:
    get:
      operationId: ctChoreHistory
      summary: Chore completion history
      description: >
        Returns a paginated history of resolved (approved or rejected) chore assignments
        for the caller's family, ordered by most recently resolved.
        Children see only their own history. Parents see the whole family.
        Maximum 200 records per page.
      tags: [CT Chores]
      security:
        - CTBearer: []
      parameters:
        - name: limit
          in: query
          description: Number of records to return (max 200, default 20)
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 20
        - name: offset
          in: query
          description: Number of records to skip for pagination
          schema:
            type: integer
            minimum: 0
            default: 0
      responses:
        "200":
          description: Paginated chore history
          content:
            application/json:
              schema:
                type: object
                required: [assignments, total, limit, offset]
                properties:
                  assignments:
                    type: array
                    items:
                      $ref: '#/components/schemas/CTChoreAssignment'
                  total:
                    type: integer
                    description: Total number of matching records (for calculating page count)
                  limit:
                    type: integer
                  offset:
                    type: integer
        "401":
          $ref: '#/components/responses/CTUnauthorized'

  /chores/api/chores/assignments/{assignment_id}/reject:
    post:
      operationId: ctRejectAssignment
      summary: Reject completed assignment
      description: >
        Parent only. Rejects a completed assignment with no point change.
        Equivalent to calling the approve endpoint with action=reject.
        Transitions status: completed → rejected.
      tags: [CT Chores]
      security:
        - CTBearer: []
      parameters:
        - $ref: '#/components/parameters/CTAssignmentId'
      responses:
        "200":
          description: Assignment rejected
          content:
            application/json:
              schema:
                type: object
                properties:
                  assignment:
                    $ref: '#/components/schemas/CTChoreAssignment'
        "401":
          $ref: '#/components/responses/CTUnauthorized'
        "403":
          $ref: '#/components/responses/CTForbidden'
        "404":
          description: Assignment not found
        "409":
          description: Assignment not in completed state

  /chores/api/chores/assignments/{assignment_id}/proof:
    post:
      operationId: ctUploadProof
      summary: Upload completion proof photo
      description: >
        Child only. Stub endpoint — photo proof is deferred to v4.0.
        Currently accepts the request and returns 200 without storing anything.
      tags: [CT Chores]
      security:
        - CTBearer: []
      parameters:
        - $ref: '#/components/parameters/CTAssignmentId'
      responses:
        "200":
          description: Accepted (no-op in current implementation)
        "401":
          $ref: '#/components/responses/CTUnauthorized'
        "403":
          $ref: '#/components/responses/CTForbidden'
        "404":
          description: Assignment not found

  # ─── CT Rewards ──────────────────────────────────────────────────────────────

  /chores/api/rewards/:
    get:
      operationId: ctListRewards
      summary: List rewards
      description: Returns all reward definitions for the caller's family.
      tags: [CT Rewards]
      security:
        - CTBearer: []
      parameters:
        - $ref: '#/components/parameters/CTSuperuserFamilyId'
      responses:
        "200":
          description: Reward list
          content:
            application/json:
              schema:
                type: object
                properties:
                  rewards:
                    type: array
                    items:
                      $ref: '#/components/schemas/CTReward'
        "401":
          $ref: '#/components/responses/CTUnauthorized'

    post:
      operationId: ctCreateReward
      summary: Create reward
      description: Parents only. Creates a new redeemable reward for the family.
      tags: [CT Rewards]
      security:
        - CTBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [title, cost]
              properties:
                title:
                  type: string
                  example: Extra screen time
                description:
                  type: string
                  nullable: true
                cost:
                  type: integer
                  description: Points required to redeem
                  example: 50
      responses:
        "201":
          description: Reward created
          content:
            application/json:
              schema:
                type: object
                properties:
                  reward:
                    $ref: '#/components/schemas/CTReward'
        "400":
          $ref: '#/components/responses/CTBadRequest'
        "401":
          $ref: '#/components/responses/CTUnauthorized'
        "403":
          $ref: '#/components/responses/CTForbidden'

  /chores/api/rewards/requests/:
    get:
      operationId: ctListRedemptionRequests
      summary: List redemption requests
      description: >
        Returns redemption requests for the caller's family, ordered by requested_at desc.
        Children see only their own requests. Filterable by status.
      tags: [CT Rewards]
      security:
        - CTBearer: []
      parameters:
        - $ref: '#/components/parameters/CTSuperuserFamilyId'
        - name: status
          in: query
          description: Filter by request status
          schema:
            type: string
            enum: [pending, approved, rejected]
      responses:
        "200":
          description: Redemption request list
          content:
            application/json:
              schema:
                type: object
                properties:
                  requests:
                    type: array
                    items:
                      $ref: '#/components/schemas/CTRedemptionRequest'
        "401":
          $ref: '#/components/responses/CTUnauthorized'

  /chores/api/rewards/{reward_id}/redeem:
    post:
      operationId: ctRedeemReward
      summary: Request reward redemption
      description: >
        Child only. Creates a pending redemption request for the specified reward.
        The child must have enough points (checked at request time; re-verified on approval).
      tags: [CT Rewards]
      security:
        - CTBearer: []
      parameters:
        - name: reward_id
          in: path
          required: true
          schema:
            type: integer
      responses:
        "201":
          description: Redemption request created
          content:
            application/json:
              schema:
                type: object
                properties:
                  request:
                    $ref: '#/components/schemas/CTRedemptionRequest'
        "400":
          description: Not enough points
        "401":
          $ref: '#/components/responses/CTUnauthorized'
        "403":
          $ref: '#/components/responses/CTForbidden'
        "404":
          description: Reward not found

  /chores/api/rewards/requests/{request_id}/resolve:
    post:
      operationId: ctResolveRedemption
      summary: Approve or reject redemption request
      description: >
        Parent only. Resolves a pending redemption request.
        action=approve deducts points from the child and transitions to approved.
        action=reject transitions to rejected with no point change.
      tags: [CT Rewards]
      security:
        - CTBearer: []
      parameters:
        - name: request_id
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                action:
                  type: string
                  enum: [approve, reject]
                  default: approve
      responses:
        "200":
          description: Request resolved
          content:
            application/json:
              schema:
                type: object
                properties:
                  request:
                    $ref: '#/components/schemas/CTRedemptionRequest'
                  child_points:
                    type: integer
                    description: Child's updated point balance
        "400":
          description: Child no longer has enough points
        "401":
          $ref: '#/components/responses/CTUnauthorized'
        "403":
          $ref: '#/components/responses/CTForbidden'
        "404":
          description: Request not found
        "409":
          description: Request already resolved

  /chores/api/rewards/{reward_id}:
    patch:
      operationId: ctUpdateReward
      summary: Update reward
      description: Parents only. Updates the title and/or point cost of an existing reward.
      tags: [CT Rewards]
      security:
        - CTBearer: []
      parameters:
        - name: reward_id
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                title:
                  type: string
                cost:
                  type: integer
                  minimum: 0
                  description: Points required to redeem
      responses:
        "200":
          description: Reward updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  reward:
                    $ref: '#/components/schemas/CTReward'
        "400":
          $ref: '#/components/responses/CTBadRequest'
        "401":
          $ref: '#/components/responses/CTUnauthorized'
        "403":
          $ref: '#/components/responses/CTForbidden'
        "404":
          description: Reward not found

    delete:
      operationId: ctDeleteReward
      summary: Delete reward
      description: Parents only. Deletes the reward definition and all its redemption requests.
      tags: [CT Rewards]
      security:
        - CTBearer: []
      parameters:
        - name: reward_id
          in: path
          required: true
          schema:
            type: integer
      responses:
        "204":
          description: Reward deleted
        "401":
          $ref: '#/components/responses/CTUnauthorized'
        "403":
          $ref: '#/components/responses/CTForbidden'
        "404":
          description: Reward not found

  /chores/api/rewards/history/:
    get:
      operationId: ctRewardHistory
      summary: Reward redemption history
      description: >
        Returns a paginated history of resolved (approved or rejected) redemption requests
        for the caller's family, ordered by most recently resolved.
        Children see only their own history. Parents see the whole family.
        Maximum 200 records per page.
      tags: [CT Rewards]
      security:
        - CTBearer: []
      parameters:
        - name: limit
          in: query
          description: Number of records to return (max 200, default 20)
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 20
        - name: offset
          in: query
          description: Number of records to skip for pagination
          schema:
            type: integer
            minimum: 0
            default: 0
      responses:
        "200":
          description: Paginated redemption history
          content:
            application/json:
              schema:
                type: object
                required: [requests, total, limit, offset]
                properties:
                  requests:
                    type: array
                    items:
                      $ref: '#/components/schemas/CTRedemptionRequest'
                  total:
                    type: integer
                    description: Total number of matching records (for calculating page count)
                  limit:
                    type: integer
                  offset:
                    type: integer
        "401":
          $ref: '#/components/responses/CTUnauthorized'

  # ─── CT Admin ────────────────────────────────────────────────────────────────

  /chores/api/admin/families:
    get:
      operationId: ctAdminListFamilies
      summary: List all families
      description: Admin/superuser only. Returns all families with member counts.
      tags: [CT Admin]
      security:
        - CTBearer: []
      responses:
        "200":
          description: Family list with member counts
          content:
            application/json:
              schema:
                type: object
                properties:
                  families:
                    type: array
                    items:
                      allOf:
                        - $ref: '#/components/schemas/CTFamily'
                        - type: object
                          properties:
                            member_count:
                              type: integer
        "401":
          $ref: '#/components/responses/CTUnauthorized'
        "403":
          $ref: '#/components/responses/CTForbidden'

    post:
      operationId: ctAdminCreateFamily
      summary: Create family with first parent
      description: Admin/superuser only. Creates a new family and its initial parent account.
      tags: [CT Admin]
      security:
        - CTBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [family_name, username, password]
              properties:
                family_name:
                  type: string
                username:
                  type: string
                password:
                  type: string
                  format: password
      responses:
        "201":
          description: Family and parent user created
          content:
            application/json:
              schema:
                type: object
                properties:
                  family:
                    $ref: '#/components/schemas/CTFamily'
                  user:
                    $ref: '#/components/schemas/CTUser'
        "400":
          $ref: '#/components/responses/CTBadRequest'
        "401":
          $ref: '#/components/responses/CTUnauthorized'
        "403":
          $ref: '#/components/responses/CTForbidden'

  /chores/api/admin/families/{family_id}:
    delete:
      operationId: ctAdminDeleteFamily
      summary: Delete family
      description: Admin/superuser only. Deletes the family and all cascaded users, chores, and rewards.
      tags: [CT Admin]
      security:
        - CTBearer: []
      parameters:
        - $ref: '#/components/parameters/CTFamilyId'
      responses:
        "200":
          description: Family deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: deleted
        "401":
          $ref: '#/components/responses/CTUnauthorized'
        "403":
          $ref: '#/components/responses/CTForbidden'
        "404":
          description: Family not found

  /chores/api/admin/families/{family_id}/members:
    get:
      operationId: ctAdminListFamilyMembers
      summary: List members of any family
      description: Admin/superuser only.
      tags: [CT Admin]
      security:
        - CTBearer: []
      parameters:
        - $ref: '#/components/parameters/CTFamilyId'
      responses:
        "200":
          description: Member list
          content:
            application/json:
              schema:
                type: object
                properties:
                  members:
                    type: array
                    items:
                      $ref: '#/components/schemas/CTUser'
        "401":
          $ref: '#/components/responses/CTUnauthorized'
        "403":
          $ref: '#/components/responses/CTForbidden'
        "404":
          description: Family not found

    post:
      operationId: ctAdminAddMember
      summary: Add member to any family
      description: Admin/superuser only. Adds a parent or child user to the specified family.
      tags: [CT Admin]
      security:
        - CTBearer: []
      parameters:
        - $ref: '#/components/parameters/CTFamilyId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [username, password]
              properties:
                username:
                  type: string
                password:
                  type: string
                  format: password
                role:
                  type: string
                  enum: [parent, child]
                  default: child
      responses:
        "201":
          description: User added
          content:
            application/json:
              schema:
                type: object
                properties:
                  user:
                    $ref: '#/components/schemas/CTUser'
        "400":
          $ref: '#/components/responses/CTBadRequest'
        "401":
          $ref: '#/components/responses/CTUnauthorized'
        "403":
          $ref: '#/components/responses/CTForbidden'

  /chores/api/admin/families/{family_id}/members/{user_id}:
    delete:
      operationId: ctAdminRemoveMember
      summary: Remove member from family
      description: Admin/superuser only. Cannot remove yourself.
      tags: [CT Admin]
      security:
        - CTBearer: []
      parameters:
        - $ref: '#/components/parameters/CTFamilyId'
        - name: user_id
          in: path
          required: true
          schema:
            type: integer
      responses:
        "200":
          description: Member removed
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: removed
        "400":
          description: Cannot remove yourself
        "401":
          $ref: '#/components/responses/CTUnauthorized'
        "403":
          $ref: '#/components/responses/CTForbidden'
        "404":
          description: User or family not found

  /chores/api/admin/users/{user_id}/reset-password:
    post:
      operationId: ctAdminResetPassword
      summary: Reset user password
      description: Admin/superuser only. Sets a new bcrypt password for any user.
      tags: [CT Admin]
      security:
        - CTBearer: []
      parameters:
        - name: user_id
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [new_password]
              properties:
                new_password:
                  type: string
                  format: password
      responses:
        "200":
          description: Password reset
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: password reset
        "400":
          $ref: '#/components/responses/CTBadRequest'
        "401":
          $ref: '#/components/responses/CTUnauthorized'
        "403":
          $ref: '#/components/responses/CTForbidden'
        "404":
          description: User not found


components:

  # ─── Security ────────────────────────────────────────────────────────────────

  securitySchemes:
    CTBearer:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: Access token issued by /chores/api/auth/login or /chores/api/auth/google
    CTBearerRefresh:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: Refresh token issued alongside the access token

  # ─── Reusable Parameters ─────────────────────────────────────────────────────

  parameters:

    # BPD parameters
    BPDStartDate:
      name: start
      in: query
      description: Filter start date (YYYY-MM-DD). Defaults to 30 days ago.
      schema:
        type: string
        format: date
        example: "2025-01-01"

    BPDEndDate:
      name: end
      in: query
      description: Filter end date (YYYY-MM-DD). Defaults to today.
      schema:
        type: string
        format: date
        example: "2025-01-31"

    BPDIncidentType:
      name: type
      in: query
      description: Filter by exact incident type string (e.g. "THEFT", "DUI").
      schema:
        type: string
        example: "THEFT"

    # ChoreTracker parameters
    CTFamilyId:
      name: family_id
      in: path
      required: true
      schema:
        type: integer

    CTAssignmentId:
      name: assignment_id
      in: path
      required: true
      schema:
        type: integer

    CTSuperuserFamilyId:
      name: family_id
      in: query
      description: Superuser only — scope results to a specific family instead of the caller's.
      schema:
        type: integer

  # ─── Reusable Responses ──────────────────────────────────────────────────────

  responses:
    CTBadRequest:
      description: Missing or invalid request body field
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/CTError'

    CTUnauthorized:
      description: Missing or invalid JWT
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/CTError'

    CTForbidden:
      description: Authenticated but insufficient role or wrong family
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/CTError'

  # ─── Schemas ─────────────────────────────────────────────────────────────────

  schemas:

    # ── BPD ────────────────────────────────────────────────────────────────────

    Incident:
      type: object
      required:
        - id
        - incident_date
        - case_number
        - incident_type
        - geocode_status
      properties:
        id:
          type: integer
          description: Database primary key
        incident_date:
          type: string
          format: date
        incident_time:
          type: string
          description: Local time string as parsed from the source log (e.g. "14:32")
          nullable: true
        case_number:
          type: string
          description: BPD case number (e.g. "25-12345")
        location:
          type: string
          description: Full address string from the incident log
          nullable: true
        incident_type:
          type: string
          description: Incident category (e.g. "THEFT", "ASSAULT", "DUI")
        description:
          type: string
          description: Free-text incident description
          nullable: true
        geocode_status:
          type: string
          enum: [ok, failed, placeholder, skipped]
          description: >
            ok — geocoded successfully;
            failed — geocoder returned no result;
            placeholder — address was a known placeholder (e.g. "CITY OF BELLINGHAM");
            skipped — address was blank or not attempted
        location_lat:
          type: number
          format: double
          nullable: true
          description: WGS-84 latitude (null when geocode_status ≠ ok)
        location_lon:
          type: number
          format: double
          nullable: true
          description: WGS-84 longitude (null when geocode_status ≠ ok)

    MapPoint:
      type: object
      required: [id, lat, lon, type, date, location]
      properties:
        id:
          type: integer
        lat:
          type: number
          format: double
        lon:
          type: number
          format: double
        type:
          type: string
          description: Incident type
        date:
          type: string
          description: Incident date string
        location:
          type: string
          description: Address string

    # ── ChoreTracker ───────────────────────────────────────────────────────────

    CTError:
      type: object
      properties:
        error:
          type: string

    CTUser:
      type: object
      required: [id, username, role, points]
      properties:
        id:
          type: integer
        family_id:
          type: integer
          nullable: true
          description: Null for admin/superuser accounts not tied to a family
        username:
          type: string
        display_name:
          type: string
          nullable: true
          description: Optional friendly name shown in the UI instead of username
        role:
          type: string
          enum: [parent, child, admin, superuser]
        points:
          type: integer
          description: Current point balance (meaningful for child accounts)
        google_email:
          type: string
          format: email
          nullable: true
          description: Gmail address used for Google OAuth login; null for username/password accounts
        pending:
          type: boolean
          description: True for invited accounts that have not yet signed in with Google to claim their account

    CTFamily:
      type: object
      required: [id, name, created_at]
      properties:
        id:
          type: integer
        name:
          type: string
        created_at:
          type: string
          format: date-time

    CTTokenResponse:
      type: object
      required: [access_token, refresh_token, user]
      properties:
        access_token:
          type: string
          description: Short-lived JWT (24 hours)
        refresh_token:
          type: string
          description: Long-lived JWT (30 days)
        user:
          $ref: '#/components/schemas/CTUser'

    CTChore:
      type: object
      required: [id, family_id, title, points, requires_proof]
      properties:
        id:
          type: integer
        family_id:
          type: integer
        title:
          type: string
        description:
          type: string
          nullable: true
        points:
          type: integer
          description: Points awarded to the child when the assignment is approved
        requires_proof:
          type: boolean
          description: Whether the child must upload a photo before marking complete

    CTChoreAssignment:
      type: object
      required: [id, chore_id, chore_title, points, child_id, child_name, status, assigned_at]
      properties:
        id:
          type: integer
        chore_id:
          type: integer
        chore_title:
          type: string
        points:
          type: integer
        requires_proof:
          type: boolean
        child_id:
          type: integer
        child_name:
          type: string
        status:
          type: string
          enum: [pending, completed, approved, rejected]
        assigned_at:
          type: string
          format: date-time
        completed_at:
          type: string
          format: date-time
          nullable: true
        approved_at:
          type: string
          format: date-time
          nullable: true

    CTReward:
      type: object
      required: [id, family_id, title, cost]
      properties:
        id:
          type: integer
        family_id:
          type: integer
        title:
          type: string
        description:
          type: string
          nullable: true
        cost:
          type: integer
          description: Points required to redeem

    CTRedemptionRequest:
      type: object
      required: [id, reward_id, reward_title, cost, child_id, child_name, status, requested_at]
      properties:
        id:
          type: integer
        reward_id:
          type: integer
        reward_title:
          type: string
        cost:
          type: integer
        child_id:
          type: integer
        child_name:
          type: string
        status:
          type: string
          enum: [pending, approved, rejected]
        requested_at:
          type: string
          format: date-time
        resolved_at:
          type: string
          format: date-time
          nullable: true
