/**
 * Fleet REST endpoints — agent gigafactory coordinator.
 *
 * POST   /fleet/jobs                  submit a job (idempotent)
 * GET    /fleet/jobs                  list jobs (by stage / idempotencyKey)
 * GET    /fleet/jobs/:id              get one job
 * PATCH  /fleet/jobs/:id              fenced state transition (carries leaseEpoch)
 * POST   /fleet/claim                 atomic claim for a factory
 * POST   /fleet/jobs/:id/lease/renew  renew a held lease
 * POST   /fleet/jobs/:id/lease/release release a held lease
 * POST   /fleet/factories/heartbeat   factory liveness
 * GET    /fleet/jobs/:id/runs         job run history
 * GET    /fleet/jobs/:id/events       append-only event stream
 * GET    /fleet/jobs/:id/events/stream live event stream (SSE, resumable)
 * GET    /fleet/metrics              fleet metrics + alerts (queue depth, utilization)
 * GET    /fleet/metrics/prom         Prometheus exposition (all products, labelled)
 * GET    /fleet/autoscale            capacity scale recommendation for this product (§5)
 * GET    /fleet/autoscale/all        capacity scale recommendations for all products (§5)
 * GET    /fleet/queue-state          per-product queue version (M0 RU gate — cheap point read)
 * POST   /fleet/jobs/:id/review/request route a building job into the review gate
 * POST   /fleet/jobs/:id/review        submit a reviewer decision (approve/reject)
 * POST   /fleet/jobs/:id/artifacts    upload a run output (base64 body → blob + pointer)
 * GET    /fleet/jobs/:id/artifacts    list a job's artifact pointers
 * GET    /fleet/artifacts/:artifactId pointer + fresh short-lived SAS download URL
 * DELETE /fleet/artifacts/:artifactId delete pointer (and backing blob)
 *
 * All routes require auth + a resolved productId, exactly like the items module.
 */

import type { FastifyInstance } from 'fastify';
import { requireProductAccess } from '../../lib/request-context.js';
import { BadRequestError, ConflictError, ForbiddenError, NotFoundError } from '../../lib/errors.js';
import { extractAuth } from '../../lib/auth.js';
import * as repo from './repository.js';
import * as coordinator from './coordinator.js';
import * as artifactsBlob from './artifacts-blob.js';
import * as enrollment from './enrollment.js';
import * as trackerBridge from './tracker-bridge.js';
import { getReaperStats } from './reaper.js';
import { getEngineBreakerSnapshot } from './engine-breaker.js';
import { renderFleetMetricsProm, scrapeTokenMatches } from './prometheus.js';
import {
  recommendScaling,
  recommendScalingStateful,
  scalingThresholdsFromEnv,
} from './autoscaler.js';
import { CORRELATION_HEADER, resolveCorrelationId } from './trace.js';
import { getAllProducts } from '../products/cache.js';
import {
  SubmitJobSchema,
  ListJobsQuerySchema,
  PatchJobSchema,
  SubmitReviewSchema,
  ClaimSchema,
  RenewLeaseSchema,
  ReleaseLeaseSchema,
  HeartbeatSchema,
  UploadArtifactSchema,
  EnrollFactorySchema,
  RotateTokenSchema,
  RevokeTokenSchema,
  IngestItemSchema,
  EchoJobSchema,
  SubmitChildrenSchema,
  UpsertBudgetSchema,
  UpdateDraftSchema,
} from './types.js';

function badRequest(issues: { message: string }[]): never {
  throw new BadRequestError(issues.map(i => i.message).join('; '));
}

/**
 * Resolve the caller's productId and enforce that any `:productId` path param
 * matches it. Prevents a caller authenticated for product A from reading or
 * mutating another product's fleet budget. Returns the caller's productId.
 */
async function requireOwnProduct(req: import('fastify').FastifyRequest): Promise<string> {
  const pid = await requireProductAccess(req);
  const { productId } = req.params as { productId?: string };
  if (productId && productId !== pid) {
    throw new ForbiddenError("Cannot access another product's fleet budget");
  }
  return pid;
}

const delay = (ms: number): Promise<void> => new Promise(resolve => setTimeout(resolve, ms));

/**
 * Echo a job's trace-context correlation id back to the caller on the
 * `x-correlation-id` response header (§4), so an external factory can carry it
 * on its subsequent renew/release calls and a tracer can stitch the work-unit
 * across the coordinator↔runner boundary. No-op when absent (legacy jobs).
 */
function echoCorrelation(reply: import('fastify').FastifyReply, correlationId?: string): void {
  if (correlationId) reply.header(CORRELATION_HEADER, correlationId);
}

/** Parse an integer query param, clamping to [min, max]; fall back to `fallback`. */
function clampInt(raw: string | undefined, fallback: number, min: number, max: number): number {
  const n = Number.parseInt(raw ?? '', 10);
  if (!Number.isFinite(n)) return fallback;
  return Math.min(max, Math.max(min, n));
}

export async function fleetRoutes(app: FastifyInstance) {
  // ── Submit (idempotent) ──
  app.post('/fleet/jobs', async (req, reply) => {
    await extractAuth(req);
    const parsed = SubmitJobSchema.safeParse(req.body);
    if (!parsed.success) badRequest(parsed.error.issues);
    const pid = parsed.data.productId || (await requireProductAccess(req));
    // Mint/propagate the trace-context correlation id from the inbound request
    // headers and bind it to the request logger so the whole submit path is
    // correlatable (§4). The job persists it; the run/lease/events inherit it.
    const correlationId = resolveCorrelationId(req.headers);
    req.log = req.log.child({ correlationId });
    const result = await coordinator.submitJob(pid, parsed.data, { correlationId });
    echoCorrelation(reply, result.job.correlationId ?? correlationId);
    reply.code(result.outcome === 'created' ? 201 : 200);
    return { outcome: result.outcome, job: result.job };
  });

  // ── List ──
  app.get('/fleet/jobs', async req => {
    await extractAuth(req);
    const parsed = ListJobsQuerySchema.safeParse(req.query);
    if (!parsed.success) badRequest(parsed.error.issues);
    const q = parsed.data;
    const pid = q.productId || (await requireProductAccess(req));
    const jobs = await repo.listJobs({
      productId: pid,
      stage: q.stage,
      idempotencyKey: q.idempotencyKey,
      limit: q.limit,
      offset: q.offset,
    });
    return { jobs, limit: q.limit, offset: q.offset };
  });

  // ── Get one ──
  app.get('/fleet/jobs/:id', async req => {
    await extractAuth(req);
    const { id } = req.params as { id: string };
    const pid = await requireProductAccess(req);
    const job = await repo.getJob(id, pid);
    if (!job) throw new NotFoundError('Job not found');
    return job;
  });

  // ── Fenced state transition ──
  app.patch('/fleet/jobs/:id', async (req, reply) => {
    await extractAuth(req);
    const { id } = req.params as { id: string };
    const pid = await requireProductAccess(req);
    const parsed = PatchJobSchema.safeParse(req.body);
    if (!parsed.success) badRequest(parsed.error.issues);
    const res = await coordinator.patchJobFenced(id, pid, parsed.data);
    if (!res.ok) {
      if (res.reason === 'not_found') throw new NotFoundError('Job not found');
      if (res.reason === 'fenced') {
        throw new ConflictError('stale leaseEpoch — transition fenced (job reassigned)');
      }
      throw new ConflictError('concurrent update conflict — retry');
    }
    echoCorrelation(reply, res.doc.correlationId);
    // §10: opt-in (FLEET_TRACKER_ECHO, default OFF) downstream echo of the new
    // stage to the linked tracker Item. Never blocks/fails the transition.
    await trackerBridge.maybeEchoOnTransition(pid, id, req.log);
    return res.doc;
  });

  // ── Operator actions (requeue / reject / cancel) ──
  // Operator-initiated, lease-free lifecycle control from the fleet control plane.
  // Fences any current factory holder by bumping the lease epoch (§14 Phase 3).
  app.post('/fleet/jobs/:id/actions/:action', async req => {
    await extractAuth(req);
    const { id, action } = req.params as { id: string; action: string };
    const pid = await requireProductAccess(req);
    if (!(coordinator.OPERATOR_ACTIONS as readonly string[]).includes(action)) {
      throw new BadRequestError(
        `unknown operator action '${action}' (expected: ${coordinator.OPERATOR_ACTIONS.join(', ')})`
      );
    }
    const res = await coordinator.operatorAction(id, pid, action as coordinator.OperatorActionKind);
    if (!res.ok) {
      if (res.reason === 'not_found') throw new NotFoundError('Job not found');
      if (res.reason === 'invalid_state') {
        throw new ConflictError('job is shipped (terminal) — operator action not allowed');
      }
      throw new ConflictError('concurrent update conflict — retry');
    }
    await trackerBridge.maybeEchoOnTransition(pid, id, req.log);
    return res.doc;
  });

  // ── Edit a not-yet-picked-up job's prompt/config (draft/queued/blocked) ──
  app.patch('/fleet/jobs/:id/draft', async req => {
    await extractAuth(req);
    const { id } = req.params as { id: string };
    const pid = await requireProductAccess(req);
    const parsed = UpdateDraftSchema.safeParse(req.body);
    if (!parsed.success) badRequest(parsed.error.issues);
    const res = await coordinator.updateDraft(id, pid, parsed.data);
    if (!res.ok) {
      if (res.reason === 'not_found') throw new NotFoundError('Job not found');
      throw new ConflictError('job already picked up — prompt is read-only');
    }
    return res.doc;
  });

  // ── Submit a draft → queued (or blocked if it has unmet deps) ──
  app.post('/fleet/jobs/:id/submit', async req => {
    await extractAuth(req);
    const { id } = req.params as { id: string };
    const pid = await requireProductAccess(req);
    const res = await coordinator.submitDraft(id, pid);
    if (!res.ok) {
      if (res.reason === 'not_found') throw new NotFoundError('Job not found');
      throw new ConflictError('concurrent update conflict — retry');
    }
    await trackerBridge.maybeEchoOnTransition(pid, id, req.log);
    return res.doc;
  });

  // ── Multi-reviewer human gate (§14 Phase 3) ──
  app.post('/fleet/jobs/:id/review/request', async req => {
    await extractAuth(req);
    const { id } = req.params as { id: string };
    const pid = await requireProductAccess(req);
    const body = (req.body ?? {}) as { requiredApprovals?: number; reviewers?: string[] };
    const res = await coordinator.requestReview(id, pid, {
      requiredApprovals: body.requiredApprovals,
      reviewers: body.reviewers,
    });
    if (!res.ok) {
      if (res.reason === 'not_found') throw new NotFoundError('Job not found');
      if (res.reason === 'invalid_state') {
        throw new ConflictError('only a building job can be routed to review');
      }
      if (res.reason === 'invalid_policy') {
        throw new BadRequestError('requiredApprovals exceeds the number of allowed reviewers');
      }
      throw new ConflictError('concurrent update conflict — retry');
    }
    await trackerBridge.maybeEchoOnTransition(pid, id, req.log);
    return res.doc;
  });

  app.post('/fleet/jobs/:id/review', async req => {
    await extractAuth(req);
    const { id } = req.params as { id: string };
    const pid = await requireProductAccess(req);
    const parsed = SubmitReviewSchema.safeParse(req.body);
    if (!parsed.success) badRequest(parsed.error.issues);
    const res = await coordinator.submitReview(id, pid, parsed.data);
    if (!res.ok) {
      if (res.reason === 'not_found') throw new NotFoundError('Job not found');
      if (res.reason === 'invalid_state') {
        throw new ConflictError('job is not awaiting review');
      }
      if (res.reason === 'not_authorized') {
        throw new BadRequestError('reviewer is not on the allowlist for this job');
      }
      throw new ConflictError('concurrent update conflict — retry');
    }
    await trackerBridge.maybeEchoOnTransition(pid, id, req.log);
    return { ...res.doc, gate: res.gate };
  });

  app.post('/fleet/claim', async (req, reply) => {
    await extractAuth(req);
    const parsed = ClaimSchema.safeParse(req.body);
    if (!parsed.success) badRequest(parsed.error.issues);
    const pid = parsed.data.productId || (await requireProductAccess(req));
    // §12: when enforcement is ON, the token must cover the productId + requested
    // capabilities + factoryId; the claim is then CONSTRAINED to the verified scope
    // (a factory cannot claim outside it). When OFF, behaves exactly as before.
    const scope = await enrollment.enforceFactoryToken(req, {
      productId: pid,
      capabilities: parsed.data.capabilities,
      factoryId: parsed.data.factoryId,
    });
    const claim = await coordinator.claimNextJob({
      productId: scope?.productId ?? pid,
      factoryId: scope?.factoryId ?? parsed.data.factoryId,
      capabilities: scope ? scope.capabilities : parsed.data.capabilities,
      leaseSeconds: parsed.data.leaseSeconds,
    });
    if (!claim) return { claimed: false };
    // Hand the job's trace back to the factory so it can carry it on its
    // renew/release calls — closing the loop across the coordinator↔runner
    // boundary (§4). Also bind it to the request logger.
    if (claim.job.correlationId) {
      req.log = req.log.child({ correlationId: claim.job.correlationId });
    }
    echoCorrelation(reply, claim.job.correlationId);
    return { claimed: true, ...claim };
  });

  // ── Lease renew ──
  app.post('/fleet/jobs/:id/lease/renew', async (req, reply) => {
    await extractAuth(req);
    const { id } = req.params as { id: string };
    const pid = await requireProductAccess(req);
    const parsed = RenewLeaseSchema.safeParse(req.body);
    if (!parsed.success) badRequest(parsed.error.issues);
    const res = await coordinator.renewLease(
      id,
      pid,
      parsed.data.leaseEpoch,
      parsed.data.leaseSeconds
    );
    if (!res.ok) {
      if (res.reason === 'not_found') throw new NotFoundError('Job or lease not found');
      if (res.reason === 'fenced') throw new ConflictError('stale leaseEpoch — renew fenced');
      throw new ConflictError('lease renew conflict — retry');
    }
    echoCorrelation(reply, res.doc.correlationId);
    return res.doc;
  });

  // ── Lease release ──
  app.post('/fleet/jobs/:id/lease/release', async (req, reply) => {
    await extractAuth(req);
    const { id } = req.params as { id: string };
    const pid = await requireProductAccess(req);
    const parsed = ReleaseLeaseSchema.safeParse(req.body);
    if (!parsed.success) badRequest(parsed.error.issues);
    const res = await coordinator.releaseLease(id, pid, parsed.data.leaseEpoch, parsed.data.stage, {
      insights: parsed.data.insights,
      result: parsed.data.result,
      prUrl: parsed.data.prUrl,
      branch: parsed.data.branch,
      prState: parsed.data.prState,
    });
    if (!res.ok) {
      if (res.reason === 'not_found') throw new NotFoundError('Job or lease not found');
      if (res.reason === 'fenced') throw new ConflictError('stale leaseEpoch — release fenced');
      throw new ConflictError('lease release conflict — retry');
    }
    echoCorrelation(reply, res.doc.correlationId);
    // §10: release often carries a terminal stage (shipped/failed) — echo it (opt-in).
    if (parsed.data.stage) await trackerBridge.maybeEchoOnTransition(pid, id, req.log);
    return res.doc;
  });

  // ── Reconcile PR state against GitHub (detect an externally-merged PR) ──
  // The platform only learns a PR merged when it merges it / a runner reports it;
  // a merge done in the GitHub UI is otherwise invisible. This pulls the live PR
  // state via `gh` and flips the run's prState to `merged` when GitHub says so.
  app.post('/fleet/jobs/:id/pr/reconcile', async req => {
    await extractAuth(req);
    const { id } = req.params as { id: string };
    const pid = await requireProductAccess(req);
    const res = await coordinator.reconcileJobPrState(id, pid);
    if (!res.ok) throw new NotFoundError('Job not found');
    // A merge often coincides with a terminal outcome — echo to the tracker Item.
    if (res.updated) await trackerBridge.maybeEchoOnTransition(pid, id, req.log);
    return res;
  });

  // ── Factory heartbeat ──
  app.post('/fleet/factories/heartbeat', async req => {
    await extractAuth(req);
    const parsed = HeartbeatSchema.safeParse(req.body);
    if (!parsed.success) badRequest(parsed.error.issues);
    const pidCandidate = parsed.data.productId || (await requireProductAccess(req));
    // §12: gated token check (default OFF). When ON the token must cover this
    // product + factory; the heartbeat is then bound to the verified scope.
    const scope = await enrollment.enforceFactoryToken(req, {
      productId: pidCandidate,
      factoryId: parsed.data.factoryId,
    });
    const pid = scope?.productId ?? pidCandidate;
    // §12 trust boundary: when enforcement is ON, a factory may advertise only the
    // capabilities it was ENROLLED with — intersect its self-reported caps with the
    // token scope (it can report fewer, e.g. an engine temporarily unavailable, but
    // never MORE). This keeps a factory from advertising e.g. engine:codex it wasn't
    // granted, which would otherwise pollute the engine picker and route decisions.
    const advertisedCaps = scope
      ? (parsed.data.capabilities ?? []).filter(c => scope.capabilities.includes(c))
      : parsed.data.capabilities;
    await coordinator.heartbeat({
      productId: pid,
      factoryId: parsed.data.factoryId,
      descriptor: parsed.data.descriptor,
      capabilities: advertisedCaps,
      health: parsed.data.health,
      load: parsed.data.load,
      seatLimit: parsed.data.seatLimit,
    });
    const factory = await repo.getFactory(parsed.data.factoryId, pid);
    return { ok: true, factory };
  });

  // ── List factories for the product (powers the fleet map + engine picker) ──
  app.get('/fleet/factories', async req => {
    await extractAuth(req);
    const pid = await requireProductAccess(req);
    const factories = await repo.listFactories(pid);
    return { factories };
  });

  // ── Runs ──
  app.get('/fleet/jobs/:id/runs', async req => {
    await extractAuth(req);
    const { id } = req.params as { id: string };
    const runs = await repo.listRunsByJob(id);
    return { runs };
  });

  // ── Events ──
  app.get('/fleet/jobs/:id/events', async req => {
    await extractAuth(req);
    const { id } = req.params as { id: string };
    const events = await repo.listEvents(id);
    return { events };
  });

  // ── Live event stream (SSE, §17 single-stream contract) ──
  // Emits a snapshot of existing events (seq > Last-Event-ID), then long-polls
  // the append-only stream for new events. Closes after `maxMs` so EventSource
  // clients reconnect cleanly (carrying Last-Event-ID to resume). Designed to
  // degrade gracefully: consumers fall back to `GET …/events` polling on error.
  app.get('/fleet/jobs/:id/events/stream', async (req, reply) => {
    await extractAuth(req);
    const { id } = req.params as { id: string };
    const query = (req.query ?? {}) as Record<string, string | undefined>;

    // Resume cursor: `Last-Event-ID` header (set by EventSource on reconnect)
    // takes precedence over the `lastEventId` query param. `-1` => send all.
    const headerId = req.headers['last-event-id'];
    const rawCursor = (Array.isArray(headerId) ? headerId[0] : headerId) ?? query.lastEventId;
    let cursor = Number.parseInt(rawCursor ?? '', 10);
    if (!Number.isFinite(cursor)) cursor = -1;

    const pollMs = clampInt(query.pollMs, 1000, 50, 10_000);
    const maxMs = clampInt(query.maxMs, 25_000, 0, 60_000);

    reply.hijack();
    const raw = reply.raw;
    raw.writeHead(200, {
      'content-type': 'text/event-stream',
      'cache-control': 'no-cache, no-transform',
      connection: 'keep-alive',
      'x-accel-buffering': 'no',
    });
    // Tell the client how long to wait before reconnecting.
    raw.write('retry: 3000\n\n');

    let open = true;
    const stop = () => {
      open = false;
    };
    req.raw.on('close', stop);
    req.raw.on('error', stop);

    const flushNew = async (): Promise<void> => {
      const all = await repo.listEvents(id);
      for (const evt of all) {
        if (evt.seq <= cursor) continue;
        if (!open) return;
        raw.write(`id: ${evt.seq}\nevent: fleet-event\ndata: ${JSON.stringify(evt)}\n\n`);
        cursor = evt.seq;
      }
    };

    try {
      await flushNew();
      const deadline = Date.now() + maxMs;
      while (open && Date.now() < deadline) {
        await delay(pollMs);
        if (!open) break;
        await flushNew();
        if (open) raw.write(': keepalive\n\n');
      }
    } catch {
      // Surface a terminal error event; the client will reconnect/fall back.
      if (open) raw.write('event: error\ndata: {"message":"stream interrupted"}\n\n');
    } finally {
      req.raw.off('close', stop);
      req.raw.off('error', stop);
      raw.end();
    }
  });

  // ── Scoring explainability — why does this job route where it does? (§7) ──
  app.get('/fleet/jobs/:id/explain', async req => {
    await extractAuth(req);
    const { id } = req.params as { id: string };
    const pid = await requireProductAccess(req);
    const explain = await coordinator.explainJob(id, pid);
    if (!explain) throw new NotFoundError('Job not found');
    return explain;
  });

  // ── Fleet metrics + alerting (§17) ──
  app.get('/fleet/metrics', async req => {
    await extractAuth(req);
    const pid = await requireProductAccess(req);
    const metrics = await coordinator.fleetMetrics(pid);
    // Attach process-wide recovery/GC telemetry (the reaper runs across products,
    // so these counters are global — operator visibility into recovery activity).
    // engineBreakers exposes any (factory, engine) pairs currently tripped/probing
    // so the dashboard can flag routing that's being restricted (§2).
    return { ...metrics, reaper: getReaperStats(), engineBreakers: getEngineBreakerSnapshot() };
  });

  // ── Prometheus exposition for fleet metrics (§4 ops export) ──
  // GLOBAL (all products, labelled) so a single scrape target covers the fleet.
  // Auth: a matching `FLEET_METRICS_TOKEN` bearer (the Prometheus scrape path),
  // else an admin/super_admin JWT — never world-readable by default.
  app.get('/fleet/metrics/prom', async (req, reply) => {
    if (!scrapeTokenMatches(req.headers['authorization'])) {
      const auth = await extractAuth(req);
      if (auth.role !== 'admin' && auth.role !== 'super_admin') {
        throw new ForbiddenError('admin role or a metrics scrape token is required');
      }
    }
    const products = getAllProducts();
    const thresholds = scalingThresholdsFromEnv();
    const perProduct = await Promise.all(
      products.map(async p => {
        const metrics = await coordinator.fleetMetrics(p.productId);
        // RAW recommendation here (not the stateful one): a scrape must never
        // mutate the hysteresis cooldown anchors that the /autoscale endpoints own.
        return {
          productId: p.productId,
          metrics,
          recommendation: recommendScaling(metrics, thresholds),
        };
      })
    );
    const body = renderFleetMetricsProm({
      products: perProduct,
      reaper: getReaperStats(),
      breakers: getEngineBreakerSnapshot(),
    });
    reply.type('text/plain; version=0.0.4; charset=utf-8');
    return body;
  });

  // ── Capacity autoscaling signal (§5) ──
  // Advisory scale recommendation for THIS product, derived from the live
  // saturation alerts (no_live_capacity / saturated / queue_starvation) + seat
  // utilization. Pull model: an external scaler GETs this and decides — the
  // coordinator never starts/stops factories itself. Thresholds are tunable via
  // FLEET_AUTOSCALE_* env.
  app.get('/fleet/autoscale', async req => {
    await extractAuth(req);
    const pid = await requireProductAccess(req);
    const metrics = await coordinator.fleetMetrics(pid);
    // Stateful: anti-flap hysteresis debounces direction reversals so a consumer
    // acting on this signal cannot thrash capacity (critical scale-out bypasses).
    return recommendScalingStateful(metrics, scalingThresholdsFromEnv());
  });

  // ── Capacity autoscaling signal — GLOBAL (all products) ──
  // One call for a fleet-wide scaler controller. Auth mirrors the Prometheus
  // exposition: a matching FLEET_METRICS_TOKEN bearer, else an admin JWT.
  app.get('/fleet/autoscale/all', async req => {
    if (!scrapeTokenMatches(req.headers['authorization'])) {
      const auth = await extractAuth(req);
      if (auth.role !== 'admin' && auth.role !== 'super_admin') {
        throw new ForbiddenError('admin role or a metrics scrape token is required');
      }
    }
    const thresholds = scalingThresholdsFromEnv();
    const recommendations = await Promise.all(
      getAllProducts().map(async p =>
        recommendScalingStateful(await coordinator.fleetMetrics(p.productId), thresholds)
      )
    );
    return {
      generatedAt: new Date().toISOString(),
      recommendations,
      // Quick roll-up so a controller can act on the actionable subset.
      actionable: recommendations.filter(r => r.action !== 'hold'),
    };
  });

  // ── M0 RU gate: per-product queue version (cheap ~1 RU point read) ──
  // A polling factory reads this each tick and only runs the expensive claim when
  // `version` changed since its last attempt. See
  // docs/GIGAFACTORY/FLEET_DISPATCH_REDESIGN.md §8/§12.
  app.get('/fleet/queue-state', async req => {
    await extractAuth(req);
    const pid = await requireProductAccess(req);
    return { productId: pid, version: await repo.getQueueVersion(pid) };
  });

  // ── Artifacts: upload (base64 body → blob + pointer) ──
  app.post('/fleet/jobs/:id/artifacts', async (req, reply) => {
    await extractAuth(req);
    const { id: jobId } = req.params as { id: string };
    const parsed = UploadArtifactSchema.safeParse(req.body);
    if (!parsed.success) badRequest(parsed.error.issues);
    // Review fix: the request/auth productId is authoritative — a spoofed
    // body.productId must NOT override it (prevents writing into another product).
    const pid = await requireProductAccess(req);
    const bytes = Buffer.from(parsed.data.contentBase64, 'base64');
    if (bytes.length === 0) badRequest([{ message: 'contentBase64 decoded to empty bytes' }]);
    const { artifact, downloadUrl } = await artifactsBlob.uploadArtifact({
      productId: pid,
      jobId,
      kind: parsed.data.kind,
      bytes,
      contentType: parsed.data.contentType,
      runId: parsed.data.runId,
    });
    reply.code(201);
    return { artifact, downloadUrl };
  });

  // ── Artifacts: list a job's pointers (productId-scoped) ──
  app.get('/fleet/jobs/:id/artifacts', async req => {
    await extractAuth(req);
    const { id: jobId } = req.params as { id: string };
    // Review fix: scope to the request productId so a caller only sees its own
    // product's artifact pointers for this job.
    const pid = await requireProductAccess(req);
    const artifacts = await repo.listArtifactsByJob(jobId, pid);
    return { artifacts };
  });

  // ── Artifacts: pointer + fresh SAS download URL ──
  app.get('/fleet/artifacts/:artifactId', async req => {
    await extractAuth(req);
    const { artifactId } = req.params as { artifactId: string };
    const pid = await requireProductAccess(req);
    const found = await artifactsBlob.getArtifactDownload(artifactId, pid);
    if (!found) throw new NotFoundError('Artifact not found');
    return found;
  });

  // ── Artifacts: delete pointer (and backing blob) ──
  app.delete('/fleet/artifacts/:artifactId', async req => {
    await extractAuth(req);
    const { artifactId } = req.params as { artifactId: string };
    const pid = await requireProductAccess(req);
    const deleted = await artifactsBlob.deleteArtifact(artifactId, pid);
    if (!deleted) throw new NotFoundError('Artifact not found');
    return { deleted: true };
  });

  // ── Enrollment + scoped tokens (§12) ──
  // Management endpoints (user auth + productId). The plaintext token is returned
  // ONCE here; only its hash is persisted.
  app.post('/fleet/factories/enroll', async (req, reply) => {
    await extractAuth(req);
    const parsed = EnrollFactorySchema.safeParse(req.body);
    if (!parsed.success) badRequest(parsed.error.issues);
    const pid = parsed.data.productId || (await requireProductAccess(req));
    const result = await enrollment.enrollFactory({
      productId: pid,
      factoryId: parsed.data.factoryId,
      capabilities: parsed.data.capabilities,
      label: parsed.data.label,
    });
    reply.code(201);
    return {
      factory: result.factory,
      tokenId: result.tokenId,
      token: result.token,
      scope: result.scope,
    };
  });

  // ── Rotate a factory's token (old token honored for a short grace overlap) ──
  app.post('/fleet/factories/:id/token/rotate', async req => {
    await extractAuth(req);
    const { id: factoryId } = req.params as { id: string };
    const parsed = RotateTokenSchema.safeParse(req.body ?? {});
    if (!parsed.success) badRequest(parsed.error.issues);
    const pid = parsed.data.productId || (await requireProductAccess(req));
    const factory = await repo.getFactory(factoryId, pid);
    if (!factory) throw new NotFoundError('Factory not found');
    const issued = await enrollment.rotateToken({
      productId: pid,
      factoryId,
      capabilities: parsed.data.capabilities,
    });
    return { tokenId: issued.tokenId, token: issued.token, scope: issued.scope };
  });

  // ── Revoke a factory's token(s) — immediate rejection ──
  app.post('/fleet/factories/:id/token/revoke', async req => {
    await extractAuth(req);
    const { id: factoryId } = req.params as { id: string };
    const parsed = RevokeTokenSchema.safeParse(req.body ?? {});
    if (!parsed.success) badRequest(parsed.error.issues);
    const pid = parsed.data.productId || (await requireProductAccess(req));
    const revoked = await enrollment.revokeToken({
      productId: pid,
      factoryId,
      tokenId: parsed.data.tokenId,
    });
    return { revoked };
  });

  // ── Phase 3 Budgets: GET/PUT per-product budget ──
  app.get('/fleet/budgets/:productId', async req => {
    await extractAuth(req);
    const productId = await requireOwnProduct(req);
    const budget = await coordinator.getBudget(productId);
    if (!budget) throw new NotFoundError('No budget configured for this product');
    return budget;
  });

  // ── Cost burndown — spend-over-time vs ceiling (§14) ──
  app.get('/fleet/budgets/:productId/burndown', async req => {
    await extractAuth(req);
    const productId = await requireOwnProduct(req);
    const { days } = req.query as { days?: string };
    const parsedDays = days ? Number.parseInt(days, 10) : undefined;
    return coordinator.costBurndown(
      productId,
      Number.isFinite(parsedDays) ? parsedDays : undefined
    );
  });

  app.put('/fleet/budgets/:productId', async (req, reply) => {
    await extractAuth(req);
    const productId = await requireOwnProduct(req);
    const parsed = UpsertBudgetSchema.safeParse(req.body);
    if (!parsed.success) badRequest(parsed.error.issues);
    const budget = await coordinator.upsertBudget(
      productId,
      parsed.data.ceilingUsd,
      parsed.data.window,
      parsed.data.engineCeilingsUsd
    );
    reply.code(200);
    return budget;
  });

  app.post('/fleet/budgets/:productId/pause', async req => {
    await extractAuth(req);
    const productId = await requireOwnProduct(req);
    const budget = await coordinator.pauseBudget(productId);
    if (!budget) throw new NotFoundError('No budget configured for this product');
    return budget;
  });

  app.post('/fleet/budgets/:productId/resume', async req => {
    await extractAuth(req);
    const productId = await requireOwnProduct(req);
    const budget = await coordinator.resumeBudget(productId);
    if (!budget) throw new NotFoundError('No budget configured for this product');
    return budget;
  });

  // ── Phase 3 DAG: submit children for an existing parent ──
  app.post('/fleet/jobs/:id/children', async (req, reply) => {
    await extractAuth(req);
    const { id: parentId } = req.params as { id: string };
    const pid = await requireProductAccess(req);
    const parsed = SubmitChildrenSchema.safeParse(req.body);
    if (!parsed.success) badRequest(parsed.error.issues);
    const parent = await repo.getJob(parentId, pid);
    if (!parent) throw new NotFoundError('Parent job not found');
    const result = await coordinator.submitChildren(parentId, pid, parsed.data.children);
    reply.code(201);
    return { parent: result.parent, children: result.childJobs };
  });

  // ── Phase 3 DAG: get the subtree rooted at a job ──
  app.get('/fleet/jobs/:id/dag', async req => {
    await extractAuth(req);
    const { id } = req.params as { id: string };
    const pid = await requireProductAccess(req);
    const dag = await coordinator.getDagSubtree(id, pid);
    if (!dag) throw new NotFoundError('Job not found');
    return { dag };
  });

  // ── Tracker bridge (§10): ingest an Item as a job (idempotent, scheduled by §7) ──
  app.post('/fleet/tracker/ingest', async (req, reply) => {
    await extractAuth(req);
    const parsed = IngestItemSchema.safeParse(req.body);
    if (!parsed.success) badRequest(parsed.error.issues);
    const pid = await requireProductAccess(req);
    const result = await trackerBridge.ingestItemAsJob(pid, parsed.data.itemId, {
      priority: parsed.data.priority,
    });
    reply.code(result.outcome === 'created' ? 201 : 200);
    return { outcome: result.outcome, job: result.job };
  });

  // ── Tracker bridge (§10): manually echo a job's current outcome to its Item ──
  app.post('/fleet/tracker/echo', async req => {
    await extractAuth(req);
    const parsed = EchoJobSchema.safeParse(req.body);
    if (!parsed.success) badRequest(parsed.error.issues);
    const pid = await requireProductAccess(req);
    return trackerBridge.echoJobToItem(pid, parsed.data.jobId, req.log);
  });
}
