/**
 * Fleet module — types for the agent-gigafactory coordinator (Phase 2 foundation).
 *
 * Durable data model for distributed agent jobs: jobs, runs, leases, factories,
 * profiles, events, and artifacts. Product-agnostic — every document carries a
 * `productId`. Zod schemas are the source of truth; doc/input types are inferred
 * from them (no `any`).
 *
 * Field names + lifecycle mirror the agent-queue gigafactory spec
 * (../learning_ai_devops_tools/agent-queue/docs/GIGAFACTORY/GIGAFACTORY_ROADMAP.md §4/§7/§8/§13).
 */

import { z } from 'zod';

// ── Enums ───────────────────────────────────────────────────────────────────

/** Canonical job lifecycle (§11). `draft` = saved but not yet submitted (not
 * claimable, editable); `blocked` = unmet deps; `dead_letter` = retries exhausted. */
export const FLEET_STAGES = [
  'draft',
  'queued',
  'blocked',
  'assigned',
  'building',
  'review',
  'testing',
  'shipped',
  'failed',
  'dead_letter',
] as const;
export type FleetStage = (typeof FLEET_STAGES)[number];

/** Stages from which a worker may still progress / that count as "in flight". */
export const ACTIVE_STAGES: readonly FleetStage[] = ['assigned', 'building', 'review', 'testing'];
/** Stages that satisfy a hard dependency / a soft dependency. */
export const DEP_DONE_HARD: readonly FleetStage[] = ['shipped'];
export const DEP_DONE_SOFT: readonly FleetStage[] = ['shipped', 'testing'];

export const FLEET_PRIORITIES = ['critical', 'high', 'medium', 'low'] as const;
export type FleetPriority = (typeof FLEET_PRIORITIES)[number];
export const PRIORITY_ORDER: Record<FleetPriority, number> = {
  critical: 0,
  high: 1,
  medium: 2,
  low: 3,
};

export const FLEET_ENGINE_CLASSES = ['agentic-coder', 'chat-coder', 'review-only'] as const;
export type FleetEngineClass = (typeof FLEET_ENGINE_CLASSES)[number];

/** Concrete coding-agent engines a factory can run. An explicit `engine` on a job
 * wins over `engineClass`/`prefersEngine` (the runner's `resolve_engine`). */
export const FLEET_ENGINES = ['devin', 'claude', 'codex', 'copilot'] as const;
export type FleetEngine = (typeof FLEET_ENGINES)[number];

export const DEPS_MODES = ['hard', 'soft'] as const;
export type DepsMode = (typeof DEPS_MODES)[number];

/** Failure classes a `retry.on` may name (mirrors agent-queue). */
export const RETRY_CLASSES = ['timeout', 'verify_failed', 'crash', 'agent_error'] as const;
export type RetryClass = (typeof RETRY_CLASSES)[number];

/** Terminal run results recorded on a FleetRunDoc. */
export const RUN_RESULTS = [
  'review',
  'testing',
  'shipped',
  'failed',
  'timeout',
  'verify_failed',
  'capability_mismatch',
  'no_engine',
  'retries_exhausted',
] as const;
export type RunResult = (typeof RUN_RESULTS)[number];

export const FACTORY_HEALTH = ['ok', 'degraded', 'down'] as const;
export type FactoryHealth = (typeof FACTORY_HEALTH)[number];

export const LEASE_STATUS = ['held', 'expired', 'released'] as const;
export type LeaseStatus = (typeof LEASE_STATUS)[number];

export const JOB_KINDS = ['leaf', 'composite'] as const;
export type JobKind = (typeof JOB_KINDS)[number];

/** Artifact categories (§13/§26). Large outputs live in blob; Cosmos holds pointers only. */
export const FLEET_ARTIFACT_KINDS = ['log', 'coverage', 'screenshot', 'build', 'other'] as const;
export type FleetArtifactKind = (typeof FLEET_ARTIFACT_KINDS)[number];

// ── Shared value objects ─────────────────────────────────────────────────────

export const CheckpointSchema = z.object({
  wipBranch: z.string(),
  wipBase: z.string().optional(),
  wipCommit: z.string().optional(),
});

export const BudgetSchema = z.object({
  usd: z.number().nonnegative().optional(),
  tokens: z.number().nonnegative().optional(),
  wall: z.number().nonnegative().optional(),
});

export const RetryPolicySchema = z.object({
  max: z.number().int().nonnegative(),
  backoff: z.string().optional(),
  on: z.array(z.enum(RETRY_CLASSES)).default([]),
});

export const ManifestSnapshotSchema = z.object({
  priority: z.enum(FLEET_PRIORITIES).default('medium'),
  capabilities: z.array(z.string()).default([]),
  engineClass: z.enum(FLEET_ENGINE_CLASSES).optional(),
  profile: z.string().optional(),
  prefersEngine: z.array(z.string()).default([]),
  allowedScope: z.array(z.string()).default([]),
  deps: z.array(z.string()).default([]),
  depsMode: z.enum(DEPS_MODES).optional(),
  budget: BudgetSchema.optional(),
  retry: RetryPolicySchema.optional(),
});

export const InsightsSchema = z.object({
  model: z.string().optional(),
  /** Concrete engine the factory actually ran (e.g. `devin`/`claude`/`codex`) —
   * resolved at run time, unlike the job's abstract `engineClass`. */
  engine: z.string().optional(),
  /** Agent session handle for traceability/recovery (e.g. a Devin session id). */
  sessionId: z.string().optional(),
  /** Web URL for the agent session, when the engine exposes one. */
  sessionUrl: z.string().optional(),
  tokensIn: z.number().optional(),
  tokensOut: z.number().optional(),
  tokensCached: z.number().optional(),
  costUsd: z.number().optional(),
  estimated: z.boolean().optional(),
  turns: z.number().optional(),
  toolCalls: z.number().optional(),
  filesChanged: z.number().optional(),
  linesAdded: z.number().optional(),
  linesDeleted: z.number().optional(),
});

// ── Container documents ───────────────────────────────────────────────────────

/**
 * FleetJobDoc — the durable job (pk `/productId`).
 * `rev` is the optimistic-concurrency token used by the coordinator's atomic
 * claim + fenced transitions (maps to Cosmos `_etag` / If-Match in production;
 * see repository.revUpdate).
 */
/**
 * Multi-reviewer human gate (§14 Phase 3). A job in the `review` stage carries a
 * policy (how many distinct approvals are required + an optional reviewer
 * allowlist) and an append-style decision set (one current decision per reviewer).
 */
export const ReviewPolicySchema = z.object({
  requiredApprovals: z.number().int().min(1).default(1),
  reviewers: z.array(z.string()).default([]),
});
export type ReviewPolicy = z.infer<typeof ReviewPolicySchema>;

export const REVIEW_DECISIONS = ['approve', 'reject'] as const;
export type ReviewDecisionKind = (typeof REVIEW_DECISIONS)[number];

export const ReviewDecisionSchema = z.object({
  reviewer: z.string().min(1),
  decision: z.enum(REVIEW_DECISIONS),
  at: z.string(),
  note: z.string().optional(),
});
export type ReviewDecision = z.infer<typeof ReviewDecisionSchema>;

export const FleetJobDocSchema = z.object({
  id: z.string(),
  productId: z.string().min(1),
  stage: z.enum(FLEET_STAGES),
  idempotencyKey: z.string().min(1),
  contentHash: z.string(),
  bodyMd: z.string(),
  manifestSnapshot: ManifestSnapshotSchema,
  priority: z.enum(FLEET_PRIORITIES),
  priorityOrder: z.number().int(),
  capabilities: z.array(z.string()).default([]),
  engine: z.enum(FLEET_ENGINES).optional(),
  engineClass: z.enum(FLEET_ENGINE_CLASSES).optional(),
  profile: z.string().optional(),
  deps: z.array(z.string()).default([]),
  depsMode: z.enum(DEPS_MODES).optional(),
  budget: BudgetSchema.optional(),
  retry: RetryPolicySchema.optional(),
  kind: z.enum(JOB_KINDS).default('leaf'),
  parentId: z.string().optional(),
  trackerItemId: z.string().optional(),
  /**
   * Optional PR target: the repo (e.g. `owner/name` or a clone URL) and base
   * branch the factory should open a pull request against (§PR mode). When set
   * and the factory runs with PR mode on, its work is committed to a job branch,
   * pushed, and a PR is opened; the PR URL is recorded on the run.
   */
  repo: z.string().optional(),
  baseBranch: z.string().optional(),
  verify: z.string().optional(),
  autoMerge: z.boolean().optional(),
  checkpoint: CheckpointSchema.optional(),
  attempts: z.number().int().nonnegative().default(0),
  /**
   * Attempt count at which the CURRENT retry budget started (operator `redrive`
   * sets this to `attempts` to grant a fresh budget). The retry policy compares
   * `attempts - attemptsBase` against `retry.max`, so `attempts` stays monotonic
   * (run ids are keyed by it) while the budget can be reset. Default 0.
   */
  attemptsBase: z.number().int().nonnegative().default(0),
  leaseEpoch: z.number().int().nonnegative().default(0),
  rev: z.number().int().nonnegative().default(0),
  blockedReason: z.string().optional(),
  /**
   * Earliest time this job may be claimed again (ISO). Set by the retry policy
   * when a failed attempt is auto-requeued with a backoff; the scheduler skips a
   * queued job until `now >= retryNotBefore`. Absent ⇒ immediately claimable.
   */
  retryNotBefore: z.string().optional(),
  /**
   * Multi-reviewer human gate state (§14 Phase 3). Both are present only while a
   * job is (or has been) in `review`; `requestReview` (re)sets them.
   */
  reviewPolicy: ReviewPolicySchema.optional(),
  reviewDecisions: z.array(ReviewDecisionSchema).optional(),
  /**
   * Last Item status echoed to the linked tracker Item (§10 round-trip). Used to
   * make re-echo of an unchanged outcome a no-op. Optional + provider-managed —
   * absent on jobs that were never echoed / have no trackerItemId.
   */
  trackerEchoedStatus: z.string().optional(),
  /**
   * Trace-context correlation id (§4) minted at submission from the inbound
   * `x-correlation-id`/`traceparent`/`x-request-id` (or freshly generated). The
   * run/lease inherit it and every lifecycle event is stamped with it, so a
   * single logical work-unit can be stitched across the coordinator↔runner
   * boundary (job → claim → run → ship). Optional — absent on legacy jobs.
   */
  correlationId: z.string().optional(),
  createdAt: z.string(),
  updatedAt: z.string(),
});
export type FleetJobDoc = z.infer<typeof FleetJobDocSchema>;

/** FleetRunDoc — one execution attempt of a job (pk `/jobId`). */
export const FleetRunDocSchema = z.object({
  id: z.string(),
  productId: z.string().min(1),
  jobId: z.string().min(1),
  attempt: z.number().int().positive(),
  factoryId: z.string().optional(),
  engine: z.string(),
  profileSnapshot: z.record(z.string(), z.unknown()).optional(),
  startedAt: z.string(),
  endedAt: z.string().optional(),
  exit: z.number().int().optional(),
  verifyResult: z.enum(['pass', 'fail']).optional(),
  result: z.enum(RUN_RESULTS).optional(),
  insights: InsightsSchema.default({}),
  /** PR deliverable produced by this run (§PR mode). */
  prUrl: z.string().optional(),
  branch: z.string().optional(),
  prState: z.enum(['open', 'merged']).optional(),
  /** Trace-context correlation id inherited from the job at claim time (§4). */
  correlationId: z.string().optional(),
});
export type FleetRunDoc = z.infer<typeof FleetRunDocSchema>;

/** FleetLeaseDoc — the single-holder lease for a job (pk `/jobId`). */
export const FleetLeaseDocSchema = z.object({
  id: z.string(),
  productId: z.string().min(1),
  jobId: z.string().min(1),
  holderFactoryId: z.string().optional(),
  expiresAt: z.string().optional(),
  leaseEpoch: z.number().int().nonnegative().default(0),
  renewals: z.number().int().nonnegative().default(0),
  status: z.enum(LEASE_STATUS).default('held'),
  rev: z.number().int().nonnegative().default(0),
  /** Trace-context correlation id inherited from the job at claim time (§4). */
  correlationId: z.string().optional(),
  updatedAt: z.string(),
});
export type FleetLeaseDoc = z.infer<typeof FleetLeaseDocSchema>;

/** FleetFactoryDoc — a registered worker host (pk `/productId`). */
export const FleetFactoryDocSchema = z.object({
  id: z.string(),
  productId: z.string().min(1),
  factoryId: z.string().min(1),
  descriptor: z.record(z.string(), z.unknown()).default({}),
  capabilities: z.array(z.string()).default([]),
  health: z.enum(FACTORY_HEALTH).default('ok'),
  load: z.number().nonnegative().default(0),
  seatLimit: z.number().int().positive().default(1),
  lastHeartbeatAt: z.string(),
});
export type FleetFactoryDoc = z.infer<typeof FleetFactoryDocSchema>;

export const FACTORY_TOKEN_STATUSES = ['active', 'rotating', 'revoked'] as const;
export type FactoryTokenStatus = (typeof FACTORY_TOKEN_STATUSES)[number];

/**
 * FleetFactoryTokenDoc — a SCOPED, rotatable credential for a factory (pk `/productId`, §12).
 *
 * Stored HASHED at rest (`tokenHash` = sha256 of the plaintext); the plaintext is
 * returned exactly once at enroll/rotate and is NEVER persisted. `scope` is
 * {productId (the partition), factoryId, capabilities[]}. `status` drives the trust
 * decision: `active` accepted; `rotating` accepted only until `rotatingUntil` (grace
 * overlap so an in-flight worker isn't cut off); `revoked` always rejected.
 */
export const FleetFactoryTokenDocSchema = z.object({
  id: z.string(),
  productId: z.string().min(1),
  factoryId: z.string().min(1),
  tokenHash: z.string().min(1),
  capabilities: z.array(z.string()).default([]),
  label: z.string().optional(),
  status: z.enum(FACTORY_TOKEN_STATUSES).default('active'),
  createdAt: z.string(),
  lastUsedAt: z.string().optional(),
  rotatingUntil: z.string().optional(),
  /** Absolute expiry (ISO). A token past `expiresAt` is rejected by verifyToken,
   *  bounding the blast radius of a leaked credential. Absent ⇒ never expires
   *  (FLEET_TOKEN_TTL_DAYS=0 at issue time). */
  expiresAt: z.string().optional(),
});
export type FleetFactoryTokenDoc = z.infer<typeof FleetFactoryTokenDocSchema>;

/** FleetProfileDoc — an immutable, versioned profile snapshot (pk `/productId`). */
export const FleetProfileDocSchema = z.object({
  id: z.string(),
  productId: z.string().min(1),
  name: z.string().min(1),
  version: z.number().int().positive(),
  snapshot: z.record(z.string(), z.unknown()),
  createdAt: z.string(),
});
export type FleetProfileDoc = z.infer<typeof FleetProfileDocSchema>;

/** FleetEventDoc — append-only audit/event stream entry (pk `/jobId`). */
export const FleetEventDocSchema = z.object({
  id: z.string(),
  productId: z.string().min(1),
  jobId: z.string().min(1),
  seq: z.number().int().nonnegative(),
  type: z.string().min(1),
  at: z.string(),
  actor: z.string().optional(),
  /** Trace-context correlation id of the owning job (§4) — stamped on every
   *  lifecycle event so the stream is correlatable without joining to the job. */
  correlationId: z.string().optional(),
  data: z.record(z.string(), z.unknown()).default({}),
});
export type FleetEventDoc = z.infer<typeof FleetEventDocSchema>;

/**
 * FleetArtifactDoc — a POINTER to a blob-stored run output (pk `/jobId`).
 *
 * Large outputs (logs, coverage, screenshots, build output) are written to blob
 * storage; only this pointer (blob key + size/content-type/sha256 metadata) lives
 * in Cosmos — NEVER the bytes themselves (doc-size + RU limits, §13). The
 * short-lived SAS read URL is minted on demand from `blobKey` and is intentionally
 * NOT persisted on the doc.
 */
export const FleetArtifactDocSchema = z.object({
  id: z.string(),
  productId: z.string().min(1),
  jobId: z.string().min(1),
  runId: z.string().optional(),
  kind: z.enum(FLEET_ARTIFACT_KINDS),
  blobKey: z.string().min(1),
  contentType: z.string().min(1),
  sizeBytes: z.number().int().nonnegative(),
  sha256: z.string().optional(),
  createdAt: z.string(),
});
export type FleetArtifactDoc = z.infer<typeof FleetArtifactDocSchema>;

// ── API request schemas (routes) ──────────────────────────────────────────────

export const SubmitJobSchema = z.object({
  productId: z.string().min(1).optional(),
  idempotencyKey: z.string().min(1),
  bodyMd: z.string().min(1),
  priority: z.enum(FLEET_PRIORITIES).default('medium'),
  capabilities: z.array(z.string()).default([]),
  engineClass: z.enum(FLEET_ENGINE_CLASSES).optional(),
  profile: z.string().optional(),
  prefersEngine: z.array(z.string()).default([]),
  allowedScope: z.array(z.string()).default([]),
  deps: z.array(z.string()).default([]),
  depsMode: z.enum(DEPS_MODES).optional(),
  budget: BudgetSchema.optional(),
  retry: RetryPolicySchema.optional(),
  kind: z.enum(JOB_KINDS).default('leaf'),
  parentId: z.string().optional(),
  trackerItemId: z.string().optional(),
  /** Optional PR target (§PR mode): repo (`owner/name` or clone URL) + base branch. */
  repo: z.string().optional(),
  baseBranch: z.string().optional(),
  /** PR mode options: verify command to run in the checkout before opening the PR,
   * and whether to auto-merge the PR once opened. */
  verify: z.string().optional(),
  autoMerge: z.boolean().optional(),
  /** Concrete engine to run (devin/claude/codex/copilot); overrides engineClass. */
  engine: z.enum(FLEET_ENGINES).optional(),
  /** Save as a non-claimable, editable draft (stage `draft`) instead of `queued`. */
  draft: z.boolean().optional(),
  /** Phase 3 DAG: inline children to create atomically with the parent. */
  children: z
    .array(
      z.object({
        idempotencyKey: z.string().min(1),
        bodyMd: z.string().min(1),
        priority: z.enum(FLEET_PRIORITIES).default('medium'),
        capabilities: z.array(z.string()).default([]),
        engineClass: z.enum(FLEET_ENGINE_CLASSES).optional(),
        profile: z.string().optional(),
        prefersEngine: z.array(z.string()).default([]),
        allowedScope: z.array(z.string()).default([]),
        deps: z.array(z.string()).default([]),
        depsMode: z.enum(DEPS_MODES).optional(),
        budget: BudgetSchema.optional(),
        retry: RetryPolicySchema.optional(),
      })
    )
    .default([]),
});
export type SubmitJobInput = z.infer<typeof SubmitJobSchema>;

/** Edit a job's prompt/config while it is still `draft`/`queued`/`blocked`
 * (i.e. not yet picked up). All fields optional — only provided ones change. */
export const UpdateDraftSchema = z.object({
  bodyMd: z.string().min(1).optional(),
  priority: z.enum(FLEET_PRIORITIES).optional(),
  capabilities: z.array(z.string()).optional(),
  engine: z.enum(FLEET_ENGINES).optional(),
  repo: z.string().optional(),
  baseBranch: z.string().optional(),
  verify: z.string().optional(),
  autoMerge: z.boolean().optional(),
});
export type UpdateDraftInput = z.infer<typeof UpdateDraftSchema>;

export const ListJobsQuerySchema = z.object({
  productId: z.string().optional(),
  stage: z.enum(FLEET_STAGES).optional(),
  idempotencyKey: z.string().optional(),
  limit: z.coerce.number().int().min(1).max(200).default(50),
  offset: z.coerce.number().int().min(0).default(0),
});
export type ListJobsQuery = z.infer<typeof ListJobsQuerySchema>;

/** Fenced state transition — worker MUST carry its leaseEpoch. */
export const PatchJobSchema = z.object({
  leaseEpoch: z.number().int().nonnegative(),
  stage: z.enum(FLEET_STAGES).optional(),
  checkpoint: CheckpointSchema.optional(),
  blockedReason: z.string().optional(),
});
export type PatchJobInput = z.infer<typeof PatchJobSchema>;

export const RequestReviewSchema = z.object({
  requiredApprovals: z.number().int().min(1).optional(),
  reviewers: z.array(z.string()).optional(),
});
export type RequestReviewInput = z.infer<typeof RequestReviewSchema>;

export const SubmitReviewSchema = z.object({
  reviewer: z.string().min(1),
  decision: z.enum(REVIEW_DECISIONS),
  note: z.string().optional(),
});
export type SubmitReviewInput = z.infer<typeof SubmitReviewSchema>;

export const ClaimSchema = z.object({
  productId: z.string().min(1).optional(),
  factoryId: z.string().min(1),
  capabilities: z.array(z.string()).default([]),
  leaseSeconds: z.number().int().positive().max(86400).default(900),
});
export type ClaimInput = z.infer<typeof ClaimSchema>;

export const RenewLeaseSchema = z.object({
  leaseEpoch: z.number().int().nonnegative(),
  leaseSeconds: z.number().int().positive().max(86400).default(900),
});
export type RenewLeaseInput = z.infer<typeof RenewLeaseSchema>;

export const ReleaseLeaseSchema = z.object({
  leaseEpoch: z.number().int().nonnegative(),
  stage: z.enum(FLEET_STAGES).optional(),
  // A factory reports cost/token/effort metrics + the run outcome when it
  // releases the lease at the end of a work unit. Recorded on the current run.
  insights: InsightsSchema.optional(),
  result: z.enum(RUN_RESULTS).optional(),
  // PR deliverable (§PR mode): the pull request the run opened + its branch + state.
  prUrl: z.string().optional(),
  branch: z.string().optional(),
  prState: z.enum(['open', 'merged']).optional(),
});
export type ReleaseLeaseInput = z.infer<typeof ReleaseLeaseSchema>;

export const HeartbeatSchema = z.object({
  productId: z.string().min(1).optional(),
  factoryId: z.string().min(1),
  descriptor: z.record(z.string(), z.unknown()).optional(),
  capabilities: z.array(z.string()).optional(),
  health: z.enum(FACTORY_HEALTH).optional(),
  load: z.number().nonnegative().optional(),
  seatLimit: z.number().int().positive().optional(),
});
export type HeartbeatInput = z.infer<typeof HeartbeatSchema>;

/**
 * Upload an artifact for a job. The bytes are carried base64-encoded in the JSON
 * body (large content is offloaded to blob server-side; nothing is stored inline
 * in Cosmos). `productId` may override the request-resolved product.
 */
export const UploadArtifactSchema = z.object({
  productId: z.string().min(1).optional(),
  runId: z.string().min(1).optional(),
  kind: z.enum(FLEET_ARTIFACT_KINDS),
  contentType: z.string().min(1).default('application/octet-stream'),
  contentBase64: z.string().min(1),
});
export type UploadArtifactInput = z.infer<typeof UploadArtifactSchema>;

/** Enroll a factory + issue its first scoped token (§12). */
export const EnrollFactorySchema = z.object({
  productId: z.string().min(1).optional(),
  factoryId: z.string().min(1).optional(),
  capabilities: z.array(z.string()).default([]),
  label: z.string().optional(),
});
export type EnrollFactoryInput = z.infer<typeof EnrollFactorySchema>;

/** Rotate a factory's token. Capabilities default to the current scope when omitted. */
export const RotateTokenSchema = z.object({
  productId: z.string().min(1).optional(),
  capabilities: z.array(z.string()).optional(),
});
export type RotateTokenInput = z.infer<typeof RotateTokenSchema>;

/** Revoke a factory's token(s); an explicit `tokenId` narrows to one credential. */
export const RevokeTokenSchema = z.object({
  productId: z.string().min(1).optional(),
  tokenId: z.string().min(1).optional(),
});
export type RevokeTokenInput = z.infer<typeof RevokeTokenSchema>;

/** Tracker bridge (§10): ingest a tracker Item as a fleet job. */
export const IngestItemSchema = z.object({
  itemId: z.string().min(1),
  priority: z.enum(FLEET_PRIORITIES).optional(),
});
export type IngestItemInput = z.infer<typeof IngestItemSchema>;

/** Tracker bridge (§10): manually echo a job's current outcome to its Item. */
export const EchoJobSchema = z.object({
  jobId: z.string().min(1),
});
export type EchoJobInput = z.infer<typeof EchoJobSchema>;

// ── Phase 3: Per-product budgets (§11/§13) ──────────────────────────────────

export const BUDGET_STATUSES = ['active', 'paused'] as const;
export type BudgetStatus = (typeof BUDGET_STATUSES)[number];

export const BUDGET_WINDOWS = ['daily', 'weekly', 'monthly'] as const;
export type BudgetWindow = (typeof BUDGET_WINDOWS)[number];

/** FleetBudgetDoc — per-product cost ceiling (pk `/productId`). */
export const FleetBudgetDocSchema = z.object({
  id: z.string(),
  productId: z.string().min(1),
  ceilingUsd: z.number().nonnegative(),
  window: z.enum(BUDGET_WINDOWS).default('monthly'),
  spentUsd: z.number().nonnegative().default(0),
  status: z.enum(BUDGET_STATUSES).default('active'),
  windowStart: z.string().optional(),
  /** Run identifiers already accrued — makes accrueSpend idempotent per run. */
  accruedRunIds: z.array(z.string()).default([]),
  /** Optional per-engine sub-ceilings (USD). An engine that reaches its ceiling is
   *  routed around at claim time (only that engine is blocked, not the product). */
  engineCeilingsUsd: z.record(z.string(), z.number().nonnegative()).optional(),
  /** Per-engine accrued spend (USD), tracked alongside `spentUsd` for the ceilings
   *  above + the dashboard breakdown. Idempotent via the same `accruedRunIds`. */
  spentByEngineUsd: z.record(z.string(), z.number().nonnegative()).optional(),
  updatedAt: z.string(),
});
export type FleetBudgetDoc = z.infer<typeof FleetBudgetDocSchema>;

/**
 * FleetQueueStateDoc — a per-product monotonic "work changed" counter (pk `/productId`).
 * The M0 RU gate (docs/GIGAFACTORY/FLEET_DISPATCH_REDESIGN.md §8/§12): a factory
 * point-reads `version` (~1 RU) and only runs the expensive claim when it has
 * changed since its last attempt. Bumped on job create + every stage change. The
 * bump is best-effort and not exact under concurrency (the gate is an
 * optimization, not a correctness boundary) — a factory's periodic safety claim
 * backstops any missed bump.
 */
export const FleetQueueStateDocSchema = z.object({
  id: z.string(),
  productId: z.string().min(1),
  version: z.number().int().nonnegative().default(0),
  updatedAt: z.string(),
});
export type FleetQueueStateDoc = z.infer<typeof FleetQueueStateDocSchema>;

/** Upsert a product's budget config. */
export const UpsertBudgetSchema = z.object({
  ceilingUsd: z.number().nonnegative(),
  window: z.enum(BUDGET_WINDOWS).default('monthly'),
  /** Optional per-engine sub-ceilings (USD), e.g. `{ "codex": 50 }`. */
  engineCeilingsUsd: z.record(z.string(), z.number().nonnegative()).optional(),
});
export type UpsertBudgetInput = z.infer<typeof UpsertBudgetSchema>;

/** Phase 3 DAG: submit children for an existing parent job. */
export const SubmitChildrenSchema = z.object({
  children: z.array(
    z.object({
      idempotencyKey: z.string().min(1),
      bodyMd: z.string().min(1),
      priority: z.enum(FLEET_PRIORITIES).default('medium'),
      capabilities: z.array(z.string()).default([]),
      engineClass: z.enum(FLEET_ENGINE_CLASSES).optional(),
      profile: z.string().optional(),
      prefersEngine: z.array(z.string()).default([]),
      allowedScope: z.array(z.string()).default([]),
      deps: z.array(z.string()).default([]),
      depsMode: z.enum(DEPS_MODES).optional(),
      budget: BudgetSchema.optional(),
      retry: RetryPolicySchema.optional(),
    })
  ),
});
export type SubmitChildrenInput = z.infer<typeof SubmitChildrenSchema>;
