import {
  BULLMQ_CONNECTION,
  BULLMQ_RESOLVED_CONFIG,
  resolvePoolQueueName
} from "../../../chunk-I6MVCB5A.js";
import "../../../chunk-RHVN6NA7.js";
import {
  DrizzleJobOrchestrator
} from "../../../chunk-OTR44OH6.js";
import "../../../chunk-T4BIIU5E.js";
import {
  JOBS_MULTI_TENANT
} from "../../../chunk-ZPL74UQN.js";
import {
  jobRuns,
  jobs
} from "../../../chunk-OKXZ63IA.js";
import "../../../chunk-MYQIQ27N.js";
import "../../../chunk-GYGNEQSC.js";
import {
  DRIZZLE
} from "../../../chunk-U64T4YZE.js";
import {
  __decorateClass,
  __decorateParam
} from "../../../chunk-2E224ZSN.js";

// runtime/subsystems/jobs/job-orchestrator.bullmq-backend.ts
import { createHash } from "crypto";
import { Inject, Injectable, Logger, Optional } from "@nestjs/common";
import { eq } from "drizzle-orm";
function sha1JobId(idempotencyKey) {
  return createHash("sha1").update(idempotencyKey).digest("hex");
}
var BullMQJobOrchestrator = class extends DrizzleJobOrchestrator {
  constructor(db, multiTenant, connection, bullConfig = null) {
    super(db, multiTenant);
    this.connection = connection;
    this.bullConfig = bullConfig;
    this.bullDb = db;
  }
  connection;
  bullConfig;
  // TODO(logging-subsystem): swap to ILogger once ADR-028 lands
  bullLogger = new Logger(BullMQJobOrchestrator.name);
  /** Lazily-opened `Queue` handles, one per pool. */
  queues = /* @__PURE__ */ new Map();
  /** Single FlowProducer for parent/child hierarchies. Lazily opened. */
  _flow = null;
  /**
   * Cached `bullmq` value constructors, populated by `loadBullMq()` on first
   * use (the `start`/`cancel`/`replay` entrypoints `await` it before touching
   * a queue). Kept off the import graph so a `drizzle`-only consumer never
   * resolves the optional `'bullmq'` package.
   */
  QueueCtor = null;
  FlowProducerCtor = null;
  bullMqLoad = null;
  /**
   * Own reference to the Drizzle client. `DrizzleJobOrchestrator.db` is
   * `private` (can't be redeclared even privately in a subclass), and the
   * spec forbids touching that file — so the subclass keeps its own handle
   * under a distinct name (same instance, passed through to `super`) for the
   * cancel-cascade snapshot + definition/run loads below.
   */
  bullDb;
  /**
   * Lazily load the optional `bullmq` package and cache its value
   * constructors. Idempotent (single in-flight promise). Throws a friendly,
   * actionable error when the consumer selected `backend: 'bullmq'` but did
   * not install the package — mirrors `createRedisClient` in the redis event
   * backend. Must be `await`ed before any `queueFor`/`flow` access.
   */
  async loadBullMq() {
    if (this.QueueCtor && this.FlowProducerCtor) return;
    if (!this.bullMqLoad) {
      this.bullMqLoad = (async () => {
        try {
          const mod = await import("bullmq");
          this.QueueCtor = mod.Queue;
          this.FlowProducerCtor = mod.FlowProducer;
        } catch {
          throw new Error(
            'BullMQ backend requires the "bullmq" package. Install it with: npm install bullmq'
          );
        }
      })();
    }
    await this.bullMqLoad;
  }
  /**
   * Open (or reuse) the `Queue` for a pool. Synchronous — callers `await
   * loadBullMq()` first so `QueueCtor` is populated.
   */
  queueFor(pool) {
    if (!this.QueueCtor) {
      throw new Error("BullMQJobOrchestrator: queueFor called before loadBullMq()");
    }
    const name = resolvePoolQueueName(pool, this.bullConfig);
    let q = this.queues.get(name);
    if (!q) {
      q = new this.QueueCtor(name, { connection: this.connection });
      this.queues.set(name, q);
    }
    return q;
  }
  flow() {
    if (!this.FlowProducerCtor) {
      throw new Error("BullMQJobOrchestrator: flow called before loadBullMq()");
    }
    if (!this._flow) {
      this._flow = new this.FlowProducerCtor({ connection: this.connection });
    }
    return this._flow;
  }
  // ==========================================================================
  // start — Postgres insert (super) + BullMQ dispatch
  // ==========================================================================
  async start(type, input, opts = {}, tx) {
    const run = await super.start(type, input, opts, tx);
    await this.dispatch(run, type);
    return run;
  }
  /**
   * Map a `job_run` row onto a BullMQ job via `queue.add`. When the run has a
   * `parentRunId` we attach it to the parent's existing BullMQ job through the
   * `parent: { id, queue }` opt — BullMQ then tracks the parent/child link in
   * its own graph. (The FlowProducer is reserved for whole-tree atomic
   * submits, exposed as an opt-in extension via `flowProducer()`; runtime
   * `ctx.spawnChild` is incremental, so `queue.add` with a parent ref is the
   * correct primitive here.)
   *
   * The `jobId` is colon-safe + stable: `sha1(dedupeKey)` when a dedupe key is
   * present (so the same logical key dedups), else the `job_run.id` UUID
   * (already colon-free).
   *
   * The domain `parentClosePolicy` cascade is still enforced in Postgres by
   * the shared `cancel` path — BullMQ's parent link is dispatch bookkeeping,
   * not the authority.
   */
  async dispatch(run, type) {
    await this.loadBullMq();
    const def = await this.loadDefinition(type);
    const jobId = run.dedupeKey ? sha1JobId(run.dedupeKey) : run.id;
    const jobOpts = {
      jobId,
      ...this.retryOpts(def),
      ...this.dedupeOpts(run, def)
    };
    if (run.parentRunId) {
      const parentRow = await this.loadRun(run.parentRunId);
      if (parentRow) {
        const parentJobId = parentRow.dedupeKey ? sha1JobId(parentRow.dedupeKey) : parentRow.id;
        jobOpts.parent = {
          id: parentJobId,
          queue: resolvePoolQueueName(parentRow.pool, this.bullConfig)
        };
      }
    }
    const payload = { runId: run.id, type, input: run.input };
    await this.queueFor(run.pool).add(type, payload, jobOpts);
  }
  /**
   * Opt-in extension (spec §Extensions): expose the FlowProducer for
   * consumers that want to submit a whole parent/child DAG atomically up
   * front, rather than incrementally via `ctx.spawnChild`. Backend-specific —
   * code using it is not portable to the Drizzle backend. Async because it
   * lazily loads the optional `bullmq` package on first use.
   */
  async flowProducer() {
    await this.loadBullMq();
    return this.flow();
  }
  retryOpts(def) {
    const policy = def.retryPolicy;
    if (!policy) return {};
    return {
      attempts: policy.attempts,
      backoff: {
        type: policy.backoff === "exponential" ? "exponential" : "fixed",
        delay: policy.baseMs
      }
    };
  }
  dedupeOpts(run, def) {
    if (!run.dedupeKey || !def.dedupeWindowMs) return {};
    return {
      deduplication: {
        id: sha1JobId(run.dedupeKey),
        ttl: def.dedupeWindowMs
      }
    };
  }
  // ==========================================================================
  // cancel — Postgres cascade (super) + remove from queue
  // ==========================================================================
  async cancel(runId, opts = {}) {
    const target = await this.loadRun(runId);
    await super.cancel(runId, opts);
    if (!target) return;
    await this.loadBullMq();
    await this.removeFromQueue(target);
    if (opts.cascade === false) return;
    const descendants = await this.bullDb.select().from(jobRuns).where(eq(jobRuns.rootRunId, target.rootRunId));
    for (const child of descendants) {
      if (child.id === runId) continue;
      await this.removeFromQueue(child);
    }
  }
  async removeFromQueue(run) {
    const jobId = run.dedupeKey ? sha1JobId(run.dedupeKey) : run.id;
    try {
      const job = await this.queueFor(run.pool).getJob(jobId);
      if (job) await job.remove();
    } catch (err) {
      this.bullLogger.warn(
        `cancel: could not remove BullMQ job ${jobId} (pool=${run.pool}): ${err.message}`
      );
    }
  }
  // ==========================================================================
  // replay — Postgres reset (super) + re-enqueue
  // ==========================================================================
  async replay(runId) {
    const run = await super.replay(runId);
    await this.dispatch(run, run.jobType);
    return run;
  }
  // ==========================================================================
  // Internals
  // ==========================================================================
  async loadDefinition(type) {
    const [def] = await this.bullDb.select().from(jobs).where(eq(jobs.type, type)).limit(1);
    if (!def) {
      throw new Error(`BullMQJobOrchestrator: no job definition for '${type}'`);
    }
    return def;
  }
  async loadRun(id) {
    const [row] = await this.bullDb.select().from(jobRuns).where(eq(jobRuns.id, id)).limit(1);
    return row ?? null;
  }
  /** Close all open queue + flow connections. Called on module destroy. */
  async closeConnections() {
    for (const q of this.queues.values()) {
      await q.close().catch(() => void 0);
    }
    this.queues.clear();
    if (this._flow) {
      await this._flow.close().catch(() => void 0);
      this._flow = null;
    }
  }
};
BullMQJobOrchestrator = __decorateClass([
  Injectable(),
  __decorateParam(0, Inject(DRIZZLE)),
  __decorateParam(1, Inject(JOBS_MULTI_TENANT)),
  __decorateParam(2, Inject(BULLMQ_CONNECTION)),
  __decorateParam(3, Optional()),
  __decorateParam(3, Inject(BULLMQ_RESOLVED_CONFIG))
], BullMQJobOrchestrator);
export {
  BullMQJobOrchestrator,
  sha1JobId
};
//# sourceMappingURL=job-orchestrator.bullmq-backend.js.map