/**
 * This file is part of the NocoBase (R) project.
 * Copyright (c) 2020-2024 NocoBase Co., Ltd.
 * Authors: NocoBase Team.
 *
 * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
 * For more information, please refer to: https://www.nocobase.com/agreement.
 */

var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
  for (var name in all)
    __defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
  if (from && typeof from === "object" || typeof from === "function") {
    for (let key of __getOwnPropNames(from))
      if (!__hasOwnProp.call(to, key) && key !== except)
        __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
  }
  return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var Dispatcher_exports = {};
__export(Dispatcher_exports, {
  default: () => Dispatcher
});
module.exports = __toCommonJS(Dispatcher_exports);
var import_node_crypto = require("node:crypto");
var import_sequelize = require("sequelize");
var import_constants = require("./constants");
var import_Plugin = require("./Plugin");
var import_utils = require("./utils");
const EXECUTION_ACQUIRE_MAX_ATTEMPTS = 5;
const QUEUE_TASK_MAX_ATTEMPTS = 3;
const RECOVERY_BATCH_SIZE = 100;
const RECOVERY_LOCK_TTL = 3e5;
class Dispatcher {
  constructor(plugin) {
    this.plugin = plugin;
  }
  ready = false;
  executing = null;
  recovering = null;
  saving = null;
  events = [];
  eventsCount = 0;
  get idle() {
    return this.ready && !this.executing && !this.saving && !this.events.length;
  }
  onQueueTask = async (event) => {
    var _a;
    const task = event;
    let next = null;
    const previous = this.executing;
    const executing = (async () => {
      await (previous == null ? void 0 : previous.catch(() => void 0));
      this.plugin.getLogger("dispatcher").info(`workflow queue task for execution (${task.executionId}) received from queue`);
      next = await this.resolveTask(task);
      if (!next) {
        return;
      }
      this.plugin.getLogger(next[0].workflowId).info(`queued execution (${next[0].id}) ready to process`);
      await this.process(next[0], next[1], { rerun: next[2] });
    })();
    this.executing = executing;
    try {
      await executing;
    } catch (error) {
      const workflowId = ((_a = next == null ? void 0 : next[0]) == null ? void 0 : _a.workflowId) ?? task.executionId;
      this.plugin.getLogger(workflowId).error(`workflow queue task failed`, { error });
      throw error;
    } finally {
      if (this.executing === executing) {
        this.executing = null;
      }
      if (this.events.length) {
        this.saveEvent();
      }
    }
  };
  setReady(ready) {
    this.ready = ready;
  }
  getEventsCount() {
    return this.eventsCount;
  }
  trigger(workflow, context, options = {}) {
    const logger = this.plugin.getLogger(workflow.id);
    if (!this.ready) {
      logger.warn(`app is not ready, event of workflow ${workflow.id} will be ignored`);
      logger.debug(`ignored event data:`, context);
      return this.handleTriggerFail(workflow, context, options, new Error("app is not ready"));
    }
    if (!options.force && !options.manually && !workflow.enabled) {
      logger.warn(`workflow ${workflow.id} is not enabled, event will be ignored`);
      return;
    }
    const duplicated = this.events.find(([w, c, { eventKey }]) => {
      if (eventKey && options.eventKey) {
        return eventKey === options.eventKey;
      }
    });
    if (duplicated) {
      logger.warn(`event of workflow ${workflow.id} is duplicated (${options.eventKey}), event will be ignored`);
      return;
    }
    if (context == null) {
      logger.warn(`workflow ${workflow.id} event data context is null, event will be ignored`);
      return this.handleTriggerFail(workflow, context, options, new Error("event context is null"));
    }
    if (options.manually || this.plugin.isWorkflowSync(workflow)) {
      return this.triggerSync(workflow, context, options);
    }
    const { transaction, ...rest } = options;
    this.events.push([workflow, context, rest]);
    this.eventsCount = this.events.length;
    logger.info(`new event triggered, now events: ${this.events.length}`);
    logger.debug(`event data:`, { context });
    this.saveEvent();
  }
  saveEvent() {
    if (this.saving) {
      return;
    }
    this.saving = (async () => {
      try {
        while (this.events.length) {
          if (this.executing && this.plugin.db.options.dialect === "sqlite") {
            await this.executing;
          }
          const event = this.events.shift();
          this.eventsCount = this.events.length;
          if (!event) continue;
          const logger = this.plugin.getLogger(event[0].id);
          logger.info(`preparing execution for event`);
          try {
            const execution = await this.createExecution(...event);
            if (!execution.dispatched) {
              try {
                await this.enqueue({
                  executionId: execution.id
                });
              } catch (qErr) {
                logger.error(`publishing execution (${execution.id}) to queue failed:`, { error: qErr });
              }
            }
          } catch (error) {
            logger.error(`failed to create execution:`, { error });
          }
        }
      } finally {
        this.saving = null;
        if (this.events.length) {
          this.saveEvent();
        }
      }
    })();
  }
  async beforeStop() {
    this.ready = false;
    this.plugin.getLogger("dispatcher").info("app is stopping, draining local queues...");
    if (this.recovering) {
      await this.recovering;
    }
    while (this.saving || this.executing || this.events.length) {
      if (this.saving) {
        await this.saving;
      }
      if (this.executing) {
        await this.executing;
      }
      if (this.events.length && !this.saving) {
        this.saveEvent();
      }
      await new Promise((resolve) => setImmediate(resolve));
    }
    this.plugin.getLogger("dispatcher").info("local queues drained");
  }
  async recover(options = {}) {
    if (!this.ready) {
      return;
    }
    if (this.recovering) {
      await this.recovering;
      return;
    }
    const recovering = this.recoverQueueTasks(options.gracePeriod ?? 0);
    this.recovering = recovering;
    try {
      await recovering;
    } finally {
      if (this.recovering === recovering) {
        this.recovering = null;
      }
    }
  }
  async recoverQueueTasks(gracePeriod) {
    const logger = this.plugin.getLogger("dispatcher");
    try {
      if (!this.plugin.serving()) {
        logger.warn(
          `${import_Plugin.WORKER_JOB_WORKFLOW_PROCESS} is not serving on this instance, workflow queue recovery will be ignored`
        );
        return;
      }
      let lock;
      try {
        lock = await this.plugin.app.lockManager.tryAcquire(`workflow:recover:${this.plugin.app.name}`);
      } catch (error) {
        if ((0, import_utils.isLockAcquireError)(error)) {
          logger.debug(`workflow queue recovery is already running on another instance`);
          return;
        }
        throw error;
      }
      await lock.runExclusive(async () => {
        let executionCursor = null;
        while (this.ready) {
          const executions = await this.findQueueingExecutions(executionCursor, gracePeriod);
          if (!executions.length) {
            break;
          }
          for (const execution of executions) {
            if (!this.ready) {
              return;
            }
            await this.enqueue({ executionId: execution.id });
          }
          executionCursor = executions[executions.length - 1].id;
        }
      }, RECOVERY_LOCK_TTL);
    } catch (error) {
      logger.error(`workflow queue recovery failed`, { error });
    }
  }
  async enqueue(task) {
    await this.plugin.app.eventQueue.publish(this.plugin.channelPendingExecution, task, {
      maxRetries: QUEUE_TASK_MAX_ATTEMPTS - 1
    });
  }
  async loadJob(jobId) {
    if (jobId == null) {
      return null;
    }
    return this.plugin.db.getRepository("jobs").findOne({
      filterByTk: jobId
    });
  }
  async findQueueingExecutions(afterId, gracePeriod) {
    const executions = await this.plugin.db.getRepository("executions").find({
      filter: {
        ...afterId == null ? {} : { id: { $gt: afterId } },
        ...gracePeriod > 0 ? { createdAt: { $lt: new Date(Date.now() - gracePeriod) } } : {},
        dispatched: false,
        status: import_constants.EXECUTION_STATUS.QUEUEING,
        startedAt: null,
        "workflow.enabled": true
      },
      sort: "id",
      limit: RECOVERY_BATCH_SIZE
    });
    for (const execution of executions) {
      this.plugin.getLogger(execution.workflowId).info(`queueing execution (${execution.id}) found, publishing task`);
    }
    return executions;
  }
  async resolveTask(task) {
    const executionInput = await this.plugin.db.getRepository("executions").findOne({
      filterByTk: task.executionId
    });
    if (!executionInput) {
      this.plugin.getLogger("dispatcher").warn(`execution (${task.executionId}) not found, queue task ignored`);
      return null;
    }
    if (task.jobId == null && task.rerun == null && executionInput.status === import_constants.EXECUTION_STATUS.STARTED && executionInput.startedAt) {
      this.plugin.getLogger(executionInput.workflowId).warn(`execution (${executionInput.id}) has already started, start task ignored`);
      return null;
    }
    let job = null;
    if (task.jobId != null) {
      job = await this.loadJob(task.jobId);
      if (!job) {
        this.plugin.getLogger(executionInput.workflowId).warn(`job (${task.jobId}) not found, resume ignored`);
        return null;
      }
      if (String(job.executionId) !== String(executionInput.id)) {
        this.plugin.getLogger(executionInput.workflowId).warn(`job (${job.id}) does not belong to execution (${executionInput.id}), resume ignored`);
        return null;
      }
    }
    const execution = await this.prepare(executionInput);
    if (!execution) {
      return null;
    }
    if (job) {
      job.execution = execution;
      return [execution, job];
    }
    return [execution, void 0, task.rerun];
  }
  async triggerSync(workflow, context, { deferred, ...options } = {}) {
    let execution;
    try {
      execution = await this.createExecution(workflow, context, options);
    } catch (err) {
      if (err instanceof Error) {
        this.plugin.getLogger(workflow.id).error(`creating execution failed: ${err.message}`, err);
      }
      return null;
    }
    try {
      const entered = await this.prepare(execution);
      if (!entered) {
        return null;
      }
      return this.process(entered, void 0, options);
    } catch (err) {
      if (err instanceof Error) {
        this.plugin.getLogger(execution.workflowId).error(`execution (${execution.id}) error: ${err.message}`, err);
      }
    }
    return null;
  }
  async validateEvent(workflow, context, options) {
    const trigger = this.plugin.triggers.get(workflow.type);
    const triggerValid = await trigger.validateEvent(workflow, context, options);
    if (!triggerValid) {
      return false;
    }
    const { stack = [] } = options;
    let valid = true;
    if (stack == null ? void 0 : stack.length) {
      const existed = await workflow.countExecutions({
        where: {
          id: stack
        }
      });
      const limitCount = workflow.options.stackLimit || 1;
      if (existed >= limitCount) {
        this.plugin.getLogger(workflow.id).warn(
          `workflow ${workflow.id} has already been triggered in stacks executions (${stack}), and max call count is ${limitCount}, newly triggering will be skipped.`
        );
        valid = false;
      }
    }
    return valid;
  }
  async handleTriggerFail(workflow, context, options, error) {
    var _a;
    try {
      await ((_a = options.onTriggerFail) == null ? void 0 : _a.call(options, workflow, context, options, error));
    } catch (triggerFailError) {
      this.plugin.getLogger(workflow.id).error(`trigger failure callback failed`, { error: triggerFailError });
    }
  }
  async createExecution(workflow, context, options) {
    const { deferred } = options;
    let stack = options.stack;
    if (options.parentExecutionId && !stack) {
      const parentExecution = await this.plugin.db.getRepository("executions").findOne({
        filterByTk: options.parentExecutionId
      });
      stack = parentExecution ? [...parentExecution.stack ?? [], parentExecution.id] : [];
    }
    let valid;
    try {
      valid = await this.validateEvent(workflow, context, { ...options, stack });
    } catch (error) {
      await this.handleTriggerFail(workflow, context, options, error);
      throw error;
    }
    if (!valid) {
      const error = new Error("event is not valid");
      await this.handleTriggerFail(workflow, context, options, error);
      throw error;
    }
    let execution;
    try {
      execution = await workflow.createExecution({
        context,
        key: workflow.key,
        eventKey: options.eventKey ?? (0, import_node_crypto.randomUUID)(),
        stack,
        parentExecutionId: options.parentExecutionId ?? null,
        dispatched: deferred ?? false,
        status: deferred ? import_constants.EXECUTION_STATUS.STARTED : import_constants.EXECUTION_STATUS.QUEUEING,
        manually: options.manually
      });
    } catch (error) {
      await this.handleTriggerFail(workflow, context, options, error);
      throw error;
    }
    this.plugin.getLogger(workflow.id).info(`execution of workflow ${workflow.id} created as ${execution.id}`);
    if (!workflow.stats) {
      workflow.stats = await workflow.getStats();
    }
    await workflow.stats.increment("executed");
    if (this.plugin.db.options.dialect !== "postgres") {
      await workflow.stats.reload();
    }
    if (!workflow.versionStats) {
      workflow.versionStats = await workflow.getVersionStats();
    }
    await workflow.versionStats.increment("executed");
    if (this.plugin.db.options.dialect !== "postgres") {
      await workflow.versionStats.reload();
    }
    execution.workflow = workflow;
    return execution;
  }
  async prepare(input, options = {}) {
    const logger = this.plugin.getLogger(input.workflowId);
    if (options.transaction) {
      try {
        return await this.acquireExecution(input, options, options.transaction);
      } catch (error) {
        if (error instanceof Error) {
          logger.error(`entering execution failed: ${error.message}`, { error });
        }
        return null;
      }
    }
    let result = null;
    try {
      await this.acquireWithRetry(
        async () => {
          const tx = await this.plugin.db.sequelize.transaction({
            isolationLevel: this.plugin.db.options.dialect === "sqlite" ? void 0 : import_sequelize.Transaction.ISOLATION_LEVELS.READ_COMMITTED
          });
          try {
            const execution = await this.acquireExecution(input, options, tx);
            await tx.commit();
            result = execution;
          } catch (error) {
            await tx.rollback();
            if (this.isConcurrentAcquireError(error)) {
              throw error;
            }
            if (error instanceof Error) {
              logger.error(`entering execution failed: ${error.message}`, { error });
            }
            result = null;
          }
        },
        {
          logger,
          conflictMessage: `acquiring queue task execution (${input.id}) conflicted with another worker, retrying`,
          maxAttemptsMessage: `acquiring queue task execution (${input.id}) reached max retry attempts`
        }
      );
    } catch (error) {
      if (error instanceof Error) {
        logger.error(`acquiring execution failed: ${error.message}`, { error });
      }
      return null;
    }
    return result;
  }
  async acquireExecution(input, options, transaction) {
    if (!options.immediate || input.status !== import_constants.EXECUTION_STATUS.QUEUEING) {
      await input.reload({ transaction });
    }
    return this.enter(input, transaction);
  }
  async acquireWithRetry(acquire, options) {
    for (let attempt = 1; attempt <= EXECUTION_ACQUIRE_MAX_ATTEMPTS; attempt++) {
      try {
        await acquire();
        break;
      } catch (error) {
        if (!this.isConcurrentAcquireError(error)) {
          throw error;
        }
        options.logger.warn(options.conflictMessage, { error });
        if (attempt >= EXECUTION_ACQUIRE_MAX_ATTEMPTS) {
          options.logger.warn(options.maxAttemptsMessage);
          break;
        }
      }
    }
  }
  async enter(execution, transaction) {
    const workflow = execution.workflow || this.plugin.enabledCache.get(execution.workflowId) || await execution.getWorkflow({ transaction });
    if (!workflow) {
      this.plugin.getLogger(execution.workflowId).warn(`workflow (${execution.workflowId}) not found for execution`, {
        workflowId: execution.workflowId,
        executionId: execution.id
      });
    }
    if (execution.status && execution.status !== import_constants.EXECUTION_STATUS.STARTED) {
      return null;
    }
    if (execution.dispatched && execution.status === import_constants.EXECUTION_STATUS.STARTED && execution.startedAt) {
      execution.workflow = workflow;
      return execution;
    }
    const values = {
      dispatched: true,
      status: import_constants.EXECUTION_STATUS.STARTED
    };
    const where = {
      id: execution.id,
      status: execution.status ?? null
    };
    if (!execution.dispatched) {
      where.dispatched = false;
    }
    if (!execution.startedAt) {
      const startedAt = /* @__PURE__ */ new Date();
      values.startedAt = startedAt;
      execution.workflow = workflow;
      values.expiresAt = this.plugin.timeoutManager.getExpiresAt(execution, startedAt);
      where.startedAt = null;
    }
    const ExecutionModelClass = this.plugin.db.getModel("executions");
    const [affected] = await ExecutionModelClass.update(values, {
      where,
      transaction
    });
    if (!affected) {
      return null;
    }
    await execution.reload({ transaction });
    execution.workflow = workflow;
    return execution;
  }
  isConcurrentAcquireError(error) {
    var _a, _b, _c, _d;
    if (!(error instanceof Error)) {
      return false;
    }
    const databaseError = error;
    const code = ((_a = databaseError.parent) == null ? void 0 : _a.code) || ((_b = databaseError.original) == null ? void 0 : _b.code);
    const errno = ((_c = databaseError.parent) == null ? void 0 : _c.errno) || ((_d = databaseError.original) == null ? void 0 : _d.errno);
    return code === "40001" || code === "40P01" || code === "ER_LOCK_DEADLOCK" || errno === 1205 || errno === 1213;
  }
  async process(execution, job = null, options = {}) {
    const { rerun, ...processorOptions } = options;
    const logger = this.plugin.getLogger(execution.workflowId);
    const run = async () => {
      var _a, _b, _c;
      if (!execution.dispatched) {
        await execution.update({ dispatched: true, status: import_constants.EXECUTION_STATUS.STARTED });
        logger.info(`execution (${execution.id}) from workflow queue updated to started`);
      }
      this.plugin.timeoutManager.scheduleExecutionTimeout(execution);
      const processor = this.plugin.createProcessor(execution, processorOptions);
      logger.info(`execution (${execution.id}) ${rerun ? "rerunning" : job ? "resuming" : "starting"}...`);
      try {
        await (rerun ? processor.rerun(rerun) : job ? processor.resume(job) : processor.start());
        logger.info(`execution (${execution.id}) finished with status: ${execution.status}`);
        logger.debug(`execution (${execution.id}) details:`, { execution });
        if (execution.status && ((_c = (_b = (_a = execution.workflow) == null ? void 0 : _a.options) == null ? void 0 : _b.deleteExecutionOnStatus) == null ? void 0 : _c.includes(execution.status))) {
          await execution.destroy();
        }
      } catch (err) {
        if (err instanceof Error) {
          logger.error(`execution (${execution.id}) error: ${err.message}`, err);
        }
      }
      return processor;
    };
    const lockManager = this.plugin.app.lockManager;
    const lock = await lockManager.tryAcquire((0, import_utils.getExecutionLockKey)(execution.id), 6e4);
    try {
      return await lock.runExclusive(run, 6e4);
    } catch (error) {
      logger.error(`execution (${execution.id}) could not acquire process lock`, { error });
      throw error;
    }
  }
}
