openapi: 3.1.0
info:
  title: Regeno Farmwalk Public API
  description: |
    The Regeno Farmwalk Public API enables external integrations to access farm data,
    agreements, and vault uploads for UK agricultural compliance management.

    ## Authentication

    The API supports two authentication methods:

    ### Option 1: Organisation API Key + JWT (Recommended for integrations)

    1. **API Key** - Organisation-scoped key passed in the `x-farmwalk-api` header (`pk.xxx...`)
    2. **JWT Token** - User-scoped bearer token obtained via OTP login

    **Authentication Flow:**

    1. Create an API key in your organisation settings (one-time, store securely)
    2. Call `/auth/login` with your email and organisation ID
    3. Receive OTP via email, then call `/auth/verify` with the code
    4. Use the returned JWT token with your API key for subsequent requests

    ```bash
    curl -X GET "https://api.farmwalk.app/api/v1/public/farms" \
      -H "x-farmwalk-api: pk.abc123..." \
      -H "Authorization: Bearer eyJ..."
    ```

    ### Option 2: Personal Access Tokens (PATs)

    Personal Access Tokens allow users to authenticate with their own identity across
    multiple organisations. PATs are ideal for personal scripts, CLI tools, and
    multi-organisation access.

    1. Create a PAT in your user settings under **Security > Personal Access Tokens**
    2. Use the token in the `x-farmwalk-api` header (`pat.xxx...`)
    3. Specify the organisation with the `X-Organisation-Id` header

    ```bash
    curl -X GET "https://api.farmwalk.app/api/v1/public/farms" \
      -H "x-farmwalk-api: pat.abc123..." \
      -H "X-Organisation-Id: org-uuid-here"
    ```

    **Note:** Organisation admins can disable PAT access for their organisation.

    ## Token Scopes

    Both API keys and PATs support scopes to limit access:

    | Scope | Description |
    |-------|-------------|
    | `read` | Read-only access. Cannot create, update, or delete data. |
    | `write` | Full access. Can read and modify data. |

    Read-only tokens receive a `403 INSUFFICIENT_SCOPE` error when attempting write operations.

    ## Rate Limiting

    The API enforces rate limits to ensure fair usage:

    | Endpoint Type | Limit |
    |---------------|-------|
    | Authentication | 10 requests / 15 min |
    | Read operations | 200 requests / min |
    | Write operations | 30 requests / min |

    Rate limit headers are included in all responses:
    - `X-RateLimit-Limit` - Maximum requests allowed
    - `X-RateLimit-Remaining` - Requests remaining in window
    - `X-RateLimit-Reset` - Unix timestamp when limit resets
    - `Retry-After` - Seconds to wait (only on 429 responses)

    ## Farm Access

    Users may have restricted farm access based on their organisation membership.
    The API automatically filters results to only include farms the authenticated
    user has permission to access.

  version: 1.2.0
  contact:
    name: Regeno Support
    email: support@regeno.earth
    url: https://regeno.earth
  license:
    name: Proprietary
    url: https://regeno.earth/terms

servers:
  - url: https://api.farmwalk.app/api/v1/public
    description: Production
  - url: https://staging.farmwalk.app/api/v1/public
    description: Staging

tags:
  - name: Authentication
    description: OTP-based authentication to obtain JWT tokens
  - name: Personal Access Tokens
    description: Manage personal access tokens for user-scoped API authentication
  - name: User
    description: User profile and organisation membership
  - name: Farms
    description: Farm data and details
  - name: My Farms
    description: |
      The caller's personal farm filters: "My Farms" and "Team Farms".

      - **My Farms** — every farm the user has pinned **or** where they are
        the LEAD or ASSISTANT consultant. The two inputs are independent:
        pin and consultant role are separately managed, and a farm is in My
        Farms if either is true.
      - **Team Farms** — farms in the org linked to the teams the caller
        belongs to, intersected with the farms they can access.

      Used to scope list endpoints to the subset the user actually cares about.
  - name: Agreements
    description: Scheme agreements (SFI, Countryside Stewardship, Red Tractor, etc.)
  - name: Vault
    description: Document and media file uploads
  - name: Organisation Settings
    description: Organisation-level settings and configuration

security:
  - ApiKeyAuth: []
    BearerAuth: []

paths:
  /auth/login:
    post:
      operationId: authLogin
      summary: Initiate OTP login
      description: |
        Starts the authentication flow by sending a one-time password (OTP)
        to the user's email address. The user must be a member of the specified
        organisation.

        **Note:** This endpoint requires the API key header but not a JWT token.
      tags:
        - Authentication
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LoginRequest'
            example:
              email: farmer@example.com
              organisationId: 550e8400-e29b-41d4-a716-446655440000
      responses:
        '200':
          description: OTP sent successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LoginResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /auth/verify:
    post:
      operationId: authVerify
      summary: Verify OTP and obtain JWT
      description: |
        Completes the authentication flow by verifying the OTP code and returning
        a JWT token valid for 7 days.

        **Note:** This endpoint requires the API key header but not a JWT token.
      tags:
        - Authentication
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VerifyRequest'
            example:
              session: "AYABeE1d..."
              code: "123456"
              email: farmer@example.com
              organisationId: 550e8400-e29b-41d4-a716-446655440000
      responses:
        '200':
          description: Authentication successful
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VerifyResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /me/tokens:
    get:
      operationId: listPersonalAccessTokens
      summary: List personal access tokens
      description: |
        Returns all personal access tokens for the authenticated user.
        Requires session authentication (browser login), not API authentication.
      tags:
        - Personal Access Tokens
      security: []
      responses:
        '200':
          description: Tokens retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PersonalAccessTokenListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'

    post:
      operationId: createPersonalAccessToken
      summary: Create personal access token
      description: |
        Creates a new personal access token. The full token is only returned once
        and cannot be retrieved again. Store it securely.

        Requires session authentication (browser login), not API authentication.
      tags:
        - Personal Access Tokens
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreatePersonalAccessTokenRequest'
            example:
              name: CI/CD Pipeline
              scope: read
              expiresAt: "2025-12-31T23:59:59Z"
      responses:
        '201':
          description: Token created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreatePersonalAccessTokenResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'

  /me/tokens/{tokenId}:
    get:
      operationId: getPersonalAccessToken
      summary: Get personal access token details
      description: |
        Returns details for a specific token (without the full token value).
        Requires session authentication.
      tags:
        - Personal Access Tokens
      security: []
      parameters:
        - $ref: '#/components/parameters/TokenId'
      responses:
        '200':
          description: Token retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PersonalAccessTokenResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'

    delete:
      operationId: revokePersonalAccessToken
      summary: Revoke personal access token
      description: |
        Revokes a personal access token. The token will immediately stop working.
        This action cannot be undone.

        Requires session authentication.
      tags:
        - Personal Access Tokens
      security: []
      parameters:
        - $ref: '#/components/parameters/TokenId'
      responses:
        '200':
          description: Token revoked successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
        '400':
          description: Token already revoked
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'

  /me/organisations:
    get:
      operationId: listUserOrganisations
      summary: List user's organisations
      description: |
        Returns all organisations the authenticated user belongs to.
        This endpoint requires a Personal Access Token and does NOT require
        the `X-Organisation-Id` header.

        Use this to discover which organisations you can access with your PAT.
      tags:
        - User
      security:
        - PersonalAccessToken: []
      responses:
        '200':
          description: Organisations retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserOrganisationsResponse'
        '400':
          description: This endpoint requires a PAT (pat.*), not an API key (pk.*)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: This endpoint requires a personal access token
                code: REQUIRES_PAT
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'

  /me/pinned-farms:
    get:
      operationId: listMyFarms
      summary: List the caller's "My Farms"
      description: |
        Returns the caller's **My Farms** in the current organisation — the
        union of farms the user has pinned and farms where they are a LEAD
        or ASSISTANT consultant. Items are sorted alphabetically by farm
        name.

        Each item reports both reasons it appears in My Farms:
        - `pin` — pin metadata if explicitly pinned (else `null`)
        - `consultantRole` — `LEAD` | `ASSISTANT` if a consultant (else `null`)

        Restricted-access members only see farms within their `farmAccess`
        subset. The path is `/me/pinned-farms` for historical URL stability;
        the response is the full "My Farms" set, not just literal pin rows.

        Use the POST endpoint on the same path to toggle a farm's pin
        (consultant role can only be set by an organisation admin).
      tags:
        - My Farms
      responses:
        '200':
          description: Pinned farms retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PinnedFarmsResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      operationId: togglePinnedFarm
      summary: Toggle a farm pin
      description: |
        Toggles the caller's pin on a farm. Pinning is the only
        user-controllable input to **My Farms** — consultant assignment
        must be done by an organisation admin.

        If the caller is a consultant on the farm, unpinning does NOT
        remove the farm from My Farms (the consultant role keeps it there).
        The response's `farmStillInMyFarms` field tells you which case
        you're in.

        Requires `write` scope. Restricted-access members cannot pin farms
        outside their `farmAccess` subset.

        **Note:** Pinning farm *groups* is not exposed in this version —
        only individual farms can be pinned via the public API.
      tags:
        - My Farms
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TogglePinRequest'
            example:
              farmId: 550e8400-e29b-41d4-a716-446655440000
      responses:
        '200':
          description: Farm was already pinned and has now been unpinned
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TogglePinUnpinnedResponse'
        '201':
          description: Farm has been newly pinned
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TogglePinPinnedResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /me/team-farms:
    get:
      operationId: listTeamFarms
      summary: List the caller's "Team Farms"
      description: |
        Returns the union of farms in the current organisation that share at
        least one (non-archived) label with the caller's organisation
        membership. Use this to scope external tooling to the same set of
        farms the user sees behind the "Team Farms" filter inside Regeno.

        The response includes the `labelIds` that drove the union so callers
        can surface which labels are responsible for inclusion.

        Restricted-access members only see team farms intersected with their
        `farmAccess` subset. Returns an empty list (and empty `labelIds`) if
        the user has no labels in this organisation.
      tags:
        - My Farms
      responses:
        '200':
          description: Team farms retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TeamFarmsResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /organisations/{orgId}/settings/personal-tokens:
    get:
      operationId: getOrgPatSettings
      summary: Get organisation PAT settings
      description: |
        Returns whether personal access tokens are allowed for this organisation.
        Requires session authentication and organisation membership.
      tags:
        - Organisation Settings
      security: []
      parameters:
        - $ref: '#/components/parameters/OrgId'
      responses:
        '200':
          description: Settings retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrgPatSettingsResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'

    patch:
      operationId: updateOrgPatSettings
      summary: Update organisation PAT settings
      description: |
        Enable or disable personal access tokens for this organisation.
        Requires session authentication and owner role.

        When disabled, users cannot use their personal tokens to access
        this organisation's data.
      tags:
        - Organisation Settings
      security: []
      parameters:
        - $ref: '#/components/parameters/OrgId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - allowPersonalTokens
              properties:
                allowPersonalTokens:
                  type: boolean
                  description: Whether to allow PAT access
            example:
              allowPersonalTokens: false
      responses:
        '200':
          description: Settings updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrgPatSettingsResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'

  /farms:
    get:
      operationId: listFarms
      summary: List farms
      description: |
        Returns a paginated list of farms the authenticated user has access to
        within the organisation. Results are filtered based on the user's
        farm access permissions.

        The `myFarmsOnly` and `teamFarmsOnly` flags narrow the list to the
        caller's My Farms or the label-based Team Farms union respectively
        (My Farms = pinned ∪ consultant-assigned). They match the same
        filters used inside Regeno and "fall through" to the wider set if
        the user has no My Farms or Team Farms (matching UI behaviour) —
        see `/me/pinned-farms` and `/me/team-farms` for the raw subsets.
      tags:
        - Farms
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
        - name: isActive
          in: query
          description: Filter by active status
          schema:
            type: boolean
        - name: myFarmsOnly
          in: query
          description: |
            When `1`, return only the caller's My Farms — pinned farms
            UNION farms where the user is a LEAD or ASSISTANT consultant.
            If the caller has no My Farms, the filter is ignored and all
            accessible farms are returned (matches the UI's fall-through
            behaviour). Any other value is treated as `0`.
          schema:
            type: string
            enum: ['0', '1']
            default: '0'
        - name: teamFarmsOnly
          in: query
          description: |
            When `1`, return only farms in the caller's "Team Farms" — the
            union of farms sharing at least one label with the caller's
            organisation membership. If the caller has no team farms, the
            filter is ignored. Any other value is treated as `0`.
          schema:
            type: string
            enum: ['0', '1']
            default: '0'
      responses:
        '200':
          description: Farms retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FarmListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /farms/{farmId}:
    get:
      operationId: getFarm
      summary: Get farm details
      description: |
        Returns detailed information about a specific farm, including
        organisation context.
      tags:
        - Farms
      parameters:
        - $ref: '#/components/parameters/FarmId'
      responses:
        '200':
          description: Farm retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FarmResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /farms/{farmId}/agreements:
    get:
      operationId: listAgreements
      summary: List farm agreements
      description: |
        Returns a paginated list of scheme agreements for a farm, optionally
        including agreement items with their land assignments.
      tags:
        - Agreements
      parameters:
        - $ref: '#/components/parameters/FarmId'
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
        - name: schemeType
          in: query
          description: Filter by scheme type
          schema:
            $ref: '#/components/schemas/SchemeType'
        - name: status
          in: query
          description: Filter by agreement status
          schema:
            $ref: '#/components/schemas/AgreementStatus'
        - name: includeItems
          in: query
          description: Include agreement items and land assignments (default true)
          schema:
            type: boolean
            default: true
      responses:
        '200':
          description: Agreements retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgreementListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /farms/{farmId}/agreements/{agreementId}:
    get:
      operationId: getAgreement
      summary: Get agreement details
      description: |
        Returns full details for a specific agreement, including items,
        land assignments, payments, inspections, and annual declarations.
      tags:
        - Agreements
      parameters:
        - $ref: '#/components/parameters/FarmId'
        - $ref: '#/components/parameters/AgreementId'
      responses:
        '200':
          description: Agreement retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgreementDetailResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /farms/{farmId}/vault/upload/start:
    post:
      operationId: startUpload
      summary: Start file upload
      description: |
        Initiates a file upload by creating a pending upload record and returning
        a presigned S3 URL. The client should upload the file directly to S3 using
        the presigned URL, then call the finish endpoint to complete the process.

        **Maximum file size:** 500 MB

        **Presigned URL expiry:** 30 minutes (can be renewed)

        ### Upload Flow

        1. Call this endpoint with file metadata
        2. Upload file to the returned `presignedUrl` using PUT
        3. Call `/vault/upload/finish` with the upload details

        ```bash
        # Step 1: Start upload
        curl -X POST ".../vault/upload/start" \
          -d '{"filename":"photo.jpg","mimeType":"image/jpeg","sizeBytes":1024000}'

        # Step 2: Upload to S3
        curl -X PUT "$presignedUrl" \
          -H "Content-Type: image/jpeg" \
          --data-binary @photo.jpg

        # Step 3: Finish upload
        curl -X POST ".../vault/upload/finish" \
          -d '{"uploadId":"...","s3Key":"...","filename":"photo.jpg",...}'
        ```
      tags:
        - Vault
      parameters:
        - $ref: '#/components/parameters/FarmId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StartUploadRequest'
            example:
              filename: field-photo-2024.jpg
              mimeType: image/jpeg
              sizeBytes: 2048576
              folderId: 550e8400-e29b-41d4-a716-446655440001
      responses:
        '200':
          description: Upload initiated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StartUploadResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /farms/{farmId}/vault/upload/finish:
    post:
      operationId: finishUpload
      summary: Complete file upload
      description: |
        Completes the upload process after the file has been uploaded to S3.
        Creates the vault file record and triggers background processors
        (transcription, AI analysis, etc.).
      tags:
        - Vault
      parameters:
        - $ref: '#/components/parameters/FarmId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FinishUploadRequest'
            example:
              uploadId: 550e8400-e29b-41d4-a716-446655440002
              s3Key: orgs/org123/farms/farm456/vault/550e8400.jpg
              filename: field-photo-2024.jpg
              mimeType: image/jpeg
              sizeBytes: 2048576
              location:
                latitude: 51.5074
                longitude: -0.1278
                accuracy: 10
              category: evidence
              tags:
                - sfi
                - sam1
      responses:
        '201':
          description: Upload completed successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FinishUploadResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Upload already completed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Upload already completed
                code: ALREADY_COMPLETED
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /farms/{farmId}/vault/upload/{uploadId}/renew:
    post:
      operationId: renewUploadUrl
      summary: Renew presigned URL
      description: |
        Generates a new presigned URL for an unfinished upload. Use this if
        the original URL has expired before the upload was completed.
      tags:
        - Vault
      parameters:
        - $ref: '#/components/parameters/FarmId'
        - $ref: '#/components/parameters/UploadId'
      responses:
        '200':
          description: URL renewed successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RenewUploadResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Upload already completed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Upload already completed. Cannot renew URL for completed uploads.
                code: ALREADY_COMPLETED
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: x-farmwalk-api
      description: |
        Organisation API key in format `pk.{64 hex characters}`.
        Create keys in your organisation settings.

        **Scopes:**
        - `read` - Read-only access (recommended for analytics/reporting)
        - `write` - Full read/write access (default)
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        JWT token obtained from `/auth/verify` endpoint.
        Valid for 7 days.
    PersonalAccessToken:
      type: apiKey
      in: header
      name: x-farmwalk-api
      description: |
        Personal Access Token in format `pat.{64 hex characters}`.
        Create tokens in your user settings under Security.

        When using PATs, you must also include the `X-Organisation-Id` header
        to specify which organisation to access.

        **Scopes:**
        - `read` - Read-only access
        - `write` - Full read/write access

  parameters:
    FarmId:
      name: farmId
      in: path
      required: true
      description: Farm UUID
      schema:
        type: string
        format: uuid
    OrgId:
      name: orgId
      in: path
      required: true
      description: Organisation UUID
      schema:
        type: string
        format: uuid
    TokenId:
      name: tokenId
      in: path
      required: true
      description: Personal Access Token UUID
      schema:
        type: string
        format: uuid
    AgreementId:
      name: agreementId
      in: path
      required: true
      description: Agreement UUID
      schema:
        type: string
        format: uuid
    UploadId:
      name: uploadId
      in: path
      required: true
      description: Upload UUID
      schema:
        type: string
        format: uuid
    Limit:
      name: limit
      in: query
      description: Maximum number of items to return (max 100)
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 20
    Offset:
      name: offset
      in: query
      description: Number of items to skip for pagination
      schema:
        type: integer
        minimum: 0
        default: 0

  schemas:
    # Authentication
    LoginRequest:
      type: object
      required:
        - email
        - organisationId
      properties:
        email:
          type: string
          format: email
          description: User's email address
        organisationId:
          type: string
          format: uuid
          description: Organisation UUID

    LoginResponse:
      type: object
      properties:
        session:
          type: string
          description: Session token for the verify step
        challengeName:
          type: string
          description: Type of challenge (e.g., CUSTOM_CHALLENGE)
        destination:
          type: string
          description: Masked destination where OTP was sent
        organisationId:
          type: string
          format: uuid
          description: Organisation ID for the verify step

    VerifyRequest:
      type: object
      required:
        - session
        - code
        - email
        - organisationId
      properties:
        session:
          type: string
          description: Session token from login response
        code:
          type: string
          description: 6-digit OTP code from email
          pattern: '^\d{6}$'
        email:
          type: string
          format: email
          description: User's email address
        organisationId:
          type: string
          format: uuid
          description: Organisation UUID

    VerifyResponse:
      type: object
      properties:
        token:
          type: string
          description: JWT token for API requests
        expiresIn:
          type: integer
          description: Token validity in seconds (604800 = 7 days)
        user:
          type: object
          properties:
            id:
              type: string
              format: uuid
            email:
              type: string
              format: email
            firstName:
              type: string
            lastName:
              type: string
        organisation:
          type: object
          properties:
            id:
              type: string
              format: uuid
            membership:
              type: object
              properties:
                role:
                  type: string
                  enum: [owner, consultant, farmer, manager]
                farmAccess:
                  type: array
                  items:
                    type: string
                    format: uuid
                  description: Farm IDs user has access to (empty = all farms)

    # Farms
    Farm:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        slug:
          type: string
        sbiNumber:
          type: string
          nullable: true
          description: Single Business Identifier
        cphNumber:
          type: string
          nullable: true
          description: County Parish Holding number
        totalHectares:
          type: number
          format: float
        parcelCount:
          type: integer
        isActive:
          type: boolean
        address:
          $ref: '#/components/schemas/Address'
        coordinates:
          $ref: '#/components/schemas/Coordinates'
        createdAt:
          type: string
          format: date-time

    FarmDetail:
      allOf:
        - $ref: '#/components/schemas/Farm'
        - type: object
          properties:
            updatedAt:
              type: string
              format: date-time
            organisation:
              type: object
              properties:
                id:
                  type: string
                  format: uuid
                name:
                  type: string
                slug:
                  type: string

    FarmListResponse:
      type: object
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/Farm'
        total:
          type: integer
        limit:
          type: integer
        offset:
          type: integer
        hasMore:
          type: boolean

    FarmResponse:
      type: object
      properties:
        item:
          $ref: '#/components/schemas/FarmDetail'

    # My Farms (pinned ∪ consultant-assigned) + Team Farms
    ConsultantRole:
      type: string
      enum:
        - LEAD
        - ASSISTANT
      description: |
        - `LEAD` - Lead consultant on the farm (one per farm)
        - `ASSISTANT` - Assistant consultant on the farm

    PinSummary:
      type: object
      description: Pin metadata for a farm the caller has explicitly pinned.
      properties:
        pinId:
          type: string
          format: uuid
          description: Unique identifier for this pin
        pinnedAt:
          type: string
          format: date-time
          description: When the user pinned this farm

    PinnedFarm:
      type: object
      description: |
        A single farm in the caller's "My Farms" — the farm plus the
        reasons it appears here. Either `pin` or `consultantRole` (or both)
        will be set.
      properties:
        farm:
          $ref: '#/components/schemas/Farm'
        pin:
          oneOf:
            - $ref: '#/components/schemas/PinSummary'
            - type: 'null'
          description: Pin metadata when the user has explicitly pinned this farm.
        consultantRole:
          oneOf:
            - $ref: '#/components/schemas/ConsultantRole'
            - type: 'null'
          description: |
            The user's consultant role on this farm, if any. Consultant role
            keeps a farm in My Farms regardless of pin state.

    PinnedFarmsResponse:
      type: object
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/PinnedFarm'
        total:
          type: integer
          description: Number of farms in My Farms

    TogglePinRequest:
      type: object
      required:
        - farmId
      properties:
        farmId:
          type: string
          format: uuid
          description: |
            UUID of the farm to pin or unpin. Must belong to the current
            organisation and be within the caller's farm-access subset.

    TogglePinPinnedResponse:
      type: object
      description: Returned when a new pin was created (201).
      properties:
        action:
          type: string
          enum: [pinned]
        pin:
          type: object
          properties:
            id:
              type: string
              format: uuid
            userId:
              type: string
              format: uuid
            organisationId:
              type: string
              format: uuid
            farmId:
              type: string
              format: uuid
            pinnedAt:
              type: string
              format: date-time
        farmStillInMyFarms:
          type: boolean
          description: |
            Always `true` after a pin — the farm is now in My Farms.
        consultantRole:
          oneOf:
            - $ref: '#/components/schemas/ConsultantRole'
            - type: 'null'
          description: |
            The caller's consultant role on this farm (if any). Informational —
            does not change as a result of the pin toggle.

    TogglePinUnpinnedResponse:
      type: object
      description: Returned when an existing pin was removed (200).
      properties:
        action:
          type: string
          enum: [unpinned]
        pinId:
          type: string
          format: uuid
          description: ID of the pin that was removed
        farmStillInMyFarms:
          type: boolean
          description: |
            `true` when the caller is also a consultant on the farm — the
            farm remains in My Farms via the consultant branch. `false`
            otherwise.
        consultantRole:
          oneOf:
            - $ref: '#/components/schemas/ConsultantRole'
            - type: 'null'

    TeamFarmsResponse:
      type: object
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/Farm'
          description: |
            Farms in the current organisation linked to the teams the caller
            belongs to, intersected with the farms they can access.
        total:
          type: integer
          description: Number of farms returned
        labelIds:
          type: array
          items:
            type: string
            format: uuid
          description: |
            The caller's non-archived label IDs in this organisation. These
            are the labels that drove the union in `items`. Empty when the
            caller carries no labels.

    # Agreements
    SchemeType:
      type: string
      enum:
        - SFI
        - CS
        - RED_TRACTOR
        - HEALTH_SAFETY
        - WILDFARMED
        - NVZ
        - LEAF_MARQUE
        - SFS
        - AECS
        - FFERMIO_BRO
        - BOORTMALT_MALTING_GROWERS
      description: |
        - `SFI` - Sustainable Farming Incentive (England)
        - `CS` - Countryside Stewardship (England)
        - `RED_TRACTOR` - Red Tractor assurance
        - `HEALTH_SAFETY` - Health & Safety compliance
        - `WILDFARMED` - Wildfarmed regenerative grain programme
        - `NVZ` - Nitrate Vulnerable Zone obligations
        - `LEAF_MARQUE` - LEAF Marque sustainable assurance
        - `SFS` - Sustainable Farming Scheme (Wales)
        - `AECS` - Agri-Environment Climate Scheme (Scotland)
        - `FFERMIO_BRO` - Ffermio Bro Capital Grants (Wales)
        - `BOORTMALT_MALTING_GROWERS` - Boortmalt malting-growers buyer-led scheme

    AgreementStatus:
      type: string
      enum:
        - PENDING
        - ACTIVE
        - EXPIRED
        - CANCELLED

    AgreementItemType:
      type: string
      enum:
        - ACTION
        - CAPITAL
        - MODULE
        - DOCUMENT_CATEGORY
        - CHECKLIST
      description: |
        - `ACTION` - Recurring scheme action (e.g., an SFI SAM action)
        - `CAPITAL` - One-off capital grant item
        - `MODULE` - A grouping/module within a scheme
        - `DOCUMENT_CATEGORY` - Documentary evidence requirement
        - `CHECKLIST` - Inspection / self-assessment checklist item

    ItemStatus:
      type: string
      enum:
        - NOT_STARTED
        - IN_PROGRESS
        - COMPLETED
        - ON_HOLD
      description: Lifecycle status of an agreement item.

    Agreement:
      type: object
      properties:
        id:
          type: string
          format: uuid
        farmId:
          type: string
          format: uuid
        schemeType:
          $ref: '#/components/schemas/SchemeType'
        reference:
          type: string
          nullable: true
          description: External reference number
        name:
          type: string
        status:
          $ref: '#/components/schemas/AgreementStatus'
        isAlwaysActive:
          type: boolean
          description: True for ongoing schemes like Red Tractor
        startDate:
          type: string
          format: date
          nullable: true
        endDate:
          type: string
          format: date
          nullable: true
        complianceScore:
          type: number
          format: float
          nullable: true
          description: Calculated compliance percentage
        totalAnnualValue:
          type: number
          format: float
          nullable: true
          description: Total annual payment value in GBP
        attributes:
          type: object
          nullable: true
          description: Scheme-specific attributes
        notes:
          type: string
          nullable: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    AgreementItem:
      type: object
      properties:
        id:
          type: string
          format: uuid
        catalogCode:
          type: string
          description: Scheme action code (e.g., SAM1, SAM2)
        itemType:
          $ref: '#/components/schemas/AgreementItemType'
        nameOverride:
          type: string
          nullable: true
        rateOverride:
          type: number
          format: float
          nullable: true
        status:
          $ref: '#/components/schemas/ItemStatus'
        progress:
          type: number
          format: float
          nullable: true
        quantity:
          type: number
          format: float
          nullable: true
        quantityClaimed:
          type: number
          format: float
          nullable: true
        areaHa:
          type: number
          format: float
          nullable: true
        lengthM:
          type: number
          format: float
          nullable: true
        calculatedPayment:
          type: number
          format: float
          nullable: true
        attributes:
          type: object
          nullable: true
        notes:
          type: string
          nullable: true
        landAssignments:
          type: array
          items:
            $ref: '#/components/schemas/LandAssignment'

    LandAssignment:
      type: object
      properties:
        id:
          type: string
          format: uuid
        landFeatureId:
          type: string
          format: uuid
        areaHa:
          type: number
          format: float
          nullable: true
        lengthM:
          type: number
          format: float
          nullable: true
        status:
          type: string
        calculatedPayment:
          type: number
          format: float
          nullable: true
        landFeature:
          type: object
          properties:
            id:
              type: string
              format: uuid
            name:
              type: string
            featureType:
              type: string

    Payment:
      type: object
      properties:
        id:
          type: string
          format: uuid
        paymentType:
          type: string
        description:
          type: string
          nullable: true
        dueDate:
          type: string
          format: date
          nullable: true
        expectedAmount:
          type: number
          format: float
          nullable: true
        paidDate:
          type: string
          format: date
          nullable: true
        paidAmount:
          type: number
          format: float
          nullable: true
        status:
          type: string

    Inspection:
      type: object
      properties:
        id:
          type: string
          format: uuid
        scheduledDate:
          type: string
          format: date
          nullable: true
        completedDate:
          type: string
          format: date
          nullable: true
        result:
          type: string
          nullable: true
        inspectorName:
          type: string
          nullable: true
        inspectorOrg:
          type: string
          nullable: true
        inspectionType:
          type: string
          nullable: true
        feedback:
          type: string
          nullable: true
        status:
          type: string

    AnnualDeclaration:
      type: object
      properties:
        id:
          type: string
          format: uuid
        year:
          type: integer
        dueDate:
          type: string
          format: date
          nullable: true
        status:
          type: string
        submittedAt:
          type: string
          format: date-time
          nullable: true

    AgreementWithItems:
      allOf:
        - $ref: '#/components/schemas/Agreement'
        - type: object
          properties:
            items:
              type: array
              items:
                $ref: '#/components/schemas/AgreementItem'

    AgreementDetail:
      allOf:
        - $ref: '#/components/schemas/AgreementWithItems'
        - type: object
          properties:
            payments:
              type: array
              items:
                $ref: '#/components/schemas/Payment'
            inspections:
              type: array
              items:
                $ref: '#/components/schemas/Inspection'
            annualDeclarations:
              type: array
              items:
                $ref: '#/components/schemas/AnnualDeclaration'

    AgreementListResponse:
      type: object
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/AgreementWithItems'
        total:
          type: integer
        limit:
          type: integer
        offset:
          type: integer
        hasMore:
          type: boolean

    AgreementDetailResponse:
      type: object
      properties:
        item:
          $ref: '#/components/schemas/AgreementDetail'

    # Vault Uploads
    StartUploadRequest:
      type: object
      required:
        - filename
        - mimeType
        - sizeBytes
      properties:
        filename:
          type: string
          description: Original filename
        mimeType:
          type: string
          description: MIME type of the file
        sizeBytes:
          type: integer
          description: File size in bytes (max 500MB)
          maximum: 524288000
        folderId:
          type: string
          format: uuid
          nullable: true
          description: Target folder UUID (optional)

    StartUploadResponse:
      type: object
      properties:
        uploadId:
          type: string
          format: uuid
          description: Unique upload identifier
        presignedUrl:
          type: string
          format: uri
          description: Presigned S3 URL for PUT upload
        s3Key:
          type: string
          description: S3 object key
        s3Bucket:
          type: string
          description: S3 bucket name
        fileType:
          type: string
          description: Detected file type category
        expiresIn:
          type: integer
          description: URL validity in seconds
        expiresAt:
          type: string
          format: date-time
          description: URL expiration timestamp

    FinishUploadRequest:
      type: object
      required:
        - uploadId
        - s3Key
        - filename
        - mimeType
        - sizeBytes
      properties:
        uploadId:
          type: string
          format: uuid
          description: Upload ID from start response
        s3Key:
          type: string
          description: S3 key from start response
        filename:
          type: string
          description: Original filename
        mimeType:
          type: string
          description: MIME type of the file
        sizeBytes:
          type: integer
          description: Actual file size in bytes
        folderId:
          type: string
          format: uuid
          nullable: true
          description: Target folder UUID
        location:
          $ref: '#/components/schemas/GeoLocation'
        locationTrail:
          type: array
          items:
            type: object
            properties:
              latitude:
                type: number
                format: float
              longitude:
                type: number
                format: float
              timestamp:
                type: string
                format: date-time
          description: GPS trail for recordings
        category:
          type: string
          nullable: true
          description: File category (e.g., evidence, receipt)
        tags:
          type: array
          items:
            type: string
          description: Searchable tags
        description:
          type: string
          nullable: true
          description: File description
        durationSeconds:
          type: integer
          nullable: true
          description: Duration for audio/video files

    FinishUploadResponse:
      type: object
      properties:
        item:
          type: object
          properties:
            id:
              type: string
              format: uuid
            filename:
              type: string
            fileType:
              type: string
            mimeType:
              type: string
            sizeBytes:
              type: integer
            folderId:
              type: string
              format: uuid
              nullable: true
            uploadedAt:
              type: string
              format: date-time
            category:
              type: string
              nullable: true
            tags:
              type: array
              items:
                type: string
            durationSeconds:
              type: integer
              nullable: true
              description: Duration in seconds for audio/video files (null otherwise)
            processingStatus:
              type: string
              description: Initial processing status (pending)

    RenewUploadResponse:
      type: object
      properties:
        uploadId:
          type: string
          format: uuid
        presignedUrl:
          type: string
          format: uri
        s3Key:
          type: string
        expiresIn:
          type: integer
        expiresAt:
          type: string
          format: date-time

    # Common
    Address:
      type: object
      nullable: true
      properties:
        line1:
          type: string
        line2:
          type: string
          nullable: true
        town:
          type: string
        county:
          type: string
        postcode:
          type: string

    Coordinates:
      type: object
      nullable: true
      properties:
        lat:
          type: number
          format: float
        lng:
          type: number
          format: float

    GeoLocation:
      type: object
      nullable: true
      properties:
        latitude:
          type: number
          format: float
        longitude:
          type: number
          format: float
        accuracy:
          type: number
          format: float
          nullable: true
          description: GPS accuracy in meters

    Error:
      type: object
      required:
        - error
        - code
      properties:
        error:
          type: string
          description: Human-readable error message
        code:
          type: string
          description: Machine-readable error code

    # Token Scopes
    TokenScope:
      type: string
      enum:
        - read
        - write
      description: |
        - `read` - Read-only access. Cannot create, update, or delete data.
        - `write` - Full access. Can read and modify data.

    # Personal Access Tokens
    PersonalAccessToken:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
          description: Descriptive name for the token
        prefix:
          type: string
          description: Token prefix (pat.xxxxxxxx)
        scope:
          $ref: '#/components/schemas/TokenScope'
        createdAt:
          type: string
          format: date-time
        lastUsedAt:
          type: string
          format: date-time
          nullable: true
        expiresAt:
          type: string
          format: date-time
          nullable: true
          description: Token expiration (null = never expires)
        revokedAt:
          type: string
          format: date-time
          nullable: true

    CreatePersonalAccessTokenRequest:
      type: object
      required:
        - name
      properties:
        name:
          type: string
          description: Descriptive name for the token
          maxLength: 100
        scope:
          $ref: '#/components/schemas/TokenScope'
          default: write
        expiresAt:
          type: string
          format: date-time
          nullable: true
          description: Optional expiration date (ISO 8601)

    CreatePersonalAccessTokenResponse:
      type: object
      properties:
        token:
          type: object
          properties:
            id:
              type: string
              format: uuid
            name:
              type: string
            prefix:
              type: string
            scope:
              $ref: '#/components/schemas/TokenScope'
            key:
              type: string
              description: |
                The full token value (pat.xxx...).
                **Only shown once** - store securely.
            createdAt:
              type: string
              format: date-time
            expiresAt:
              type: string
              format: date-time
              nullable: true

    PersonalAccessTokenResponse:
      type: object
      properties:
        token:
          $ref: '#/components/schemas/PersonalAccessToken'

    PersonalAccessTokenListResponse:
      type: object
      properties:
        tokens:
          type: array
          items:
            $ref: '#/components/schemas/PersonalAccessToken'

    # User Organisations
    UserOrganisation:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        slug:
          type: string
        role:
          type: string
          enum: [owner, consultant, farmer, manager]
        allowPersonalTokens:
          type: boolean
          description: Whether the org allows PAT access

    UserOrganisationsResponse:
      type: object
      properties:
        organisations:
          type: array
          items:
            $ref: '#/components/schemas/UserOrganisation'

    # Organisation PAT Settings
    OrgPatSettingsResponse:
      type: object
      properties:
        settings:
          type: object
          properties:
            allowPersonalTokens:
              type: boolean
              description: Whether personal access tokens can access this organisation

  responses:
    BadRequest:
      description: Invalid request parameters
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: Email is required
            code: MISSING_EMAIL
    Unauthorized:
      description: Missing or invalid authentication
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: Invalid API key
            code: INVALID_API_KEY
    Forbidden:
      description: Insufficient permissions
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: User is not a member of this organisation
            code: NOT_MEMBER
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: Farm not found
            code: FARM_NOT_FOUND
    RateLimited:
      description: Rate limit exceeded
      headers:
        Retry-After:
          schema:
            type: integer
          description: Seconds until rate limit resets
        X-RateLimit-Limit:
          schema:
            type: integer
        X-RateLimit-Remaining:
          schema:
            type: integer
        X-RateLimit-Reset:
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: Rate limit exceeded
            code: RATE_LIMITED
    InternalError:
      description: Internal server error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: An unexpected error occurred
            code: INTERNAL_ERROR
