var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
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 __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
  // If the importer is in node compatibility mode or this is not an ESM
  // file that has been converted to a CommonJS file using a Babel-
  // compatible transform (i.e. "__esModule" has not been set), then set
  // "default" to the CommonJS "module.exports" for node compatibility.
  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
  mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var Plugin_exports = {};
__export(Plugin_exports, {
  default: () => PluginWorkflowServer
});
module.exports = __toCommonJS(Plugin_exports);
var import_path = __toESM(require("path"));
var import_database = require("@tachybase/database");
var import_server = require("@tachybase/server");
var import_utils = require("@tachybase/utils");
var import_lru_cache = require("lru-cache");
var import_actions = __toESM(require("./actions"));
var import_constants = require("./constants");
var import_plugin = __toESM(require("./features/_deprecated-js-parse/plugin"));
var import_plugin2 = __toESM(require("./features/_deprecated-json-parse/plugin"));
var import_Plugin = require("./features/aggregate/Plugin");
var import_Plugin2 = require("./features/delay/Plugin");
var import_Plugin3 = require("./features/dynamic-calculation/Plugin");
var import_interception = require("./features/interception");
var import_Plugin4 = require("./features/loop/Plugin");
var import_Plugin5 = require("./features/manual/Plugin");
var import_plugin3 = __toESM(require("./features/notice/plugin"));
var import_omni_trigger = require("./features/omni-trigger");
var import_Plugin6 = require("./features/parallel/Plugin");
var import_Plugin7 = require("./features/request/Plugin");
var import_response = require("./features/response");
var import_plugin4 = __toESM(require("./features/script/plugin"));
var import_Plugin8 = require("./features/sql/Plugin");
var import_plugin5 = require("./features/trigger-instruction/plugin");
var import_variables = require("./features/variables");
var import_functions = __toESM(require("./functions"));
var import_CalculationInstruction = __toESM(require("./instructions/CalculationInstruction"));
var import_ConditionInstruction = __toESM(require("./instructions/ConditionInstruction"));
var import_CreateInstruction = __toESM(require("./instructions/CreateInstruction"));
var import_DestroyInstruction = __toESM(require("./instructions/DestroyInstruction"));
var import_EndInstruction = __toESM(require("./instructions/EndInstruction"));
var import_QueryInstruction = __toESM(require("./instructions/QueryInstruction"));
var import_UpdateInstruction = __toESM(require("./instructions/UpdateInstruction"));
var import_UpdateOrCreateInstruction = __toESM(require("./instructions/UpdateOrCreateInstruction"));
var import_Processor = __toESM(require("./Processor"));
var import_CollectionTrigger = __toESM(require("./triggers/CollectionTrigger"));
var import_ScheduleTrigger = __toESM(require("./triggers/ScheduleTrigger"));
class PluginWorkflowServer extends import_server.Plugin {
  constructor(app, options) {
    super(app, options);
    this.instructions = new import_utils.Registry();
    this.triggers = new import_utils.Registry();
    this.functions = new import_utils.Registry();
    this.enabledCache = /* @__PURE__ */ new Map();
    this.ready = false;
    this.executing = null;
    this.pending = [];
    this.events = [];
    this.eventsCount = 0;
    this.meter = null;
    this.onBeforeSave = async (instance, options) => {
      const Model = instance.constructor;
      if (instance.enabled) {
        instance.set("current", true);
      } else if (!instance.current) {
        const count = await Model.count({
          where: {
            key: instance.key
          },
          transaction: options.transaction
        });
        if (!count) {
          instance.set("current", true);
        }
      }
      if (!instance.changed("enabled") || !instance.enabled) {
        return;
      }
      const previous = await Model.findOne({
        where: {
          key: instance.key,
          current: true,
          id: {
            [import_database.Op.ne]: instance.id
          }
        },
        transaction: options.transaction
      });
      if (previous) {
        await previous.update(
          { enabled: false, current: null },
          {
            transaction: options.transaction,
            hooks: false
          }
        );
        this.toggle(previous, false);
      }
    };
    this.prepare = async () => {
      if (this.executing && this.db.options.dialect === "sqlite") {
        await this.executing;
      }
      const event = this.events.shift();
      this.eventsCount = this.events.length;
      if (!event) {
        this.getLogger("dispatcher").warn(`events queue is empty, no need to prepare`);
        return;
      }
      const logger = this.getLogger(event[0].id);
      logger.info(`preparing execution for event`);
      try {
        const execution = await this.createExecution(...event);
        if (execution && !this.executing && !this.pending.length) {
          this.pending.push([execution]);
        }
      } catch (err) {
        logger.error(`failed to create execution: ${err.message}`, err);
      }
      if (this.events.length) {
        await this.prepare();
      } else {
        this.dispatch();
      }
    };
    this.addFeature(import_Plugin8.PluginSql);
    this.addFeature(import_Plugin7.PluginRequest);
    this.addFeature(import_Plugin6.PluginParallel);
    this.addFeature(import_Plugin5.PluginManual);
    this.addFeature(import_Plugin4.PluginLoop);
    this.addFeature(import_Plugin3.PluginDynamicCalculation);
    this.addFeature(import_Plugin2.PluginDelay);
    this.addFeature(import_Plugin.PluginAggregate);
    this.addFeature(import_plugin2.default);
    this.addFeature(import_plugin.default);
    this.addFeature(import_plugin4.default);
    this.addFeature(import_interception.PluginInterception);
    this.addFeature(import_variables.PluginVariables);
    this.addFeature(import_response.PluginResponse);
    this.addFeature(import_omni_trigger.PluginOmniTrigger);
    this.addFeature(import_plugin5.PluginTriggerInstruction);
    this.addFeature(import_plugin3.default);
  }
  getLogger(workflowId) {
    const now = /* @__PURE__ */ new Date();
    const date = `${now.getFullYear()}-${`0${now.getMonth() + 1}`.slice(-2)}-${`0${now.getDate()}`.slice(-2)}`;
    const key = `${date}-${workflowId}}`;
    if (this.loggerCache.has(key)) {
      return this.loggerCache.get(key);
    }
    const logger = this.createLogger({
      dirname: import_path.default.join("workflows", date),
      filename: `${workflowId}.log`,
      transports: process.env.APP_ENV !== "production" ? ["console"] : ["file"]
    });
    this.loggerCache.set(key, logger);
    return logger;
  }
  isWorkflowSync(workflow) {
    const trigger = this.triggers.get(workflow.type);
    if (!trigger) {
      throw new Error(`invalid trigger type ${workflow.type} of workflow ${workflow.id}`);
    }
    return trigger.sync ?? workflow.sync;
  }
  registerTrigger(type, trigger) {
    if (typeof trigger === "function") {
      this.triggers.register(type, new trigger(this));
    } else if (trigger) {
      this.triggers.register(type, trigger);
    } else {
      throw new Error("invalid trigger type to register");
    }
  }
  registerInstruction(type, instruction) {
    if (typeof instruction === "function") {
      this.instructions.register(type, new instruction(this));
    } else if (instruction) {
      this.instructions.register(type, instruction);
    } else {
      throw new Error("invalid instruction type to register");
    }
  }
  initTriggers(more = {}) {
    this.registerTrigger("collection", import_CollectionTrigger.default);
    this.registerTrigger("schedule", import_ScheduleTrigger.default);
    for (const [name, trigger] of Object.entries(more)) {
      this.registerTrigger(name, trigger);
    }
  }
  initInstructions(more = {}) {
    this.registerInstruction("calculation", import_CalculationInstruction.default);
    this.registerInstruction("condition", import_ConditionInstruction.default);
    this.registerInstruction("end", import_EndInstruction.default);
    this.registerInstruction("create", import_CreateInstruction.default);
    this.registerInstruction("updateorcreate", import_UpdateOrCreateInstruction.default);
    this.registerInstruction("destroy", import_DestroyInstruction.default);
    this.registerInstruction("query", import_QueryInstruction.default);
    this.registerInstruction("update", import_UpdateInstruction.default);
    for (const [name, instruction] of Object.entries({ ...more })) {
      this.registerInstruction(name, instruction);
    }
  }
  async load() {
    const { db, options } = this;
    (0, import_actions.default)(this);
    this.initTriggers(options.triggers);
    this.initInstructions(options.instructions);
    (0, import_functions.default)(this, options.functions);
    this.loggerCache = new import_lru_cache.LRUCache({
      max: 20,
      updateAgeOnGet: true,
      dispose(logger) {
        logger.end();
      }
    });
    this.app.acl.registerSnippet({
      name: `pm.${this.name}.workflows`,
      actions: [
        "workflows:*",
        "workflows.nodes:*",
        "executions:list",
        "executions:get",
        "executions:cancel",
        "executions:retry",
        "flow_nodes:update",
        "flow_nodes:destroy",
        "flow_nodes:moveUp",
        "flow_nodes:moveDown",
        "workflowCategories:*"
      ]
    });
    this.app.acl.registerSnippet({
      name: "ui.*",
      actions: ["workflows:list"]
    });
    this.app.acl.allow("workflows", ["trigger", "list"], "loggedIn");
    db.on("workflows.beforeSave", this.onBeforeSave);
    db.on("workflows.afterSave", (model) => this.toggle(model));
    db.on("workflows.afterDestroy", (model) => this.toggle(model, false));
    this.app.on("beforeStart", async () => {
      const collection = db.getCollection("workflows");
      const workflows = await collection.repository.find({
        filter: { enabled: true }
      });
      workflows.forEach((workflow) => {
        this.toggle(workflow);
      });
    });
    this.app.on("afterStart", () => {
      this.app.setMaintainingMessage("check for not started executions");
      this.ready = true;
      this.dispatch();
    });
    this.app.on("beforeStop", async () => {
      const repository = db.getRepository("workflows");
      const workflows = await repository.find({
        filter: { enabled: true }
      });
      workflows.forEach((workflow) => {
        this.toggle(workflow, false);
      });
      this.ready = false;
      if (this.events.length) {
        await this.prepare();
      }
      if (this.executing) {
        await this.executing;
      }
    });
    this.db.on("webhooks.afterCreate", this.removeWebhooksCache.bind(this));
    this.db.on("webhooks.afterUpdate", this.removeWebhooksCache.bind(this));
    this.db.on("webhooks.afterDestroy", this.removeWebhooksCache.bind(this));
  }
  async removeWebhooksCache() {
    this.app.cache.del("webhooks");
  }
  toggle(workflow, enable) {
    const type = workflow.get("type");
    const trigger = this.triggers.get(type);
    if (!trigger) {
      this.getLogger(workflow.id).error(`trigger type ${workflow.type} of workflow ${workflow.id} is not implemented`);
      return;
    }
    if (enable ?? workflow.get("enabled")) {
      const prev = workflow.previous();
      if (prev.config) {
        trigger.off({ ...workflow.get(), ...prev });
      }
      trigger.on(workflow);
      this.enabledCache.set(workflow.id, workflow);
    } else {
      trigger.off(workflow);
      this.enabledCache.delete(workflow.id);
    }
  }
  trigger(workflow, context, options = {}) {
    const logger = this.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;
    }
    if (context == null) {
      logger.warn(`workflow ${workflow.id} event data context is null, event will be ignored`);
      return;
    }
    if (this.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 });
    if (this.events.length > 1) {
      return;
    }
    setTimeout(this.prepare);
  }
  async triggerSync(workflow, context, options = {}) {
    let execution;
    try {
      execution = await this.createExecution(workflow, context, options);
    } catch (err) {
      this.getLogger(workflow.id).error(`creating execution failed: ${err.message}`, err);
      return null;
    }
    try {
      return this.process(execution, null, options);
    } catch (err) {
      this.getLogger(execution.workflowId).error(`execution (${execution.id}) error: ${err.message}`, err);
    }
    return null;
  }
  async resume(job) {
    if (!job.execution) {
      job.execution = await job.getExecution();
    }
    this.getLogger(job.execution.workflowId).info(
      `execution (${job.execution.id}) resuming from job (${job.id}) added to pending list`
    );
    this.pending.push([job.execution, job]);
    this.dispatch();
  }
  createProcessor(execution, options = {}) {
    return new import_Processor.default(execution, { ...options, plugin: this });
  }
  async createExecution(workflow, context, options) {
    const { transaction = await this.db.sequelize.transaction() } = options;
    const sameTransaction = options.transaction === transaction;
    const trigger = this.triggers.get(workflow.type);
    const valid = await trigger.validateEvent(workflow, context, { ...options, transaction });
    if (!valid) {
      if (!sameTransaction) {
        await transaction.commit();
      }
      return null;
    }
    let execution;
    try {
      execution = await workflow.createExecution(
        {
          context,
          key: workflow.key,
          status: import_constants.EXECUTION_STATUS.QUEUEING,
          parentNode: options.parentNode || null,
          parentId: options.parent ? options.parent.id : null
        },
        { transaction }
      );
    } catch (err) {
      if (!sameTransaction) {
        await transaction.rollback();
      }
      throw err;
    }
    this.getLogger(workflow.id).info(`execution of workflow ${workflow.id} created as ${execution.id}`);
    await workflow.increment(["executed", "allExecuted"], { transaction });
    if (this.db.options.dialect !== "postgres") {
      await workflow.reload({ transaction });
    }
    await workflow.constructor.update(
      {
        allExecuted: workflow.allExecuted
      },
      {
        where: {
          key: workflow.key
        },
        transaction
      }
    );
    if (!sameTransaction) {
      await transaction.commit();
    }
    execution.workflow = workflow;
    return execution;
  }
  dispatch() {
    if (!this.ready) {
      this.getLogger("dispatcher").warn(`app is not ready, new dispatching will be ignored`);
      return;
    }
    if (this.executing) {
      this.getLogger("dispatcher").warn(`workflow executing is not finished, new dispatching will be ignored`);
      return;
    }
    if (this.events.length) {
      return this.prepare();
    }
    this.executing = (async () => {
      let next = null;
      if (this.pending.length) {
        next = this.pending.shift();
        this.getLogger(next[0].workflowId).info(`pending execution (${next[0].id}) ready to process`);
      } else {
        const execution = await this.db.getRepository("executions").findOne({
          filter: {
            status: import_constants.EXECUTION_STATUS.QUEUEING,
            "workflow.enabled": true,
            "workflow.id": {
              [import_database.Op.not]: null
            }
          },
          appends: ["workflow"],
          sort: "createdAt"
        });
        if (execution) {
          this.getLogger(execution.workflowId).info(`execution (${execution.id}) fetched from db`);
          next = [execution];
        }
      }
      if (next) {
        await this.process(...next);
      }
      this.executing = null;
      if (next) {
        this.dispatch();
      }
    })();
  }
  async process(execution, job, options = {}) {
    if (execution.status === import_constants.EXECUTION_STATUS.QUEUEING) {
      await execution.update({ status: import_constants.EXECUTION_STATUS.STARTED }, { transaction: options.transaction });
    }
    const logger = this.getLogger(execution.workflowId);
    const processor = this.createProcessor(execution, options);
    logger.info(`execution (${execution.id}) ${job ? "resuming" : "starting"}...`);
    try {
      await (job ? processor.resume(job) : processor.start());
      logger.info(`execution (${execution.id}) finished with status: ${execution.status}`, { execution });
      if (execution.status !== 0) {
        const executionDuration = execution.updatedAt.getTime() - execution.createdAt.getTime();
        await execution.update({ executionCost: executionDuration }, { transaction: options.transaction });
      }
      if (execution.status && execution.parentNode) {
        const { database } = execution.constructor;
        const { model } = database.getCollection("executions");
        const parent = await model.findByPk(execution.parentId, options);
        const jobs = await parent.getJobs();
        const job2 = jobs.find((v) => v.status === import_constants.JOB_STATUS.PENDING && v.nodeId === execution.parentNode);
        if (job2) {
          const lastSavedJob = processor.lastSavedJob;
          job2.status = execution.status;
          job2.result = lastSavedJob.result || execution.context;
          await this.resume(job2);
        } else {
          logger.error(`execution (${execution.id}) error: parent job not found`);
        }
      }
    } catch (err) {
      logger.error(`execution (${execution.id}) error: ${err.message}`, err);
    }
    return processor;
  }
  useDataSourceTransaction(dataSourceName = "main", transaction, create = false) {
    const { db } = this.app.dataSourceManager.dataSources.get(dataSourceName).collectionManager;
    if (!db) {
      return;
    }
    if (db.sequelize === (transaction == null ? void 0 : transaction.sequelize)) {
      return transaction;
    }
    if (create) {
      return db.sequelize.transaction();
    }
  }
}
