"/admin/fabricator/job":
    post:
        tags: [Fabricator Jobs]
        operationId: createFabricatorJob
        summary: Create a job from a verified scan request
        description: |
            Creates a new manufacturing job from a scan request that has reached
            `ReadyForFabricator`. Acquires a Redis distributed lock on the scan request,
            creates the Job DB record, and appends the new `jobId` to the scan request's
            `jobIds` array. Rarely called directly from Arda — usually the Big Order
            checklist creates jobs automatically.
        requestBody:
            required: true
            content:
                application/json:
                    schema:
                        type: object
                        required: [scanRequestId]
                        properties:
                            scanRequestId: { type: string }
                            priority:
                                type: integer
                                description: "Optional; clamped server-side to `[-1, 100]`."
        responses:
            "200":
                description: Created `Job`.
                content:
                    application/json:
                        schema: { $ref: "../components/schemas/Job.yaml" }
            "401": { $ref: "../admin-api.yaml#/components/responses/Unauthorized" }
            "403": { $ref: "../admin-api.yaml#/components/responses/Forbidden" }
            "404":
                description: Scan request not found.
                content:
                    application/json:
                        schema: { $ref: "../admin-api.yaml#/components/schemas/Error" }
            "422":
                description: Blank `scanRequestId`, or scan request is not in `ReadyForFabricator`.
                content:
                    application/json:
                        schema: { $ref: "../admin-api.yaml#/components/schemas/Error" }
        x-access-policies: [Fabricator]
        x-scopes: ["fabricator:write", "admin:all"]

"/admin/fabricator/job/{jobId}":
    parameters:
        - in: path
          name: jobId
          required: true
          schema: { type: string }
    get:
        tags: [Fabricator Jobs]
        operationId: getFabricatorJob
        summary: Fetch a job
        description: Returns the full `Job` record. Read-only passthrough; no filtering or anonymization.
        responses:
            "200":
                description: Job record.
                content:
                    application/json:
                        schema: { $ref: "../components/schemas/Job.yaml" }
            "401": { $ref: "../admin-api.yaml#/components/responses/Unauthorized" }
            "403": { $ref: "../admin-api.yaml#/components/responses/Forbidden" }
            "404":
                description: Job not found.
                content:
                    application/json:
                        schema: { $ref: "../admin-api.yaml#/components/schemas/Error" }
        x-access-policies: [Fabricator]
        x-scopes: ["fabricator:read", "admin:all"]
    put:
        tags: [Fabricator Jobs]
        operationId: updateFabricatorJob
        summary: Update a job and/or run a state-machine action
        description: |
            The primary mutating endpoint for jobs. Updates job fields and/or drives the
            state machine via an `action`. Multiple updates can be mixed into a single
            call (e.g. change priority *and* run an action).

            Supported `action` values include `TryScanVerification`,
            `TryExternalPreprocessorUpload`, `AddMessage` (requires `message`),
            `TryNFC` (requires `nfcTagId`), `TryToolPathCreation`,
            `ToolPathCreationSuccess`, `QASuccess` / `TryCreateInventoryItem`,
            `ResetToAwaitingScanVerification`, `ResetToAwaitingToolpath`,
            `ResetToAwaitingManufacture`, `DestroyJob`. Reset actions are blocked
            if the job already has live inventory.

            Priority is clamped to `[-1, 100]`. IPD updates set
            `ipdSource = BigOrderIpdSource.Admin`. Every update appends to `job.history`.
        requestBody:
            required: true
            content:
                application/json:
                    schema:
                        type: object
                        properties:
                            action: { type: string, description: State-machine action name. }
                            metaData:
                                description: Action-specific payload.
                            priority:
                                type: integer
                                description: "Clamped to `[-1, 100]`."
                            name: { type: string }
                            ipd:
                                type: integer
                                description: Sets `ipdSource = Admin`.
                            nfcTagId:
                                type: string
                                description: Required when `action = TryNFC`.
                            message:
                                type: string
                                description: Required when `action = AddMessage`.
                        additionalProperties: true
        responses:
            "200":
                description: Updated job (or action-specific shape — see markdown for the action).
                content:
                    application/json:
                        schema: { $ref: "../components/schemas/Job.yaml" }
            "401": { $ref: "../admin-api.yaml#/components/responses/Unauthorized" }
            "403":
                description: Reset blocked because job has live inventory items, or caller lacks permission.
                content:
                    application/json:
                        schema: { $ref: "../admin-api.yaml#/components/schemas/Error" }
            "404":
                description: Job not found.
                content:
                    application/json:
                        schema: { $ref: "../admin-api.yaml#/components/schemas/Error" }
            "422":
                description: Invalid state transition for the action, or missing required field for the action.
                content:
                    application/json:
                        schema: { $ref: "../admin-api.yaml#/components/schemas/Error" }
        x-access-policies: [Fabricator, FabricatorAdmin]
        x-scopes: ["fabricator:write", "admin:all"]
    delete:
        tags: [Fabricator Jobs]
        operationId: deleteFabricatorJob
        summary: Hard-delete a job
        description: |
            Hard-deletes a job and its linked `WaxId` + `NfcTag` records. Stricter auth
            than `updateJob`. Destructive; no undo.
        responses:
            "200":
                description: '"OK".'
                content:
                    text/plain:
                        schema: { type: string, example: OK }
            "401": { $ref: "../admin-api.yaml#/components/responses/Unauthorized" }
            "403": { $ref: "../admin-api.yaml#/components/responses/Forbidden" }
            "404":
                description: Job not found.
                content:
                    application/json:
                        schema: { $ref: "../admin-api.yaml#/components/schemas/Error" }
        x-access-policies: [Admin]
        x-scopes: ["fabricator:write", "admin:all"]

"/admin/fabricator/job/{jobId}/test_preprocessor":
    parameters:
        - in: path
          name: jobId
          required: true
          schema: { type: string }
          description: Path param is present but **ignored** — the handler always uses a hardcoded test scan.
    get:
        tags: [Fabricator Jobs]
        operationId: testScanPreprocessor
        summary: Debug — run preprocessor against hardcoded test scan
        description: |
            Spawns the scan-preprocessor Python script against a hardcoded test scan.
            Returns immediately after the subprocess is spawned. Despite taking `:jobId`,
            the handler does not use it. Intended for verifying the preprocessor on a dev box.
        responses:
            "200":
                description: '"OK" (subprocess spawned; not yet finished).'
                content:
                    text/plain:
                        schema: { type: string, example: OK }
            "401": { $ref: "../admin-api.yaml#/components/responses/Unauthorized" }
            "403": { $ref: "../admin-api.yaml#/components/responses/Forbidden" }
        x-access-policies: [Fabricator]
        x-scopes: ["fabricator:read", "admin:all"]

"/admin/fabricator/job/{jobId}/extra":
    parameters:
        - in: path
          name: jobId
          required: true
          schema: { type: string }
    get:
        tags: [Fabricator Jobs]
        operationId: getJobExtraInfo
        summary: Auxiliary job info (bin id + linked inventory)
        description: |
            Returns auxiliary data not on the core `Job` record: the factory bin
            identifier (a hash of `jobId` via `HashUtils.factoryBinHash`) and any
            inventory items linked to the job. Safe to call repeatedly; no writes.
        responses:
            "200":
                description: Auxiliary info.
                content:
                    application/json:
                        schema:
                            type: object
                            required: [binIdentifier, inventoryItems]
                            properties:
                                binIdentifier: { type: string }
                                inventoryItems:
                                    type: object
                                    required: [items, count]
                                    properties:
                                        items:
                                            type: array
                                            items: { $ref: "../admin-api.yaml#/components/schemas/BigInventoryItem" }
                                        count: { type: integer }
            "401": { $ref: "../admin-api.yaml#/components/responses/Unauthorized" }
            "403": { $ref: "../admin-api.yaml#/components/responses/Forbidden" }
            "404":
                description: Job not found.
                content:
                    application/json:
                        schema: { $ref: "../admin-api.yaml#/components/schemas/Error" }
        x-access-policies: [FabricatorAdmin]
        x-scopes: ["fabricator:read", "admin:all"]

"/admin/fabricator/jobs":
    get:
        tags: [Fabricator Jobs]
        operationId: listFabricatorJobs
        summary: List jobs with filters and pagination
        description: |
            The primary query endpoint — used by Arda's fabricator grid and by Looper
            monitor scripts. Most filters are exact-match. `waxId` and `nfcTagId` are
            **shortcut queries** — when either is present the handler returns that one
            job and ignores all other filters.
        parameters:
            - in: query
              name: limit
              schema: { type: integer }
            - in: query
              name: offset
              schema: { type: integer }
            - in: query
              name: jobState
              schema: { type: integer }
              description: Exact `JobState` enum value.
            - in: query
              name: priority
              schema: { type: integer }
            - in: query
              name: minPriority
              schema: { type: integer }
            - in: query
              name: maxPriority
              schema: { type: integer }
            - in: query
              name: minCreatedAt
              schema: { type: integer, format: int64 }
            - in: query
              name: maxCreatedAt
              schema: { type: integer, format: int64 }
            - in: query
              name: ipd
              schema: { type: integer }
            - in: query
              name: minIpd
              schema: { type: integer }
            - in: query
              name: maxIpd
              schema: { type: integer }
            - in: query
              name: name
              schema: { type: string }
            - in: query
              name: bigOrderId
              schema: { type: string }
            - in: query
              name: waxId
              schema: { type: string }
              description: Shortcut — returns the single job with this WaxId.
            - in: query
              name: nfcTagId
              schema: { type: string }
              description: Shortcut — returns the single job with this NFC tag.
        responses:
            "200":
                description: Jobs page.
                content:
                    application/json:
                        schema:
                            type: object
                            required: [items, count]
                            properties:
                                items: { type: array, items: { $ref: "../components/schemas/Job.yaml" } }
                                count: { type: integer }
                                totalCount: { type: integer }
            "401": { $ref: "../admin-api.yaml#/components/responses/Unauthorized" }
            "403": { $ref: "../admin-api.yaml#/components/responses/Forbidden" }
            "422":
                description: Unknown query param, or invalid `jobState` parse.
                content:
                    application/json:
                        schema: { $ref: "../admin-api.yaml#/components/schemas/Error" }
        x-access-policies: [Fabricator, FabricatorReadOnly]
        x-scopes: ["fabricator:read", "admin:all"]
    post:
        tags: [Fabricator Jobs]
        operationId: getJobsWithQueryDeprecated
        summary: Body-based jobs query (deprecated)
        description: |
            Body-based query variant with cursor pagination. **Deprecated** — prefer
            `GET /admin/fabricator/jobs`. Kept around for legacy Looper scripts.
        deprecated: true
        requestBody:
            required: true
            content:
                application/json:
                    schema:
                        type: object
                        properties:
                            limit: { type: integer }
                            cursorId: { type: string }
                        additionalProperties: true
        responses:
            "200":
                description: Jobs page (same shape as the GET variant).
                content:
                    application/json:
                        schema:
                            type: object
                            required: [items, count]
                            properties:
                                items:
                                    type: array
                                    items: { $ref: "../components/schemas/Job.yaml" }
                                count: { type: integer }
                                totalCount: { type: integer }
                                nextCursorId: { type: string }
            "401": { $ref: "../admin-api.yaml#/components/responses/Unauthorized" }
            "403": { $ref: "../admin-api.yaml#/components/responses/Forbidden" }
            "422":
                description: Empty/invalid body, or invalid filter values.
                content:
                    application/json:
                        schema: { $ref: "../admin-api.yaml#/components/schemas/Error" }
        x-access-policies: [Fabricator]
        x-scopes: ["fabricator:write", "admin:all"]

"/admin/fabricator/job/{jobId}/scan":
    parameters:
        - in: path
          name: jobId
          required: true
          schema: { type: string }
    get:
        tags: [Fabricator Jobs]
        operationId: downloadScanFileFromJob
        summary: Download the customer's scan zip
        description: Downloads the raw scan zip uploaded by the customer's iOS face-scan app.
        responses:
            "200":
                description: Scan zip.
                content:
                    application/zip:
                        schema: { type: string, format: binary }
            "401": { $ref: "../admin-api.yaml#/components/responses/Unauthorized" }
            "403": { $ref: "../admin-api.yaml#/components/responses/Forbidden" }
            "404":
                description: Job or scan file not found.
                content:
                    application/json:
                        schema: { $ref: "../admin-api.yaml#/components/schemas/Error" }
        x-access-policies: [FabricatorAdmin]
        x-scopes: ["fabricator:read", "admin:all"]

"/admin/fabricator/job/{jobId}/mesh":
    parameters:
        - in: path
          name: jobId
          required: true
          schema: { type: string }
    get:
        tags: [Fabricator Jobs]
        operationId: downloadMeshFromJob
        summary: Download the processed mesh
        description: Downloads the processed mesh file (output of the scan preprocessor).
        responses:
            "200":
                description: Mesh file.
                content:
                    application/octet-stream:
                        schema: { type: string, format: binary }
            "400":
                description: Job mesh not ready (job state earlier than `AwaitingToolPathCreation`).
                content:
                    application/json:
                        schema: { $ref: "../admin-api.yaml#/components/schemas/Error" }
            "401": { $ref: "../admin-api.yaml#/components/responses/Unauthorized" }
            "403": { $ref: "../admin-api.yaml#/components/responses/Forbidden" }
            "404":
                description: Job or mesh not found.
                content:
                    application/json:
                        schema: { $ref: "../admin-api.yaml#/components/schemas/Error" }
        x-access-policies: [FabricatorAdmin]
        x-scopes: ["fabricator:read", "admin:all"]

"/admin/fabricator/job/{jobId}/debug_start_toolpath":
    parameters:
        - in: path
          name: jobId
          required: true
          schema: { type: string }
    get:
        tags: [Fabricator Jobs]
        operationId: startToolPathFromFusion
        summary: "Production endpoint — start toolpath generation"
        description: |
            Despite the `debug_` prefix, this is a **production** endpoint called by
            Fusion 360 workstations. Allocates a `WaxId`, creates a `ToolPath` record,
            advances the job state, and returns the compressed mesh data Fusion needs to
            generate G-code.
        responses:
            "200":
                description: Toolpath setup payload.
                content:
                    application/json:
                        schema:
                            type: object
                            required: [id, ipd, meshData]
                            properties:
                                id: { type: string, description: New ToolPath id. }
                                ipd:
                                    type: integer
                                    description: Job's IPD — Fusion needs this.
                                meshData:
                                    type: string
                                    description: Deflate-compressed, then base64-encoded mesh.
            "401": { $ref: "../admin-api.yaml#/components/responses/Unauthorized" }
            "403": { $ref: "../admin-api.yaml#/components/responses/Forbidden" }
            "404":
                description: Job not found.
                content:
                    application/json:
                        schema: { $ref: "../admin-api.yaml#/components/schemas/Error" }
            "422":
                description: Job is not in a state that can start toolpath creation.
                content:
                    application/json:
                        schema: { $ref: "../admin-api.yaml#/components/schemas/Error" }
        x-access-policies: [FabricatorAdmin]
        x-scopes: ["fabricator:read", "admin:all"]

"/admin/fabricator/job/{jobId}/debug_upload_toolpath":
    parameters:
        - in: path
          name: jobId
          required: true
          schema: { type: string }
    put:
        tags: [Fabricator Jobs]
        operationId: uploadToolPathForJob
        summary: "Production endpoint — upload finished G-code"
        description: |
            Despite the `debug_` prefix, this is a **production** endpoint called by
            Fusion 360 to upload the finished G-code file. Stores the file on S3,
            updates the ToolPath record, and runs `ToolPathCreationSuccess`.
            **Note:** if a job has multiple toolpaths, this endpoint only touches the
            first one (`toolPaths[0]`).
        requestBody:
            required: true
            content:
                multipart/form-data:
                    schema:
                        type: object
                        required: [file]
                        properties:
                            file:
                                type: string
                                format: binary
                                description: Completed `.nc` G-code file.
        responses:
            "200":
                description: '"OK".'
                content:
                    text/plain:
                        schema: { type: string, example: OK }
            "401": { $ref: "../admin-api.yaml#/components/responses/Unauthorized" }
            "403": { $ref: "../admin-api.yaml#/components/responses/Forbidden" }
            "404":
                description: Tool path or job not found.
                content:
                    application/json:
                        schema: { $ref: "../admin-api.yaml#/components/schemas/Error" }
        x-access-policies: [FabricatorAdmin]
        x-scopes: ["fabricator:write", "admin:all"]

"/admin/fabricator/job/{jobId}/debug_revert_history":
    parameters:
        - in: path
          name: jobId
          required: true
          schema: { type: string }
    get:
        tags: [Fabricator Jobs]
        operationId: revertJobHistory
        summary: Undo the last history entry
        description: |
            Pops the last history entry on a job and reverses its state-machine effect.
            Use sparingly — revert semantics depend on what the previous action was.
        responses:
            "200":
                description: Updated `Job`.
                content:
                    application/json:
                        schema: { $ref: "../components/schemas/Job.yaml" }
            "401": { $ref: "../admin-api.yaml#/components/responses/Unauthorized" }
            "403": { $ref: "../admin-api.yaml#/components/responses/Forbidden" }
        x-access-policies: [Admin, FabricatorAdmin]
        x-scopes: ["fabricator:read", "admin:all"]

"/admin/fabricator/job/{jobId}/toolpath":
    parameters:
        - in: path
          name: jobId
          required: true
          schema: { type: string }
    get:
        tags: [Fabricator Jobs]
        operationId: downloadToolPath
        summary: Download the WaxId-filtered G-code
        description: |
            Downloads the G-code toolpath file filtered by the job's `WaxId`. The
            G-code from Fusion contains 20 `(Bore<N>)` sections; only the bores whose
            bit is `1` in the WaxId are kept. Response is `text/plain` (attachment
            headers temporarily disabled). Uses the **most recent** toolpath snapshot.
        responses:
            "200":
                description: G-code (filtered).
                content:
                    text/plain:
                        schema: { type: string }
            "401": { $ref: "../admin-api.yaml#/components/responses/Unauthorized" }
            "403": { $ref: "../admin-api.yaml#/components/responses/Forbidden" }
            "404":
                description: Job or toolpath not found.
                content:
                    application/json:
                        schema: { $ref: "../admin-api.yaml#/components/schemas/Error" }
        x-access-policies: [FabricatorAdmin]
        x-scopes: ["fabricator:read", "admin:all"]

"/admin/fabricator/jobs/checklist":
    get:
        tags: [Fabricator Jobs]
        operationId: runJobsChecklist
        summary: Run jobs checklist (not implemented)
        description: |
            **Not implemented.** The underlying `runJobsChecklist` is a stub; the
            endpoint always throws and surfaces as a 500. Kept registered so callers
            don't get a 404. Don't rely on it in new code.
        responses:
            "500":
                description: Always — handler is a stub.
                content:
                    application/json:
                        schema: { $ref: "../admin-api.yaml#/components/schemas/Error" }
            "401": { $ref: "../admin-api.yaml#/components/responses/Unauthorized" }
            "403": { $ref: "../admin-api.yaml#/components/responses/Forbidden" }
        x-access-policies: [FabricatorAdmin, FabricatorReadOnly]
        x-scopes: ["fabricator:read", "admin:all"]

"/admin/fabricator/jobs/report":
    get:
        tags: [Fabricator Jobs]
        operationId: runJobsReport
        summary: Jobs dashboard (totals + per-state)
        description: |
            High-level jobs dashboard — totals plus a per-state breakdown. Posts a
            formatted summary to the `Fabricator` Discord channel as a side effect.
            Expensive — fetches every job from the DB; avoid calling in a loop.
        responses:
            "200":
                description: Report payload.
                content:
                    application/json:
                        schema:
                            type: object
                            required: [totalJobs, totalJobsInProgress, totalJobsReadyToShip, jobsByJobState]
                            properties:
                                totalJobs: { type: integer }
                                totalJobsInProgress: { type: integer }
                                totalJobsReadyToShip: { type: integer }
                                jobsByJobState:
                                    type: object
                                    additionalProperties: { type: integer }
                                    description: Keyed by `JobState` enum **string name** (e.g. `AwaitingInit`).
            "401": { $ref: "../admin-api.yaml#/components/responses/Unauthorized" }
            "403": { $ref: "../admin-api.yaml#/components/responses/Forbidden" }
        x-access-policies: [Admin, FabricatorReadOnly]
        x-scopes: ["fabricator:read", "admin:all"]

"/admin/fabricator/jobs/report2":
    get:
        tags: [Fabricator Jobs]
        operationId: runProblemJobsReport
        summary: Problem-jobs dashboard
        description: |
            Returns jobs stuck in problem states beyond expected business-day
            thresholds. Used by Looper / monitor scripts to surface issues on Discord.
            `ManufacturingFailed` is flagged regardless of age.
        responses:
            "200":
                description: Problem-jobs report.
                content:
                    application/json:
                        schema:
                            type: object
                            required: [jobsByProblemStates, jobsWithoutInventory, problemJobs]
                            properties:
                                jobsByProblemStates:
                                    type: object
                                    properties:
                                        AwaitingToolPathCreation: { type: array, items: { type: string } }
                                        AwaitingManufacture: { type: array, items: { type: string } }
                                        ManufacturingFailed: { type: array, items: { type: string } }
                                        AwaitingQA: { type: array, items: { type: string } }
                                        AwaitingNFC: { type: array, items: { type: string } }
                                        ScanVerificationFailed: { type: array, items: { type: string } }
                                jobsWithoutInventory:
                                    type: array
                                    items: { type: string }
                                    description: Jobs in `AwaitingFulfilment` with no inventory item.
                                problemJobs:
                                    type: integer
                                    description: Total count of flagged jobs.
            "401": { $ref: "../admin-api.yaml#/components/responses/Unauthorized" }
            "403": { $ref: "../admin-api.yaml#/components/responses/Forbidden" }
        x-access-policies: [Admin, FabricatorReadOnly]
        x-scopes: ["fabricator:read", "admin:all"]

"/admin/fabricator/product/{id}":
    parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
          description: Numeric Shopify product id (parsed as integer).
    get:
        tags: [Fabricator Jobs]
        operationId: getFabricatorProduct
        summary: Fetch a Shopify product
        description: Read-only passthrough to Shopify Admin API for a product, scoped to the fabricator domain.
        responses:
            "200":
                description: Shopify product payload — see Shopify's spec for the full shape.
                content:
                    application/json:
                        schema:
                            type: object
                            properties:
                                id: { type: integer, format: int64 }
                                title: { type: string }
                                handle: { type: string }
                                vendor: { type: string }
                                product_type: { type: string }
                                status: { type: string }
                                tags: { type: string }
                                variants:
                                    type: array
                                    items:
                                        type: object
                                        additionalProperties: true
                                created_at: { type: string, format: date-time }
                                updated_at: { type: string, format: date-time }
                            additionalProperties: true
            "401": { $ref: "../admin-api.yaml#/components/responses/Unauthorized" }
            "403": { $ref: "../admin-api.yaml#/components/responses/Forbidden" }
            "422":
                description: Blank or non-numeric `id`.
                content:
                    application/json:
                        schema: { $ref: "../admin-api.yaml#/components/schemas/Error" }
        x-access-policies: [Fabricator]
        x-scopes: ["fabricator:read", "admin:all"]

"/admin/fabricator/variant/{id}":
    parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
          description: Numeric Shopify variant id (parsed as integer).
    get:
        tags: [Fabricator Jobs]
        operationId: getFabricatorVariant
        summary: Fetch a Shopify variant
        description: Read-only passthrough to Shopify Admin API for a product variant.
        responses:
            "200":
                description: Shopify variant payload — see Shopify's spec for the full shape.
                content:
                    application/json:
                        schema:
                            type: object
                            properties:
                                id: { type: integer, format: int64 }
                                product_id: { type: integer, format: int64 }
                                title: { type: string }
                                sku: { type: string }
                                price: { type: string }
                                inventory_quantity: { type: integer }
                                weight: { type: number }
                                weight_unit: { type: string }
                                barcode: { type: string }
                                option1: { type: string }
                                option2: { type: string }
                                option3: { type: string }
                                requires_shipping: { type: boolean }
                            additionalProperties: true
            "401": { $ref: "../admin-api.yaml#/components/responses/Unauthorized" }
            "403": { $ref: "../admin-api.yaml#/components/responses/Forbidden" }
            "422":
                description: Blank or non-numeric `id`.
                content:
                    application/json:
                        schema: { $ref: "../admin-api.yaml#/components/schemas/Error" }
        x-access-policies: [Fabricator]
        x-scopes: ["fabricator:read", "admin:all"]
