/**
 * 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 ExecutionTimeoutManager_exports = {};
__export(ExecutionTimeoutManager_exports, {
  default: () => ExecutionTimeoutManager
});
module.exports = __toCommonJS(ExecutionTimeoutManager_exports);
var import_database = require("@nocobase/database");
var import_constants = require("./constants");
var import_utils = require("./utils");
const LOAD_BATCH_SIZE = 1e3;
const DEFAULT_SCAN_INTERVAL = 3e4;
const SCAN_JITTER = 5e3;
const MAX_TIMER_DELAY = 2147483647;
const EXECUTION_LOCK_TIMEOUT = 6e4;
class ExecutionTimeoutManager {
  constructor(plugin) {
    this.plugin = plugin;
  }
  timers = /* @__PURE__ */ new Map();
  scanTimer = null;
  nextExpiresAtTimer = null;
  nextExpiresAt = null;
  scanning = null;
  stopped = true;
  getTimeout(execution) {
    var _a, _b;
    const timeout = Number(((_b = (_a = execution.workflow) == null ? void 0 : _a.options) == null ? void 0 : _b.timeout) ?? 0);
    return Number.isFinite(timeout) && timeout > 0 ? timeout : 0;
  }
  getExpiresAt(execution, startedAt) {
    const timeout = this.getTimeout(execution);
    return timeout > 0 ? new Date(startedAt.getTime() + timeout) : null;
  }
  async load() {
    this.stopped = false;
    await this.scanExpiredExecutions();
    await this.scheduleNextExpiresAtTimer();
    this.scheduleScan();
  }
  async unload() {
    var _a;
    this.stopped = true;
    if (this.scanTimer) {
      clearTimeout(this.scanTimer);
      this.scanTimer = null;
    }
    if (this.nextExpiresAtTimer) {
      clearTimeout(this.nextExpiresAtTimer);
      this.nextExpiresAtTimer = null;
      this.nextExpiresAt = null;
    }
    for (const timer of this.timers.values()) {
      clearTimeout(timer);
    }
    this.timers.clear();
    await ((_a = this.scanning) == null ? void 0 : _a.catch(() => {
    }));
  }
  isExpired(execution, now = new Date(Date.now())) {
    return !!execution.expiresAt && execution.expiresAt.getTime() <= now.getTime();
  }
  getRemainingMs(execution, now = new Date(Date.now())) {
    if (!execution.expiresAt) {
      return null;
    }
    return execution.expiresAt.getTime() - now.getTime();
  }
  async abort(execution, options = {}) {
    const aborted = await (0, import_utils.abortExecution)(this.plugin, execution, {
      ...options,
      reason: import_constants.EXECUTION_REASON.TIMEOUT
    });
    if (aborted) {
      this.clearExecutionTimeout(execution.id);
    }
    return aborted;
  }
  async abortExecutionIfExpired(execution, options = {}) {
    if (execution.status !== import_constants.EXECUTION_STATUS.STARTED || !this.isExpired(execution)) {
      return false;
    }
    return this.abort(execution, options);
  }
  async abortExecutionIfExpiredWithLock(executionOrId) {
    const executionId = typeof executionOrId === "object" ? executionOrId.id : executionOrId;
    const lockManager = this.plugin.app.lockManager;
    const lock = await lockManager.tryAcquire((0, import_utils.getExecutionLockKey)(executionId), EXECUTION_LOCK_TIMEOUT);
    return lock.runExclusive(async () => {
      const execution = await this.plugin.db.getRepository("executions").findOne({
        filterByTk: executionId
      });
      if (!execution) {
        return false;
      }
      return this.abortExecutionIfExpired(execution);
    }, EXECUTION_LOCK_TIMEOUT);
  }
  clear(executionId) {
    this.clearExecutionTimeout(executionId);
  }
  async shouldContinue(execution, options = {}) {
    if (execution.status !== import_constants.EXECUTION_STATUS.STARTED) {
      return false;
    }
    if (!this.isExpired(execution)) {
      return true;
    }
    await this.abortExecutionIfExpired(execution, options);
    return false;
  }
  /**
   * Owner-only per-execution timer. Only call from code paths that have acquired
   * local execution ownership, such as Dispatcher/Processor processing paths.
   */
  scheduleExecutionTimeout(execution) {
    const id = String(execution.id);
    this.clearExecutionTimeout(id);
    if (execution.status !== import_constants.EXECUTION_STATUS.STARTED || !execution.expiresAt) {
      return;
    }
    this.scheduleNextExpiresAtIfEarlier(execution.expiresAt);
    const remaining = this.getRemainingMs(execution);
    if (remaining == null) {
      return;
    }
    if (remaining <= 0) {
      setImmediate(() => {
        this.handleExecutionTimeout(id).catch((error) => {
          this.plugin.getLogger(execution.workflowId).error(`execution (${execution.id}) timeout handling failed`, {
            error
          });
        });
      });
      return;
    }
    this.timers.set(
      id,
      setTimeout(
        () => {
          if (execution.expiresAt && execution.expiresAt.getTime() > Date.now()) {
            this.scheduleExecutionTimeout(execution);
            return;
          }
          this.handleExecutionTimeout(id).catch((error) => {
            this.plugin.getLogger(execution.workflowId).error(`execution (${execution.id}) timeout handling failed`, {
              error
            });
          });
        },
        Math.min(remaining, MAX_TIMER_DELAY)
      )
    );
  }
  invalidateNextExpiresAtIfMatches(expiresAt) {
    if (!expiresAt || !this.nextExpiresAt || this.nextExpiresAt.getTime() !== expiresAt.getTime()) {
      return;
    }
    this.clearNextExpiresAtTimer();
    this.scheduleNextExpiresAtTimer().catch((error) => {
      this.plugin.getLogger("timeout").error(`workflow execution timeout one-shot refresh failed`, { error });
    });
  }
  async scanExpiredExecutions() {
    if (this.scanning) {
      return this.scanning;
    }
    if (this.stopped) {
      return;
    }
    this.scanning = (async () => {
      const ExecutionModelClass = this.plugin.db.getModel("executions");
      const executions = await ExecutionModelClass.findAll({
        attributes: ["id", "workflowId", "status", "expiresAt"],
        where: {
          status: import_constants.EXECUTION_STATUS.STARTED,
          expiresAt: {
            [import_database.Op.not]: null,
            [import_database.Op.lt]: /* @__PURE__ */ new Date()
          }
        },
        order: [
          ["expiresAt", "ASC"],
          ["id", "ASC"]
        ],
        limit: LOAD_BATCH_SIZE
      });
      for (const execution of executions) {
        await this.abortExecutionIfExpiredWithLock(execution);
      }
    })();
    try {
      await this.scanning;
    } finally {
      this.scanning = null;
    }
  }
  scheduleScan() {
    if (this.stopped) {
      return;
    }
    if (this.scanTimer) {
      clearTimeout(this.scanTimer);
    }
    this.scanTimer = setTimeout(
      async () => {
        this.scanTimer = null;
        if (this.stopped) {
          return;
        }
        try {
          await this.scanExpiredExecutions();
          this.scheduleNextExpiresAtTimer();
        } catch (error) {
          this.plugin.getLogger("timeout").error(`workflow execution timeout scan failed`, { error });
        } finally {
          this.scheduleScan();
        }
      },
      DEFAULT_SCAN_INTERVAL + Math.floor(Math.random() * SCAN_JITTER)
    );
  }
  async scheduleNextExpiresAtTimer() {
    if (this.stopped) {
      return;
    }
    const ExecutionModelClass = this.plugin.db.getModel("executions");
    const execution = await ExecutionModelClass.findOne({
      attributes: ["id", "expiresAt"],
      where: {
        status: import_constants.EXECUTION_STATUS.STARTED,
        expiresAt: {
          [import_database.Op.not]: null
        }
      },
      order: [
        ["expiresAt", "ASC"],
        ["id", "ASC"]
      ]
    });
    if (!(execution == null ? void 0 : execution.expiresAt)) {
      this.clearNextExpiresAtTimer();
      return;
    }
    this.scheduleNextExpiresAtIfEarlier(execution.expiresAt, true);
  }
  scheduleNextExpiresAtIfEarlier(expiresAt, force = false) {
    if (this.stopped) {
      return;
    }
    if (!force && this.nextExpiresAt && this.nextExpiresAt.getTime() <= expiresAt.getTime()) {
      return;
    }
    this.clearNextExpiresAtTimer();
    this.nextExpiresAt = expiresAt;
    const remaining = expiresAt.getTime() - Date.now();
    const delay = Math.max(0, Math.min(remaining, MAX_TIMER_DELAY));
    this.nextExpiresAtTimer = setTimeout(() => {
      this.handleNextExpiresAtTimeout(expiresAt).catch((error) => {
        this.plugin.getLogger("timeout").error(`workflow execution timeout one-shot failed`, { error });
      });
    }, delay);
  }
  async handleNextExpiresAtTimeout(expiresAt) {
    this.nextExpiresAtTimer = null;
    this.nextExpiresAt = null;
    if (expiresAt.getTime() > Date.now()) {
      this.scheduleNextExpiresAtIfEarlier(expiresAt, true);
      return;
    }
    await this.scanExpiredExecutions();
    await this.scheduleNextExpiresAtTimer();
  }
  clearNextExpiresAtTimer() {
    if (this.nextExpiresAtTimer) {
      clearTimeout(this.nextExpiresAtTimer);
      this.nextExpiresAtTimer = null;
    }
    this.nextExpiresAt = null;
  }
  clearExecutionTimeout(executionId) {
    const id = String(executionId);
    const timer = this.timers.get(id);
    if (!timer) {
      return;
    }
    clearTimeout(timer);
    this.timers.delete(id);
  }
  async handleExecutionTimeout(executionId) {
    this.clearExecutionTimeout(executionId);
    if (this.plugin.abortRunningExecution(executionId, import_constants.EXECUTION_REASON.TIMEOUT)) {
      return;
    }
    await this.abortExecutionIfExpiredWithLock(executionId);
  }
}
