(function (root, factory) {
  "use strict";

  const api = Object.freeze(factory());
  if (typeof module === "object" && module.exports) module.exports = api;
  if (root) root.MbnOperatorWorkflow = api;
})(typeof globalThis === "object" ? globalThis : this, function () {
  "use strict";

  const DEFAULT_FADE_DURATION = 6;
  const MAX_TEXT_LENGTH = 128;
  const identifierPattern = /^[A-Za-z0-9_-]{1,128}$/;
  const stockCodePattern = /^[A-Za-z0-9]{1,32}$/;
  const controlCharacterPattern = /[\u0000-\u001f\u007f]/;
  const runtimeStockAsset = Object.freeze({
    relativePath: "bin/Debug/Res/종목.ini",
    sha256: "45DFB1804828F0E4A45F73D681AF0709CE5662872FAA49CFC9F7A0B940409E20"
  });

  function freezeCut(cut) {
    return Object.freeze({
      ...cut,
      section: cut.group,
      alias: cut.aliases[0],
      cutCode: cut.aliases[0],
      aliases: Object.freeze([...cut.aliases])
    });
  }

  // bin/Debug/Res/종목.ini (SHA-256 45DFB180...940409E20), in runtime order.
  // The two '-' separator rows are deliberately absent. The source RES copy differs
  // at the after-hours label, so the operator-visible runtime asset is authoritative.
  const stockCuts = Object.freeze([
    freezeCut({ id: "basic-expected", label: "1열판기본_예상체결가", group: "1열판 기본", detail: "예상체결가", builderKey: "s5001", aliases: ["5001"], kind: "basic", subtype: "EXPECTED_OPENING" }),
    freezeCut({ id: "basic-current", label: "1열판기본_현재가", group: "1열판 기본", detail: "현재가", builderKey: "s5001", aliases: ["5001", "N5001"], kind: "basic", subtype: "CURRENT" }),
    freezeCut({ id: "basic-after-hours", label: "1열판기본_시간외단일가", group: "1열판 기본", detail: "시간외단일가", builderKey: "s5001", aliases: ["5001"], kind: "basic", subtype: "AFTER_HOURS_SINGLE_PRICE" }),

    freezeCut({ id: "detail-open", label: "1열판상세_시가", group: "1열판 상세", detail: "시가", builderKey: "s5006", aliases: ["5006"], kind: "open-detail", subtype: "시가" }),
    freezeCut({ id: "detail-face-value", label: "1열판상세_액면가", group: "1열판 상세", detail: "액면가", builderKey: "s5011", aliases: ["5011"], kind: "detail", subtype: "FaceValue" }),
    freezeCut({ id: "detail-pbr", label: "1열판상세_PBR", group: "1열판 상세", detail: "PBR", builderKey: "s5011", aliases: ["5011"], kind: "detail", subtype: "Valuation" }),
    freezeCut({ id: "detail-volume", label: "1열판상세_거래량", group: "1열판 상세", detail: "거래량", builderKey: "s5011", aliases: ["5011"], kind: "detail", subtype: "Volume" }),

    freezeCut({ id: "yield-5d", label: "수익률그래프_5일", group: "수익률 그래프", detail: "5일", builderKey: "s5086", aliases: ["5086"], kind: "yield", subtype: "5일" }),
    freezeCut({ id: "yield-20d", label: "수익률그래프_20일", group: "수익률 그래프", detail: "20일", builderKey: "s5086", aliases: ["5086"], kind: "yield", subtype: "20일" }),
    freezeCut({ id: "yield-60d", label: "수익률그래프_60일", group: "수익률 그래프", detail: "60일", builderKey: "s5086", aliases: ["5086"], kind: "yield", subtype: "60일" }),
    freezeCut({ id: "yield-120d", label: "수익률그래프_120일", group: "수익률 그래프", detail: "120일", builderKey: "s5086", aliases: ["5086"], kind: "yield", subtype: "120일" }),
    freezeCut({ id: "yield-240d", label: "수익률그래프_240일", group: "수익률 그래프", detail: "240일", builderKey: "s5086", aliases: ["5086"], kind: "yield", subtype: "240일" }),

    freezeCut({ id: "candle-daily", label: "캔들그래프_일봉", group: "캔들 그래프", detail: "일봉", builderKey: "s8010", aliases: ["8035"], kind: "candle", subtype: "PRICE" }),
    freezeCut({ id: "candle-5d", label: "캔들그래프_5일", group: "캔들 그래프", detail: "5일", builderKey: "s8010", aliases: ["8061"], kind: "candle", subtype: "PRICE" }),
    freezeCut({ id: "candle-20d", label: "캔들그래프_20일", group: "캔들 그래프", detail: "20일", builderKey: "s8010", aliases: ["8040"], kind: "candle", subtype: "PRICE" }),
    freezeCut({ id: "candle-60d", label: "캔들그래프_60일", group: "캔들 그래프", detail: "60일", builderKey: "s8010", aliases: ["8046"], kind: "candle", subtype: "PRICE" }),
    freezeCut({ id: "candle-120d", label: "캔들그래프_120일", group: "캔들 그래프", detail: "120일", builderKey: "s8010", aliases: ["8051"], kind: "candle", subtype: "PRICE" }),
    freezeCut({ id: "candle-240d", label: "캔들그래프_240일", group: "캔들 그래프", detail: "240일", builderKey: "s8010", aliases: ["8056"], kind: "candle", subtype: "PRICE" }),

    freezeCut({ id: "candle-volume-daily", label: "캔들그래프(거래량)_일봉", group: "캔들 그래프 · 거래량", detail: "일봉", builderKey: "s8010", aliases: ["8035"], kind: "candle", subtype: "VOLUME" }),
    freezeCut({ id: "candle-volume-5d", label: "캔들그래프(거래량)_5일", group: "캔들 그래프 · 거래량", detail: "5일", builderKey: "s8010", aliases: ["8061"], kind: "candle", subtype: "VOLUME" }),
    freezeCut({ id: "candle-volume-20d", label: "캔들그래프(거래량)_20일", group: "캔들 그래프 · 거래량", detail: "20일", builderKey: "s8010", aliases: ["8040"], kind: "candle", subtype: "VOLUME" }),
    freezeCut({ id: "candle-volume-60d", label: "캔들그래프(거래량)_60일", group: "캔들 그래프 · 거래량", detail: "60일", builderKey: "s8010", aliases: ["8046"], kind: "candle", subtype: "VOLUME" }),
    freezeCut({ id: "candle-volume-120d", label: "캔들그래프(거래량)_120일", group: "캔들 그래프 · 거래량", detail: "120일", builderKey: "s8010", aliases: ["8051"], kind: "candle", subtype: "VOLUME" }),
    freezeCut({ id: "candle-volume-240d", label: "캔들그래프(거래량)_240일", group: "캔들 그래프 · 거래량", detail: "240일", builderKey: "s8010", aliases: ["8056"], kind: "candle", subtype: "VOLUME" }),

    freezeCut({ id: "candle-expected-5d", label: "캔들그래프(예상체결가)_5일", group: "캔들 그래프 · 예상체결가", detail: "5일", builderKey: "s8010", aliases: ["8061"], kind: "candle", subtype: "EXPECTED" }),
    freezeCut({ id: "candle-expected-20d", label: "캔들그래프(예상체결가)_20일", group: "캔들 그래프 · 예상체결가", detail: "20일", builderKey: "s8010", aliases: ["8040"], kind: "candle", subtype: "EXPECTED" }),
    freezeCut({ id: "candle-expected-60d", label: "캔들그래프(예상체결가)_60일", group: "캔들 그래프 · 예상체결가", detail: "60일", builderKey: "s8010", aliases: ["8046"], kind: "candle", subtype: "EXPECTED" }),
    freezeCut({ id: "candle-expected-120d", label: "캔들그래프(예상체결가)_120일", group: "캔들 그래프 · 예상체결가", detail: "120일", builderKey: "s8010", aliases: ["8051"], kind: "candle", subtype: "EXPECTED" }),
    freezeCut({ id: "candle-expected-240d", label: "캔들그래프(예상체결가)_240일", group: "캔들 그래프 · 예상체결가", detail: "240일", builderKey: "s8010", aliases: ["8056"], kind: "candle", subtype: "EXPECTED" }),

    freezeCut({ id: "order-book", label: "호가창_표그래프", group: "표 그래프", detail: "호가창", builderKey: "s8003", aliases: ["8003"], kind: "order-book", subtype: "TABLE_GRAPH" }),
    freezeCut({ id: "traders", label: "거래원_표그래프", group: "표 그래프", detail: "거래원", builderKey: "s5037", aliases: ["5037"], kind: "traders", subtype: "TABLE_GRAPH" })
  ]);

  if (stockCuts.length !== 31 || new Set(stockCuts.map(cut => cut.id)).size !== 31) {
    throw new Error("The legacy stock workflow must contain exactly 31 unique cut variants.");
  }

  function manualAction(id, label, builderKey, alias, table, prerequisite) {
    return Object.freeze({
      id,
      label,
      builderKey,
      alias,
      cutCode: alias,
      aliases: Object.freeze([alias]),
      available: false,
      autoAdd: false,
      status: "unavailable",
      prerequisiteTable: table,
      prerequisites: Object.freeze([
        `선택 종목에 대한 ${table} 수동 입력 데이터`,
        prerequisite,
        "수동 데이터 검증과 명시적 플레이리스트 추가 흐름"
      ])
    });
  }

  // These buttons opened GraphE/INPUT_* manual-data workflows in the source program.
  // A stock search result alone is not sufficient input, so they must never auto-add a cut.
  const manualStockActions = Object.freeze([
    manualAction("manual-revenue-mix", "주요매출 구성", "s5076", "5076", "INPUT_PIE", "구성 항목과 비중 및 기준일 입력"),
    manualAction("manual-growth-metrics", "성장성 지표", "s5079", "5079", "INPUT_GROW", "성장성 지표 값과 분기 입력"),
    manualAction("manual-sales", "매출액", "s5080", "5080", "INPUT_SELL", "기간별 매출액 수동 값 입력"),
    manualAction("manual-operating-profit", "영업이익", "s5081", "5081", "INPUT_PROFIT", "기간별 영업이익 수동 값 입력")
  ]);

  const cutsById = new Map(stockCuts.map(cut => [cut.id, cut]));

  function valueFrom(raw, names) {
    for (const name of names) {
      if (Object.prototype.hasOwnProperty.call(raw, name) && raw[name] !== undefined && raw[name] !== null) {
        return raw[name];
      }
    }

    const keys = Object.keys(raw);
    for (const name of names) {
      const key = keys.find(candidate => candidate.toLocaleUpperCase("en-US") === name.toLocaleUpperCase("en-US"));
      if (key !== undefined && raw[key] !== undefined && raw[key] !== null) return raw[key];
    }
    return undefined;
  }

  function cleanText(value) {
    if (typeof value !== "string") return null;
    const text = value.trim();
    return text.length > 0 && text.length <= MAX_TEXT_LENGTH && !controlCharacterPattern.test(text)
      ? text
      : null;
  }

  function normalizeMarket(value) {
    const text = String(value ?? "")
      .trim()
      .toLocaleUpperCase("en-US")
      .replace(/[\s-]+/g, "_");
    const isNxt = text.includes("NXT");
    if (["KOSPI", "KOSPI_STOCK", "코스피", "코스피종목", "NXT_KOSPI", "KOSPI_NXT", "코스피_NXT", "NXT코스피"].includes(text)) {
      return { market: "kospi", isNxt };
    }
    if (["KOSDAQ", "KOSDAQ_STOCK", "코스닥", "코스닥종목", "NXT_KOSDAQ", "KOSDAQ_NXT", "코스닥_NXT", "NXT코스닥"].includes(text)) {
      return { market: "kosdaq", isNxt };
    }
    return null;
  }

  function normalizeBoolean(value) {
    if (value === true || value === false) return value;
    if (value === 1 || value === "1") return true;
    if (value === 0 || value === "0") return false;
    return null;
  }

  function validateStockResult(raw) {
    if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
      return { valid: false, error: "stock-object-required", value: null };
    }

    const marketValue = valueFrom(raw, ["market", "MARKET", "marketName", "MARKET_NAME", "groupCode", "GROUP_CODE"]);
    const normalizedMarket = normalizeMarket(marketValue);
    if (!normalizedMarket) return { valid: false, error: "unsupported-market", value: null };

    const rawStockName = cleanText(valueFrom(raw, [
      "stockName", "STOCK_NAME", "F_STOCK_NAME", "F_STOCK_WANNAME", "F_KNAM", "F_INPUT_NAME", "NAME", "WANNAME"
    ]));
    const rawDisplayName = cleanText(valueFrom(raw, [
      "displayName", "DISPLAY_NAME", "F_STOCK_WANNAME", "F_STOCK_NAME", "F_KNAM", "F_INPUT_NAME", "NAME", "WANNAME"
    ]));
    if (!rawStockName && !rawDisplayName) {
      return { valid: false, error: "stock-name-required", value: null };
    }

    const rawCode = valueFrom(raw, [
      "stockCode", "STOCK_CODE", "F_STOCK_CODE", "F_SYMB", "SYMB", "F_PART_CODE", "CODE"
    ]);
    const stockCode = typeof rawCode === "number" && Number.isSafeInteger(rawCode) && rawCode >= 0
      ? String(rawCode)
      : cleanText(rawCode);
    if (!stockCode || !stockCodePattern.test(stockCode)) {
      return { valid: false, error: "invalid-stock-code", value: null };
    }

    const explicitNxtValue = valueFrom(raw, ["isNxt", "IS_NXT", "nxt", "NXT"]);
    const explicitNxt = explicitNxtValue === undefined ? null : normalizeBoolean(explicitNxtValue);
    if (explicitNxtValue !== undefined && explicitNxt === null) {
      return { valid: false, error: "invalid-nxt-flag", value: null };
    }

    const nameImpliesNxt = `${rawStockName || ""} ${rawDisplayName || ""}`.includes("(NXT)");
    const inferredNxt = normalizedMarket.isNxt || nameImpliesNxt;
    if (explicitNxt === false && inferredNxt) {
      return { valid: false, error: "inconsistent-nxt-selection", value: null };
    }
    const isNxt = explicitNxt === true || inferredNxt;

    const withoutNxtSuffix = value => value.replace(/\s*\(NXT\)\s*/g, "").trim();
    const stockName = cleanText(withoutNxtSuffix(rawStockName || rawDisplayName));
    if (!stockName) return { valid: false, error: "stock-name-required", value: null };
    const baseDisplayName = cleanText(rawDisplayName || rawStockName);
    if (!baseDisplayName) return { valid: false, error: "display-name-required", value: null };
    const displayName = isNxt
      ? `${withoutNxtSuffix(baseDisplayName)}(NXT)`
      : baseDisplayName;

    return {
      valid: true,
      error: null,
      value: Object.freeze({
        market: normalizedMarket.market,
        stockName,
        displayName,
        stockCode,
        isNxt
      })
    };
  }

  function normalizeStockResult(raw) {
    const validation = validateStockResult(raw);
    return validation.valid ? validation.value : null;
  }

  function getStockCut(cutId) {
    return typeof cutId === "string" ? cutsById.get(cutId) || null : null;
  }

  function isStockCutAllowed(stock, cutId) {
    const normalized = normalizeStockResult(stock);
    const cut = getStockCut(cutId);
    return Boolean(normalized && cut && (!normalized.isNxt || cut.id === "basic-current"));
  }

  function requireOptions(options) {
    if (!options || typeof options !== "object" || Array.isArray(options)) {
      throw new TypeError("Stock playlist options are required.");
    }
    const id = cleanText(options.id);
    if (!id || !identifierPattern.test(id)) {
      throw new TypeError("A safe stock playlist entry id is required.");
    }

    const ma5 = options.ma5 === undefined ? false : normalizeBoolean(options.ma5);
    const ma20 = options.ma20 === undefined ? false : normalizeBoolean(options.ma20);
    if (ma5 === null || ma20 === null) {
      throw new TypeError("Moving-average flags must be boolean values.");
    }

    const fadeDuration = options.fadeDuration === undefined
      ? DEFAULT_FADE_DURATION
      : Number(options.fadeDuration);
    if (!Number.isInteger(fadeDuration) || fadeDuration < 0 || fadeDuration > 60) {
      throw new RangeError("Fade duration must be an integer from 0 through 60.");
    }
    return { id, ma5, ma20, fadeDuration };
  }

  function marketGroup(stock, stockSuffix) {
    const prefix = stock.isNxt ? "NXT_" : "";
    return `${prefix}${stock.market.toLocaleUpperCase("en-US")}${stockSuffix}`;
  }

  function selectionFor(stock, cut, movingAverages) {
    const common = {
      subject: stock.displayName,
      dataCode: stock.stockCode
    };
    switch (cut.kind) {
      case "basic":
        return {
          groupCode: marketGroup(stock, ""),
          ...common,
          graphicType: "1열판기본",
          subtype: cut.subtype
        };
      case "open-detail":
        return {
          groupCode: marketGroup(stock, "_STOCK"),
          ...common,
          graphicType: "1열판상세",
          subtype: cut.subtype
        };
      case "detail":
        return {
          groupCode: marketGroup(stock, "_STOCK"),
          ...common,
          graphicType: "1열판상세",
          subtype: cut.subtype
        };
      case "yield":
        return {
          groupCode: marketGroup(stock, ""),
          ...common,
          graphicType: "수익률그래프",
          subtype: cut.subtype
        };
      case "candle": {
        const flags = [];
        if (movingAverages.ma5) flags.push("MA5");
        if (movingAverages.ma20) flags.push("MA20");
        return {
          groupCode: marketGroup(stock, "_STOCK"),
          ...common,
          graphicType: flags.join(","),
          subtype: cut.subtype
        };
      }
      case "order-book":
        return {
          groupCode: marketGroup(stock, ""),
          ...common,
          graphicType: "ORDER_BOOK",
          subtype: "TABLE_GRAPH"
        };
      case "traders":
        return {
          groupCode: marketGroup(stock, ""),
          ...common,
          graphicType: "TRADER",
          subtype: "TABLE_GRAPH"
        };
      default:
        throw new Error("The stock cut mapping is unavailable.");
    }
  }

  function createStockPlaylistEntry(stock, cutId, options) {
    const normalized = normalizeStockResult(stock);
    if (!normalized) throw new TypeError("A valid selected stock is required.");
    const cut = getStockCut(cutId);
    if (!cut) throw new RangeError("The selected stock cut is unknown.");
    if (!isStockCutAllowed(normalized, cut.id)) {
      throw new RangeError("NXT stocks support only 1열판기본_현재가.");
    }

    const normalizedOptions = requireOptions(options);
    const movingAverages = Object.freeze({
      ma5: cut.kind === "candle" && normalizedOptions.ma5,
      ma20: cut.kind === "candle" && normalizedOptions.ma20
    });
    const selection = Object.freeze(selectionFor(normalized, cut, movingAverages));
    const code = normalized.isNxt ? "N5001" : cut.aliases[0];
    const operator = Object.freeze({
      source: "legacy-stock-workflow",
      cutId: cut.id,
      legacyLabel: cut.label,
      legacyGroup: cut.group,
      legacyDetail: cut.detail,
      stock: normalized,
      movingAverages
    });

    return {
      id: normalizedOptions.id,
      builderKey: cut.builderKey,
      code,
      aliases: [...cut.aliases],
      title: normalized.displayName,
      detail: cut.label,
      category: "stock",
      market: normalized.market,
      stockName: normalized.stockName,
      displayName: normalized.displayName,
      stockCode: normalized.stockCode,
      isNxt: normalized.isNxt,
      selection,
      enabled: true,
      fadeDuration: normalizedOptions.fadeDuration,
      graphicType: selection.graphicType,
      subtype: selection.subtype,
      operator
    };
  }

  function sameStringArray(left, right) {
    return Array.isArray(left) && Array.isArray(right) &&
      left.length === right.length && left.every((value, index) => value === right[index]);
  }

  function sameSelection(left, right) {
    if (!left || typeof left !== "object" || !right || typeof right !== "object") return false;
    return ["groupCode", "subject", "graphicType", "subtype", "dataCode"]
      .every(key => typeof left[key] === "string" && left[key] === right[key]);
  }

  function restoreStockPlaylistEntry(value) {
    if (!value || typeof value !== "object" || Array.isArray(value) ||
        typeof value.enabled !== "boolean") {
      return null;
    }
    const operator = value.operator;
    if (!operator || typeof operator !== "object" ||
        operator.source !== "legacy-stock-workflow" ||
        typeof operator.cutId !== "string" ||
        !operator.movingAverages || typeof operator.movingAverages !== "object" ||
        typeof operator.movingAverages.ma5 !== "boolean" ||
        typeof operator.movingAverages.ma20 !== "boolean") {
      return null;
    }

    let recreated;
    try {
      recreated = createStockPlaylistEntry(operator.stock, operator.cutId, {
        id: value.id,
        ma5: operator.movingAverages.ma5,
        ma20: operator.movingAverages.ma20,
        fadeDuration: value.fadeDuration
      });
    } catch {
      return null;
    }

    const trustedScalarKeys = [
      "id", "builderKey", "code", "title", "detail", "category", "market",
      "stockName", "displayName", "stockCode", "isNxt", "fadeDuration",
      "graphicType", "subtype"
    ];
    if (!trustedScalarKeys.every(key => value[key] === recreated[key]) ||
        !sameStringArray(value.aliases, recreated.aliases) ||
        !sameSelection(value.selection, recreated.selection) ||
        operator.legacyLabel !== recreated.operator.legacyLabel ||
        operator.legacyGroup !== recreated.operator.legacyGroup ||
        operator.legacyDetail !== recreated.operator.legacyDetail) {
      return null;
    }

    return { ...recreated, enabled: value.enabled };
  }

  return {
    runtimeStockAsset,
    stockCuts,
    manualStockActions,
    normalizeMarket,
    validateStockResult,
    normalizeStockResult,
    getStockCut,
    isStockCutAllowed,
    createStockPlaylistEntry,
    restoreStockPlaylistEntry
  };
});
