'use strict';

var crypto = require('crypto');
require('fs');
var path3 = require('path');
require('child_process');
require('util');

function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }

var path3__default = /*#__PURE__*/_interopDefault(path3);

var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);

// src/platform/core/cancellation.ts
var USER_CANCELLED_ERROR_CODE = "JUHONG_E_USER_CANCELLED";
function hasCancellationErrorCode(error, code) {
  return Boolean(
    error && typeof error === "object" && "code" in error && error.code === code
  );
}
function isUserCancelledError(error) {
  return hasCancellationErrorCode(error, USER_CANCELLED_ERROR_CODE);
}

// src/platform/core/timestamp-helpers.ts
function padTimestampPart(value) {
  return String(value).padStart(2, "0");
}
function getTimestampParts(now = /* @__PURE__ */ new Date()) {
  return {
    year: now.getFullYear(),
    month: padTimestampPart(now.getMonth() + 1),
    day: padTimestampPart(now.getDate()),
    hour: padTimestampPart(now.getHours()),
    minute: padTimestampPart(now.getMinutes()),
    second: padTimestampPart(now.getSeconds())
  };
}
function getHumanReadableNow(now = /* @__PURE__ */ new Date()) {
  const { year, month, day, hour, minute, second } = getTimestampParts(now);
  return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
}
var DEFAULT_GEN_HTML_COMPONENT_SLUG = "component";
function normalizeGenHtmlComponentName(componentName) {
  return componentName.trim();
}
function slugifyGenHtmlComponentName(componentName) {
  const slug = normalizeGenHtmlComponentName(componentName).toLowerCase().replace(/['"`]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
  return slug || DEFAULT_GEN_HTML_COMPONENT_SLUG;
}
function createGenHtmlOutputPath(componentName, uniqueToken = Date.now()) {
  return path3__default.default.join(".ju-hong-e", "local-server", "generated", `${slugifyGenHtmlComponentName(componentName)}-${uniqueToken}`);
}

// src/features/gen-html-multiple/domain/scheduler-state.ts
var DEFAULT_GEN_HTML_MULTIPLE_CONCURRENCY = 1;
var MAX_GEN_HTML_MULTIPLE_CONCURRENCY = 6;
function normalizeDesiredConcurrency(value, fallback = DEFAULT_GEN_HTML_MULTIPLE_CONCURRENCY) {
  const parsed = Number(value);
  if (!Number.isFinite(parsed)) {
    return fallback;
  }
  return Math.max(1, Math.min(MAX_GEN_HTML_MULTIPLE_CONCURRENCY, Math.floor(parsed)));
}
function summarizeMultipleComponentStates(components) {
  const queueCount = components.filter((component) => component.status === "queued").length;
  const runningCount = components.filter((component) => component.status === "running" || component.status === "refreshing").length;
  const completedCount = components.filter((component) => component.status === "completed").length;
  const failedCount = components.filter((component) => component.status === "failed").length;
  const cancelledCount = components.filter((component) => component.status === "cancelled").length;
  return {
    cancelledCount,
    completedCount,
    failedCount,
    queueCount,
    runningCount
  };
}
function areAllComponentsSettled(components) {
  return components.length > 0 && components.every(
    (component) => component.status === "completed" || component.status === "failed" || component.status === "cancelled"
  );
}

// src/features/gen-html-multiple/infrastructure/scheduler.ts
function createMultipleOutputPathToken(sequence, requestId) {
  return `${Date.now()}-${sequence + 1}-${requestId.slice(0, 8)}`;
}
function createQueuedComponentState(component, sequence) {
  const id = crypto.randomUUID();
  const outputPath = createGenHtmlOutputPath(component.componentName, createMultipleOutputPathToken(sequence, id));
  return {
    componentName: component.componentName,
    errorMessage: "",
    errorReportPath: "",
    finishedAt: "",
    generatedFiles: [],
    historyPath: "",
    id,
    log: "",
    outputPath,
    previewUrl: "",
    queuedAt: getHumanReadableNow(),
    references: component.references,
    refreshStartedAt: "",
    restartRequired: false,
    startedAt: "",
    status: "queued"
  };
}
function findNextQueuedComponent(components) {
  const index = components.findIndex((component2) => component2.status === "queued");
  if (index < 0) {
    return void 0;
  }
  const component = components[index];
  return component ? { component, index } : void 0;
}
var GenHtmlMultipleScheduler = class {
  constructor(options) {
    __publicField(this, "activeComponentIds", /* @__PURE__ */ new Set());
    /** 실행 중인 컴포넌트별 AbortController. 개별 "취소" 요청 시 AI 자식 프로세스를 종료한다. */
    __publicField(this, "abortControllers", /* @__PURE__ */ new Map());
    /** 실행 중인 worker 의 종료 promise. 전체 종료(abort) 후 정리 완료를 기다리는 데 사용한다. */
    __publicField(this, "workerPromises", /* @__PURE__ */ new Map());
    __publicField(this, "options");
    __publicField(this, "batchPromise");
    __publicField(this, "resolveBatch");
    this.options = options;
  }
  /**
   * @description 새 batch를 시작하고 dispatch를 수행한다.
   * @param components 실행할 컴포넌트 목록
   * @param concurrency 희망 동시 실행 수
   * @returns batch 완료 promise
   */
  startBatch(components, concurrency) {
    this.options.state.overallStatus = "running";
    this.options.state.desiredConcurrency = normalizeDesiredConcurrency(concurrency);
    this.options.state.components = components.map(createQueuedComponentState);
    this.options.state.summary.batchCount += 1;
    this.options.state.summary.completedComponents = 0;
    this.options.state.summary.failedComponents = 0;
    this.options.state.restartRequired = false;
    this.recalculateState();
    this.options.trace(
      "gen-html-multiple:scheduler:batch:start",
      `components=${components.length} concurrency=${this.options.state.desiredConcurrency}`
    );
    this.batchPromise = new Promise((resolve) => {
      this.resolveBatch = resolve;
    });
    this.dispatch();
    return this.batchPromise;
  }
  /**
   * @description 실행 중 concurrency를 변경한다.
   * @param concurrency 새 concurrency
   */
  setDesiredConcurrency(concurrency) {
    const previous = this.options.state.desiredConcurrency;
    this.options.state.desiredConcurrency = normalizeDesiredConcurrency(concurrency, previous);
    this.options.trace(
      "gen-html-multiple:scheduler:concurrency:changed",
      `${previous}=>${this.options.state.desiredConcurrency}`
    );
    this.dispatch();
  }
  /**
   * @description 현재 batch 완료 promise를 반환한다.
   * @returns batch promise 또는 undefined
   */
  getBatchPromise() {
    return this.batchPromise;
  }
  /**
   * @description
   * 진행 중인 batch 를 전체 종료한다. (사용자 종료 요청 시 사용)
   * 실행 중인 worker 는 AbortController 로 중단해 AI 자식 프로세스를 종료하고(백그라운드 고아 작업 방지),
   * 대기 중인 queued 컴포넌트는 cancelled terminal 로 정리한다.
   * abort 된 worker 가 cancelled 로 반영될 때까지 기다리려면 waitForActiveWorkers() 를 함께 호출한다.
   */
  cancel() {
    if (this.options.state.overallStatus !== "running") {
      return;
    }
    const summary = summarizeMultipleComponentStates(this.options.state.components);
    this.options.trace("gen-html-multiple:scheduler:cancel", `queued=${summary.queueCount} running=${summary.runningCount}`);
    for (const controller of this.abortControllers.values()) {
      controller.abort();
    }
    for (const component of this.options.state.components) {
      if (component.status === "queued") {
        component.status = "cancelled";
        component.errorMessage = "\uC804\uCCB4 \uC885\uB8CC\uB85C \uC2DC\uC791 \uC804 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4.";
        component.finishedAt = getHumanReadableNow();
        component.log += "[cancelled] \uC804\uCCB4 \uC885\uB8CC\uB85C \uC2DC\uC791 \uC804 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4.\n";
      }
    }
    this.options.state.overallStatus = "completed";
    this.recalculateState();
    this.resolveBatch?.();
    this.resolveBatch = void 0;
    this.batchPromise = void 0;
  }
  /**
   * @description
   * 현재 실행 중인(또는 abort 직후 정리 중인) worker 들이 모두 settle 될 때까지 기다린다.
   * cancel() 직후 호출하면 abort 된 worker 가 cancelled 로 반영된 최종 상태를 보장한다.
   * @returns 모든 활성 worker 가 종료되면 resolve 되는 promise
   */
  async waitForActiveWorkers() {
    await Promise.allSettled([...this.workerPromises.values()]);
  }
  /**
   * @description
   * 개별 컴포넌트를 취소한다. (ESC와 동일한 효과)
   * 실행 중이면 AbortController로 AI 자식 프로세스를 종료하고, 대기 중이면 시작 전에 취소 처리한다.
   * @param componentId 취소할 컴포넌트 id
   * @returns 취소 요청이 적용됐는지 여부 (이미 종료된 컴포넌트면 false)
   */
  cancelComponent(componentId) {
    const component = this.options.state.components.find((candidate) => candidate.id === componentId);
    if (!component || component.status === "completed" || component.status === "failed" || component.status === "cancelled") {
      return false;
    }
    this.options.trace("gen-html-multiple:scheduler:component:cancel", `${component.componentName} status=${component.status}`);
    if (component.status === "running" || component.status === "refreshing") {
      this.abortControllers.get(componentId)?.abort();
      return true;
    }
    component.status = "cancelled";
    component.errorMessage = "\uC0AC\uC6A9\uC790\uAC00 \uC791\uC5C5\uC744 \uCDE8\uC18C\uD588\uC2B5\uB2C8\uB2E4.";
    component.finishedAt = getHumanReadableNow();
    component.log += "[cancelled] \uC2DC\uC791 \uC804 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4.\n";
    this.recalculateState();
    this.resolveIfSettled();
    return true;
  }
  dispatch() {
    this.recalculateState();
    if (this.options.state.overallStatus !== "running") {
      this.options.trace("gen-html-multiple:scheduler:dispatch:skip", "not-running");
      return;
    }
    while (this.activeComponentIds.size < this.options.state.desiredConcurrency) {
      const next = findNextQueuedComponent(this.options.state.components);
      if (!next) {
        break;
      }
      this.startWorker(next.component, next.index);
    }
    this.recalculateState();
    this.resolveIfSettled();
  }
  startWorker(component, index) {
    this.activeComponentIds.add(component.id);
    const controller = new AbortController();
    this.abortControllers.set(component.id, controller);
    component.status = "running";
    component.startedAt = getHumanReadableNow();
    component.log += `[start] ${component.componentName}
`;
    this.options.trace(
      "gen-html-multiple:scheduler:worker:start",
      `active=${this.activeComponentIds.size} component=${component.componentName}`
    );
    const workerPromise = this.options.runComponent(component, index, controller.signal).then((result) => {
      component.finishedAt = getHumanReadableNow();
      component.generatedFiles = result.generatedFiles;
      component.historyPath = result.historyPath;
      component.previewUrl = result.previewUrl;
      component.restartRequired = result.restartRequired;
      component.status = "completed";
      component.log += `[done] preview=${result.previewUrl}
`;
    }).catch((error) => {
      const cancelled = isUserCancelledError(error) || controller.signal.aborted;
      component.errorMessage = cancelled ? "\uC0AC\uC6A9\uC790\uAC00 \uC791\uC5C5\uC744 \uCDE8\uC18C\uD588\uC2B5\uB2C8\uB2E4." : error instanceof Error ? error.message : String(error);
      component.finishedAt = getHumanReadableNow();
      component.status = cancelled ? "cancelled" : "failed";
      component.log += `[${cancelled ? "cancelled" : "failed"}] ${component.errorMessage}
`;
      if (component.errorReportPath) {
        component.log += `[error-report] ${component.errorReportPath}
`;
      }
    }).finally(() => {
      this.activeComponentIds.delete(component.id);
      this.abortControllers.delete(component.id);
      this.workerPromises.delete(component.id);
      this.options.trace(
        "gen-html-multiple:scheduler:worker:end",
        `active=${this.activeComponentIds.size} component=${component.componentName} status=${component.status}`
      );
      this.dispatch();
    });
    this.workerPromises.set(component.id, workerPromise);
  }
  recalculateState() {
    const summary = summarizeMultipleComponentStates(this.options.state.components);
    this.options.state.activeCount = this.activeComponentIds.size;
    this.options.state.queueCount = summary.queueCount;
    this.options.state.runningCount = summary.runningCount;
    this.options.state.failedCount = summary.failedCount;
    this.options.state.summary.completedComponents = summary.completedCount;
    this.options.state.summary.failedComponents = summary.failedCount;
    this.options.state.restartRequired = this.options.state.components.some((component) => component.restartRequired);
  }
  resolveIfSettled() {
    if (this.options.state.overallStatus !== "running" || !areAllComponentsSettled(this.options.state.components)) {
      return;
    }
    this.options.state.overallStatus = "completed";
    this.recalculateState();
    this.options.trace(
      "gen-html-multiple:scheduler:batch:end",
      `completed=${this.options.state.summary.completedComponents} failed=${this.options.state.summary.failedComponents}`
    );
    this.resolveBatch?.();
    this.resolveBatch = void 0;
    this.batchPromise = void 0;
  }
};

exports.GenHtmlMultipleScheduler = GenHtmlMultipleScheduler;
