/**
 * Fleet repositories — one per fleet_* container, cloud-agnostic via @bytelyst/datastore.
 *
 * Partition keys (see lib/cosmos-init.ts):
 *   fleet_jobs       /productId      fleet_runs      /jobId
 *   fleet_leases     /jobId          fleet_factories /productId
 *   fleet_profiles   /productId      fleet_events    /jobId
 *   fleet_artifacts  /jobId
 *
 * Optimistic concurrency: jobs and leases carry a monotonic `rev` token.
 * `revUpdate*` is a compare-and-swap — it writes only when the stored `rev`
 * still equals the caller's expected `rev`, otherwise it reports a conflict
 * WITHOUT writing. In production (Cosmos) this maps to an `_etag` / If-Match
 * conditional replace; on the memory provider it is enforced by re-reading the
 * current `rev` immediately before the write, which is exact for the sequential
 * calls the coordinator + tests make (see coordinator.claimNextJob).
 */

import type { DocumentCollection } from '@bytelyst/datastore';
import { getCollection } from '../../lib/datastore.js';
import type {
  FleetArtifactDoc,
  FleetBudgetDoc,
  FleetEventDoc,
  FleetFactoryDoc,
  FleetFactoryTokenDoc,
  FleetJobDoc,
  FleetLeaseDoc,
  FleetProfileDoc,
  FleetQueueStateDoc,
  FleetRunDoc,
  FleetStage,
} from './types.js';

// ── Collections ───────────────────────────────────────────────────────────────

function jobs(): DocumentCollection<FleetJobDoc> {
  return getCollection<FleetJobDoc>('fleet_jobs', '/productId');
}
function runs(): DocumentCollection<FleetRunDoc> {
  return getCollection<FleetRunDoc>('fleet_runs', '/jobId');
}
function leases(): DocumentCollection<FleetLeaseDoc> {
  return getCollection<FleetLeaseDoc>('fleet_leases', '/jobId');
}
function factories(): DocumentCollection<FleetFactoryDoc> {
  return getCollection<FleetFactoryDoc>('fleet_factories', '/productId');
}
function profiles(): DocumentCollection<FleetProfileDoc> {
  return getCollection<FleetProfileDoc>('fleet_profiles', '/productId');
}
function events(): DocumentCollection<FleetEventDoc> {
  return getCollection<FleetEventDoc>('fleet_events', '/jobId');
}
function artifacts(): DocumentCollection<FleetArtifactDoc> {
  return getCollection<FleetArtifactDoc>('fleet_artifacts', '/jobId');
}
function factoryTokens(): DocumentCollection<FleetFactoryTokenDoc> {
  return getCollection<FleetFactoryTokenDoc>('fleet_factory_tokens', '/productId');
}
function budgets(): DocumentCollection<FleetBudgetDoc> {
  return getCollection<FleetBudgetDoc>('fleet_budgets', '/productId');
}
function queueState(): DocumentCollection<FleetQueueStateDoc> {
  return getCollection<FleetQueueStateDoc>('fleet_queue_state', '/productId');
}

/** Result of a compare-and-swap update. */
export type RevResult<T> = { ok: true; doc: T } | { ok: false; reason: 'not_found' | 'conflict' };

// ── Queue state (M0 RU gate — per-product monotonic "work changed" counter) ─────

/** Current queue version for a product (0 if never bumped). A ~1 RU point read. */
export async function getQueueVersion(productId: string): Promise<number> {
  const doc = await queueState().findById(productId, productId);
  return doc?.version ?? 0;
}

/**
 * Best-effort monotonic bump of a product's queue version. Called on job create +
 * every stage change so a polling factory can detect "something changed" with a
 * cheap point read instead of a full `listJobs` scan. NEVER throws — the gate is
 * an optimization, so a bump failure must not fail the underlying job write.
 */
export async function bumpQueueVersion(productId: string): Promise<void> {
  try {
    const cur = await queueState().findById(productId, productId);
    await queueState().upsert({
      id: productId,
      productId,
      version: (cur?.version ?? 0) + 1,
      updatedAt: new Date().toISOString(),
    });
  } catch {
    /* best-effort: the gate is an optimization, never fail the job write */
  }
}

// ── Jobs ──────────────────────────────────────────────────────────────────────

export async function createJob(doc: FleetJobDoc): Promise<FleetJobDoc> {
  const created = await jobs().create(doc);
  await bumpQueueVersion(doc.productId); // new queued work appeared
  return created;
}

export async function getJob(id: string, productId: string): Promise<FleetJobDoc | null> {
  return jobs().findById(id, productId);
}

export interface ListJobsFilter {
  productId: string;
  stage?: FleetStage;
  idempotencyKey?: string;
  limit?: number;
  offset?: number;
}

export async function listJobs(f: ListJobsFilter): Promise<FleetJobDoc[]> {
  const filter: Record<string, string> = { productId: f.productId };
  if (f.stage) filter.stage = f.stage;
  if (f.idempotencyKey) filter.idempotencyKey = f.idempotencyKey;
  return jobs().findMany({
    filter,
    sort: { priorityOrder: 1, createdAt: 1 },
    offset: f.offset,
    limit: f.limit,
  });
}

/** All jobs for a product sharing an idempotency-key (dedupe lookups). */
export async function findJobsByIdempotencyKey(
  productId: string,
  idempotencyKey: string
): Promise<FleetJobDoc[]> {
  return jobs().findMany({ filter: { productId, idempotencyKey } });
}

/** All child jobs of a given parent (Phase 3 DAG). */
export async function listChildrenByParent(
  productId: string,
  parentId: string
): Promise<FleetJobDoc[]> {
  return jobs().findMany({ filter: { productId, parentId }, sort: { createdAt: 1 } });
}

/** Unconditional merge update (use only when concurrency is not contended). */
export async function updateJob(
  id: string,
  productId: string,
  updates: Partial<FleetJobDoc>
): Promise<FleetJobDoc | null> {
  const cur = await jobs().findById(id, productId);
  if (!cur) return null;
  const updated = await jobs().update(id, productId, {
    ...updates,
    rev: (cur.rev ?? 0) + 1,
    updatedAt: new Date().toISOString(),
  });
  if (updates.stage !== undefined) await bumpQueueVersion(productId); // claimable set changed
  return updated;
}

/**
 * Compare-and-swap on `rev` — the atomic-claim / fenced-transition primitive.
 * Delegates to the datastore's `updateIfMatch`, which performs the compare and the
 * write as one indivisible operation (Cosmos If-Match; a synchronous compare-set on
 * the memory provider). This is genuinely atomic under TRUE concurrency — there is
 * no read → check → write with an intervening await that two callers could interleave.
 */
export async function revUpdateJob(
  id: string,
  productId: string,
  expectedRev: number,
  updates: Partial<FleetJobDoc>
): Promise<RevResult<FleetJobDoc>> {
  const res = await jobs().updateIfMatch(
    id,
    productId,
    { rev: expectedRev },
    { ...updates, updatedAt: new Date().toISOString() }
  );
  // A successful stage transition changes the claimable set — bump the gate so
  // polling factories notice. Only on the winning CAS + when stage actually moved.
  if (res.ok && updates.stage !== undefined) await bumpQueueVersion(productId);
  return res;
}

export async function deleteJob(id: string, productId: string): Promise<void> {
  await jobs().delete(id, productId);
}

/** Terminal jobs (cross-partition) last updated before `cutoffIso` — retention GC
 *  candidates. `stages` is the terminal set the caller considers prunable. */
export async function listTerminalJobsOlderThan(
  cutoffIso: string,
  stages: readonly FleetStage[]
): Promise<FleetJobDoc[]> {
  return jobs().findMany({
    filter: { stage: { $in: stages as unknown as string[] }, updatedAt: { $lt: cutoffIso } },
  });
}

/**
 * Record the last tracker Item status echoed for a job (§10 round-trip idempotency).
 * Deliberately does NOT bump `rev` — it is bookkeeping for the downstream echo, not a
 * lifecycle transition, so it never interferes with the coordinator's fenced CAS.
 */
export async function setTrackerEchoedStatus(
  id: string,
  productId: string,
  status: string
): Promise<FleetJobDoc | null> {
  const cur = await jobs().findById(id, productId);
  if (!cur) return null;
  return jobs().update(id, productId, {
    trackerEchoedStatus: status,
    updatedAt: new Date().toISOString(),
  });
}

// ── Runs ────────────────────────────────────────────────────────────────────

export async function createRun(doc: FleetRunDoc): Promise<FleetRunDoc> {
  return runs().create(doc);
}

export async function updateRun(
  id: string,
  jobId: string,
  updates: Partial<FleetRunDoc>
): Promise<FleetRunDoc | null> {
  const cur = await runs().findById(id, jobId);
  if (!cur) return null;
  return runs().update(id, jobId, updates);
}

export async function listRunsByJob(jobId: string): Promise<FleetRunDoc[]> {
  return runs().findMany({ filter: { jobId }, sort: { attempt: 1 } });
}

export async function deleteRun(id: string, jobId: string): Promise<void> {
  await runs().delete(id, jobId);
}

/** All runs for a product (cross-partition) — used for cost aggregation. */
export async function listRunsByProduct(productId: string): Promise<FleetRunDoc[]> {
  return runs().findMany({ filter: { productId } });
}

// ── Leases ──────────────────────────────────────────────────────────────────

export async function getLease(jobId: string): Promise<FleetLeaseDoc | null> {
  return leases().findById(jobId, jobId);
}

export async function createLease(doc: FleetLeaseDoc): Promise<FleetLeaseDoc> {
  return leases().create(doc);
}

/** Compare-and-swap on a lease's `rev` — atomic via the datastore updateIfMatch. */
export async function revUpdateLease(
  jobId: string,
  expectedRev: number,
  updates: Partial<FleetLeaseDoc>
): Promise<RevResult<FleetLeaseDoc>> {
  return leases().updateIfMatch(
    jobId,
    jobId,
    { rev: expectedRev },
    { ...updates, updatedAt: new Date().toISOString() }
  );
}

export async function listExpiredLeases(nowIso: string): Promise<FleetLeaseDoc[]> {
  return leases().findMany({
    filter: { status: 'held', expiresAt: { $lt: nowIso } },
  });
}

/** All currently-held leases (cross-partition) — used by stale-factory reclaim. */
export async function listHeldLeases(): Promise<FleetLeaseDoc[]> {
  return leases().findMany({ filter: { status: 'held' } });
}

/** Finished (expired/released) leases last touched before `cutoffIso` — GC candidates.
 *  These are pure coordination ephemera (not audit history), safe to delete. */
export async function listFinishedLeasesOlderThan(cutoffIso: string): Promise<FleetLeaseDoc[]> {
  return leases().findMany({
    filter: { status: { $in: ['expired', 'released'] }, updatedAt: { $lt: cutoffIso } },
  });
}

export async function deleteLease(jobId: string): Promise<void> {
  await leases().delete(jobId, jobId);
}

// ── Factories ─────────────────────────────────────────────────────────────────

export async function getFactory(
  factoryId: string,
  productId: string
): Promise<FleetFactoryDoc | null> {
  return factories().findById(factoryId, productId);
}

export async function upsertFactory(doc: FleetFactoryDoc): Promise<FleetFactoryDoc> {
  return factories().upsert(doc);
}

export async function listFactories(productId: string): Promise<FleetFactoryDoc[]> {
  return factories().findMany({ filter: { productId } });
}

/** All registered factories across every product (cross-partition) — used by the
 *  reaper to find stale holders and GC long-dead factory docs. */
export async function listAllFactories(): Promise<FleetFactoryDoc[]> {
  return factories().findMany({});
}

export async function deleteFactory(factoryId: string, productId: string): Promise<void> {
  await factories().delete(factoryId, productId);
}

// ── Profiles ────────────────────────────────────────────────────────────────

export async function createProfile(doc: FleetProfileDoc): Promise<FleetProfileDoc> {
  return profiles().create(doc);
}

export async function getProfile(id: string, productId: string): Promise<FleetProfileDoc | null> {
  return profiles().findById(id, productId);
}

export async function listProfiles(productId: string): Promise<FleetProfileDoc[]> {
  return profiles().findMany({ filter: { productId } });
}

// ── Events (append-only) ────────────────────────────────────────────────────

export interface AppendEventInput {
  jobId: string;
  productId: string;
  type: string;
  actor?: string;
  /** Trace-context correlation id of the owning job (§4), stamped on the event. */
  correlationId?: string;
  data?: Record<string, unknown>;
}

/** Append an ordered event to a job's stream; `seq` is monotonic per job. */
export async function appendEvent(input: AppendEventInput): Promise<FleetEventDoc> {
  const existing = await events().findMany({ filter: { jobId: input.jobId } });
  const seq = existing.length;
  const doc: FleetEventDoc = {
    id: `${input.jobId}:evt:${seq}`,
    productId: input.productId,
    jobId: input.jobId,
    seq,
    type: input.type,
    at: new Date().toISOString(),
    actor: input.actor,
    correlationId: input.correlationId,
    data: input.data ?? {},
  };
  return events().create(doc);
}

export async function listEvents(jobId: string): Promise<FleetEventDoc[]> {
  const docs = await events().findMany({ filter: { jobId }, sort: { seq: 1 } });
  return docs;
}

export async function deleteEvent(id: string, jobId: string): Promise<void> {
  await events().delete(id, jobId);
}

// ── Artifacts (pointers only — bytes live in blob, never Cosmos; §13) ──────────

export async function createArtifact(doc: FleetArtifactDoc): Promise<FleetArtifactDoc> {
  return artifacts().create(doc);
}

/**
 * All artifact pointers for a job, oldest-first (single partition — pk `/jobId`),
 * scoped to the owning `productId` so a caller can only list its own product's
 * artifacts (a foreign product sees none).
 */
export async function listArtifactsByJob(
  jobId: string,
  productId: string
): Promise<FleetArtifactDoc[]> {
  return artifacts().findMany({ filter: { jobId, productId }, sort: { createdAt: 1 } });
}

/**
 * Fetch one artifact pointer by id, scoped to its owning product. The container
 * is partitioned by `/jobId` (not `/productId`), so this is a small filtered
 * query rather than a point read; the `productId` predicate enforces ownership
 * for the by-id routes (foreign-product / unknown id → null).
 */
export async function getArtifact(id: string, productId: string): Promise<FleetArtifactDoc | null> {
  const found = await artifacts().findMany({ filter: { id, productId }, limit: 1 });
  return found[0] ?? null;
}

/**
 * Delete an artifact pointer by id, scoped to its owning product. Resolves the
 * partition (`jobId`) from the located doc, then deletes. Returns the removed doc
 * (so callers can also clean up the backing blob) or null when not found.
 */
export async function deleteArtifact(
  id: string,
  productId: string
): Promise<FleetArtifactDoc | null> {
  const doc = await getArtifact(id, productId);
  if (!doc) return null;
  await artifacts().delete(id, doc.jobId);
  return doc;
}

// ── Factory tokens (scoped, rotatable credentials — §12; hashed at rest) ────────

export async function createFactoryToken(doc: FleetFactoryTokenDoc): Promise<FleetFactoryTokenDoc> {
  return factoryTokens().create(doc);
}

export async function getFactoryToken(
  id: string,
  productId: string
): Promise<FleetFactoryTokenDoc | null> {
  return factoryTokens().findById(id, productId);
}

/** Resolve a token by its sha256 hash (verify path — partition-agnostic lookup). */
export async function findFactoryTokenByHash(
  tokenHash: string
): Promise<FleetFactoryTokenDoc | null> {
  const found = await factoryTokens().findMany({ filter: { tokenHash }, limit: 1 });
  return found[0] ?? null;
}

/** All token records for a factory within a product (rotate/revoke lookups). */
export async function listFactoryTokens(
  productId: string,
  factoryId: string
): Promise<FleetFactoryTokenDoc[]> {
  return factoryTokens().findMany({ filter: { productId, factoryId } });
}

export async function updateFactoryToken(
  id: string,
  productId: string,
  updates: Partial<FleetFactoryTokenDoc>
): Promise<FleetFactoryTokenDoc | null> {
  const cur = await factoryTokens().findById(id, productId);
  if (!cur) return null;
  return factoryTokens().update(id, productId, updates);
}

/** All factory tokens across every product (cross-partition) — used by token GC. */
export async function listAllFactoryTokens(): Promise<FleetFactoryTokenDoc[]> {
  return factoryTokens().findMany({});
}

export async function deleteFactoryToken(id: string, productId: string): Promise<void> {
  await factoryTokens().delete(id, productId);
}

// ── Budgets (Phase 3 §11/§13) ──────────────────────────────────────────────

export async function getBudget(productId: string): Promise<FleetBudgetDoc | null> {
  return budgets().findById(productId, productId);
}

export async function upsertBudget(doc: FleetBudgetDoc): Promise<FleetBudgetDoc> {
  return budgets().upsert(doc);
}

export async function updateBudget(
  productId: string,
  updates: Partial<FleetBudgetDoc>
): Promise<FleetBudgetDoc | null> {
  const cur = await budgets().findById(productId, productId);
  if (!cur) return null;
  return budgets().update(productId, productId, {
    ...updates,
    updatedAt: new Date().toISOString(),
  });
}
