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

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

  const DEFAULT_FADE_DURATION = 6;
  const SCHEMA_VERSION = 1;
  const PAIR_LIST_SCHEMA_VERSION = 1;
  const MAX_SAVED_PAIRS = 500;
  const identifierPattern = /^[A-Za-z0-9_-]{1,128}$/;
  const stockCodePattern = /^[A-Za-z0-9._-]{1,32}$/;
  const worldSymbolPattern = /^[A-Za-z0-9@._:-]{1,64}$/;
  const controlCharacterPattern = /[\u0000-\u001f\u007f]/;

  const runtimeAsset = Object.freeze({
    relativePath: "bin/Debug/Res/종목비교.ini",
    sha256: "980F74C000EF6A34B85FDAAF6D2756269483637186C91A93548F35C5CFC1B7E4"
  });

  const marketTargetRows = [
    ["kospi", "코스피 지수", "Kospi"],
    ["kosdaq", "코스닥 지수", "Kosdaq"],
    ["kospi200", "코스피200 지수", "Kospi200"],
    ["futures", "선물 지수", "Futures"],
    ["krx100", "KRX100지수", "Krx100"],
    ["won-dollar", "환율-원달러", "WonDollar"],
    ["won-yen", "환율-원엔", "WonYen"],
    ["won-yuan", "환율-원위엔", "WonYuan"],
    ["won-euro", "환율-원유로", "WonEuro"],
    ["dow", "해외지수-다우", "Dow"],
    ["nasdaq", "해외지수-나스닥", "Nasdaq"],
    ["sp500", "해외지수-S&P", "Sp500"],
    ["germany-dax", "해외지수-독일", "GermanyDax"],
    ["united-kingdom-ftse", "해외지수-영국", "UnitedKingdomFtse"],
    ["france-cac", "해외지수-프랑스", "FranceCac"],
    ["nikkei", "해외지수-일본", "Nikkei"],
    ["hang-seng", "해외지수-홍콩", "HangSeng"],
    ["taiwan-weighted", "해외지수-대만", "TaiwanWeighted"],
    ["singapore-straits-times", "해외지수-싱가포르", "SingaporeStraitsTimes"],
    ["thailand-set", "해외지수-태국", "ThailandSet"],
    ["philippines-composite", "해외지수-필리핀", "PhilippinesComposite"],
    ["malaysia-klse", "해외지수-말레이시아", "MalaysiaKlse"],
    ["indonesia-composite", "해외지수-인도네시아", "IndonesiaComposite"],
    ["shanghai-composite", "해외지수-중국", "ShanghaiComposite"]
  ];

  const marketTargets = Object.freeze(marketTargetRows.map(([id, displayName, target]) =>
    Object.freeze({ kind: "market-target", id, displayName, target })));
  if (marketTargets.length !== 24 || new Set(marketTargets.map(value => value.target)).size !== 24) {
    throw new Error("The legacy comparison selector must expose exactly 24 market targets.");
  }
  const marketTargetsByTarget = new Map(marketTargets.map(value => [value.target, value]));
  const marketTargetsByLookup = new Map();
  for (const target of marketTargets) {
    marketTargetsByLookup.set(target.id.toLocaleLowerCase("en-US"), target);
    marketTargetsByLookup.set(target.target.toLocaleLowerCase("en-US"), target);
    marketTargetsByLookup.set(target.displayName.toLocaleLowerCase("ko-KR"), target);
  }

  function action(id, label, kind, details = {}) {
    return Object.freeze({ id, label, kind, ...details });
  }

  const actions = Object.freeze([
    action("two-column-expected", "2열판 예상체결", "two-column", { mode: "EXPECTED" }),
    action("two-column-current", "2열판", "two-column", { mode: "CURRENT" }),
    action("comparison-candle", "종목별 비교분석_캔들 그래프", "comparison", {
      builderKey: "s5026", code: "5026", graphicType: "COMPARISON_CANDLE", period: "FiveDays"
    }),
    action("comparison-line", "종목별 비교분석_라인 그래프", "comparison", {
      builderKey: "s5087", code: "5087", graphicType: "COMPARISON_LINE", period: "FiveDays"
    }),
    action("return-5d", "5일", "return", { period: "FiveDays" }),
    action("return-1m", "1개월", "return", { period: "OneMonth" }),
    action("return-3m", "3개월", "return", { period: "ThreeMonths" }),
    action("return-6m", "6개월", "return", { period: "SixMonths" }),
    action("return-12m", "12개월", "return", { period: "TwelveMonths" })
  ]);
  if (actions.length !== 9 || new Set(actions.map(value => value.id)).size !== 9) {
    throw new Error("The legacy comparison tree must expose exactly nine actions.");
  }
  const actionsById = new Map(actions.map(value => [value.id, value]));
  const yieldActionIds = Object.freeze(actions
    .filter(value => value.kind === "return")
    .map(value => value.id));
  const expectedMarketTargets = new Set(["Kospi", "Kosdaq", "Kospi200", "Krx100"]);

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

  function getMarketTarget(value) {
    if (value && typeof value === "object" && !Array.isArray(value)) value = value.target ?? value.id ?? value.displayName;
    const text = cleanText(value);
    return text ? marketTargetsByLookup.get(text.toLocaleLowerCase("ko-KR")) ?? null : null;
  }

  function normalizeMarket(value) {
    const text = String(value || "").trim().toLocaleLowerCase("en-US");
    return text === "kospi" || text === "kosdaq" ? text : null;
  }

  function normalizeTarget(value) {
    if (!value || typeof value !== "object" || Array.isArray(value)) return null;
    switch (value.kind) {
      case "market-target": {
        const target = marketTargetsByTarget.get(value.target);
        if (!target) return null;
        if ((value.id !== undefined && value.id !== target.id) ||
            (value.displayName !== undefined && value.displayName !== target.displayName)) return null;
        return target;
      }
      case "krx-stock":
      case "nxt-stock": {
        const market = normalizeMarket(value.market);
        const stockName = cleanText(value.stockName ?? value.name);
        const stockCode = cleanText(value.stockCode ?? value.code, 32);
        if (!market || !stockName || stockName.includes(",") || stockName.includes("(NXT)") ||
            !stockCode || !stockCodePattern.test(stockCode)) return null;
        const displayName = value.kind === "nxt-stock" ? `${stockName}(NXT)` : stockName;
        if (value.displayName !== undefined && value.displayName !== displayName) return null;
        return Object.freeze({ kind: value.kind, market, stockName, stockCode, displayName });
      }
      case "world-stock": {
        const inputName = cleanText(value.inputName);
        const displayName = cleanText(value.displayName ?? value.name);
        const symbol = cleanText(value.symbol, 64);
        const nation = cleanText(value.nation, 2)?.toLocaleUpperCase("en-US");
        if (!inputName || inputName.includes(",") || !displayName || !symbol ||
            !worldSymbolPattern.test(symbol) || (nation !== "US" && nation !== "TW")) return null;
        return Object.freeze({ kind: "world-stock", inputName, displayName, symbol, nation });
      }
      default:
        return null;
    }
  }

  function targetKey(value) {
    const target = normalizeTarget(value);
    if (!target) return null;
    switch (target.kind) {
      case "market-target": return `market:${target.target}`;
      case "krx-stock": return `krx:${target.market}:${target.stockCode}`;
      case "nxt-stock": return `nxt:${target.market}:${target.stockCode}`;
      case "world-stock": return `world:${target.nation}:${target.symbol}`;
      default: return null;
    }
  }

  function subjectTokenForTarget(value) {
    const target = normalizeTarget(value);
    if (!target) return null;
    switch (target.kind) {
      case "market-target": return target.target;
      case "krx-stock": return target.stockName;
      case "nxt-stock": return `${target.stockName}(NXT)`;
      case "world-stock": return target.inputName;
      default: return null;
    }
  }

  function pairParts(value) {
    if (Array.isArray(value)) return [value[0], value[1]];
    if (value && typeof value === "object") return [value.first ?? value.pair?.[0], value.second ?? value.pair?.[1]];
    return [null, null];
  }

  function krxIdentityEnvelope(value) {
    const pair = normalizePair(value);
    if (!pair || pair.some(target => target.kind !== "krx-stock")) return "";
    return pair
      .map(target => `${target.market === "kospi" ? "KOSPI" : "KOSDAQ"}:${target.stockCode}`)
      .join("|");
  }

  function normalizePair(value) {
    const [firstValue, secondValue] = pairParts(value);
    const first = normalizeTarget(firstValue);
    const second = normalizeTarget(secondValue);
    return first && second ? Object.freeze([first, second]) : null;
  }

  function normalizePairState(value) {
    const [firstValue, secondValue] = pairParts(value);
    const first = firstValue == null ? null : normalizeTarget(firstValue);
    const second = secondValue == null ? null : normalizeTarget(secondValue);
    return (firstValue == null || first) && (secondValue == null || second)
      ? Object.freeze([first, second])
      : null;
  }

  function rotatePair(value, selectedValue) {
    const pair = normalizePairState(value);
    const selected = normalizeTarget(selectedValue);
    return pair && selected ? Object.freeze([selected, pair[0]]) : null;
  }

  function swapPair(value) {
    const pair = normalizePairState(value);
    return pair ? Object.freeze([pair[1], pair[0]]) : null;
  }

  function clearPair() {
    return Object.freeze([null, null]);
  }

  function pairLabel(value) {
    const pair = normalizePair(value);
    return pair ? `${pair[0].displayName} ↔ ${pair[1].displayName}` : "";
  }

  function unsupported(actionId, reason) {
    return Object.freeze({ actionId, supported: false, reason, effectiveMode: null, warning: null });
  }

  function supported(actionId, effectiveMode = null, warning = null) {
    return Object.freeze({ actionId, supported: true, reason: null, effectiveMode, warning });
  }

  function getCompatibility(actionId, pairValue) {
    const selectedAction = actionsById.get(actionId);
    if (!selectedAction) return unsupported(actionId, "unknown-action");
    const pair = normalizePair(pairValue);
    if (!pair) return unsupported(actionId, "pair-incomplete-or-invalid");
    const futuresCount = pair.filter(value => value.kind === "market-target" && value.target === "Futures").length;
    const hasWorld = pair.some(value => value.kind === "world-stock");

    if (selectedAction.kind === "comparison" || selectedAction.kind === "return") {
      return pair.every(value => value.kind === "krx-stock")
        ? supported(actionId, "GRAPH")
        : unsupported(actionId, "comparison-graphs-require-two-krx-stocks");
    }

    if (futuresCount > 1) return unsupported(actionId, "two-futures-targets-are-unsupported");
    if (futuresCount === 1) {
      const counterpart = pair.find(value => !(value.kind === "market-target" && value.target === "Futures"));
      if (!counterpart || counterpart.kind !== "market-target") {
        return unsupported(actionId, "futures-requires-a-market-target-counterpart");
      }
      return selectedAction.mode === "EXPECTED"
        ? supported(actionId, "CURRENT", "The current Core routes a futures expected action through the current s5032 branch.")
        : supported(actionId, "CURRENT");
    }

    if (selectedAction.mode === "EXPECTED") {
      const allowed = pair.every(value => value.kind === "krx-stock" ||
        (value.kind === "market-target" && expectedMarketTargets.has(value.target)));
      return allowed
        ? supported(actionId, "EXPECTED")
        : unsupported(actionId, "expected-mode-supports-only-krx-stocks-and-four-domestic-indices");
    }

    const hasMarketTarget = pair.some(value => value.kind === "market-target");
    if (hasMarketTarget && hasWorld) {
      return unsupported(actionId, "mixed-market-and-world-is-unsupported");
    }
    return supported(actionId, "CURRENT");
  }

  function isActionAllowed(actionId, pairValue) {
    return getCompatibility(actionId, pairValue).supported;
  }

  function buildComparisonSelection(actionId, pairValue) {
    const selectedAction = actionsById.get(actionId);
    const pair = normalizePair(pairValue);
    const compatibility = getCompatibility(actionId, pair);
    if (!selectedAction || !pair || !compatibility.supported) {
      throw new RangeError(`The comparison action is unavailable: ${compatibility.reason || "invalid-selection"}.`);
    }
    const subject = pair.map(subjectTokenForTarget).join(",");
    if (selectedAction.kind === "two-column") {
      const futures = pair.some(value => value.kind === "market-target" && value.target === "Futures");
      const code = futures ? "5032" : "8032";
      return Object.freeze({
        builderKey: futures ? "s5032" : "s8018",
        code,
        aliases: Object.freeze([code]),
        selection: Object.freeze({
          groupCode: pair.some(value => value.kind === "market-target") ? "INDEX" : "STOCK",
          subject,
          graphicType: "2열판",
          subtype: selectedAction.mode,
          dataCode: ""
        }),
        effectiveMode: compatibility.effectiveMode,
        warning: compatibility.warning
      });
    }
    const builderKey = selectedAction.kind === "return" ? "s5029" : selectedAction.builderKey;
    const code = selectedAction.kind === "return" ? "5029" : selectedAction.code;
    const graphicType = selectedAction.kind === "return" ? "RETURN_COMPARISON" : selectedAction.graphicType;
    const dataCode = krxIdentityEnvelope(pair);
    if (!dataCode) {
      throw new RangeError("Comparison graphs require two exact KRX stock identities.");
    }
    return Object.freeze({
      builderKey,
      code,
      aliases: Object.freeze([code]),
      selection: Object.freeze({
        groupCode: "STOCK",
        subject,
        graphicType,
        subtype: selectedAction.period,
        dataCode
      }),
      effectiveMode: compatibility.effectiveMode,
      warning: compatibility.warning
    });
  }

  function requireOptions(options) {
    if (!options || typeof options !== "object" || Array.isArray(options)) {
      throw new TypeError("Comparison playlist options are required.");
    }
    const id = cleanText(options.id);
    if (!id || !identifierPattern.test(id)) throw new TypeError("A safe comparison playlist id is required.");
    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, fadeDuration };
  }

  function createComparisonPlaylistEntry(actionId, pairValue, options) {
    const selectedAction = actionsById.get(actionId);
    if (!selectedAction) throw new RangeError("The comparison action is unknown.");
    const pair = normalizePair(pairValue);
    if (!pair) throw new RangeError("Select two comparison targets first.");
    const normalized = requireOptions(options);
    const mapping = buildComparisonSelection(actionId, pair);
    const entry = {
      id: normalized.id,
      builderKey: mapping.builderKey,
      code: mapping.code,
      aliases: mapping.aliases,
      title: pairLabel(pair),
      detail: selectedAction.label,
      category: selectedAction.kind === "two-column" ? "plate" : "chart",
      market: "compare",
      selection: mapping.selection,
      enabled: true,
      fadeDuration: normalized.fadeDuration,
      graphicType: mapping.selection.graphicType,
      subtype: mapping.selection.subtype,
      effectiveMode: mapping.effectiveMode,
      warning: mapping.warning,
      operator: Object.freeze({
        source: "legacy-comparison-workflow",
        schemaVersion: SCHEMA_VERSION,
        actionId,
        pair,
        effectiveMode: mapping.effectiveMode,
        assetPath: runtimeAsset.relativePath,
        assetSha256: runtimeAsset.sha256
      })
    };
    return entry;
  }

  function createYieldBatchEntries(pairValue, options = {}) {
    if (!options || typeof options !== "object" || Array.isArray(options)) {
      throw new TypeError("Yield batch options must be an object.");
    }
    const idPrefix = cleanText(options.idPrefix ?? "comparison-return", 96);
    if (!idPrefix || !identifierPattern.test(idPrefix)) throw new TypeError("A safe yield batch id prefix is required.");
    return Object.freeze(yieldActionIds.map(actionId => createComparisonPlaylistEntry(actionId, pairValue, {
      id: `${idPrefix}-${actionId.replace("return-", "")}`,
      fadeDuration: options.fadeDuration
    })));
  }

  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 restoreComparisonPlaylistEntry(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-comparison-workflow" ||
        operator.schemaVersion !== SCHEMA_VERSION || operator.assetPath !== runtimeAsset.relativePath ||
        operator.assetSha256 !== runtimeAsset.sha256 || !actionsById.has(operator.actionId)) return null;
    let recreated;
    try {
      recreated = createComparisonPlaylistEntry(operator.actionId, operator.pair, {
        id: value.id,
        fadeDuration: value.fadeDuration
      });
    } catch {
      return null;
    }
    const scalars = [
      "id", "builderKey", "code", "title", "detail", "category", "market",
      "fadeDuration", "graphicType", "subtype", "effectiveMode", "warning"
    ];
    if (!scalars.every(key => value[key] === recreated[key]) ||
        !Array.isArray(value.aliases) || value.aliases.length !== recreated.aliases.length ||
        !value.aliases.every((alias, index) => alias === recreated.aliases[index]) ||
        !sameSelection(value.selection, recreated.selection) ||
        operator.effectiveMode !== recreated.operator.effectiveMode) return null;
    return { ...recreated, enabled: value.enabled };
  }

  function createPairRecord(idValue, pairValue) {
    const id = cleanText(idValue);
    const pair = normalizePair(pairValue);
    if (!id || !identifierPattern.test(id) || !pair) return null;
    return Object.freeze({ id, pair });
  }

  function emptyPairList() {
    return Object.freeze({ schemaVersion: PAIR_LIST_SCHEMA_VERSION, pairs: Object.freeze([]) });
  }

  function freezePairList(records) {
    return Object.freeze({ schemaVersion: PAIR_LIST_SCHEMA_VERSION, pairs: Object.freeze(records) });
  }

  function normalizePairList(value) {
    if (!value || typeof value !== "object" || Array.isArray(value) ||
        value.schemaVersion !== PAIR_LIST_SCHEMA_VERSION || !Array.isArray(value.pairs)) return emptyPairList();
    const records = [];
    const ids = new Set();
    for (const raw of value.pairs) {
      if (records.length >= MAX_SAVED_PAIRS) break;
      const record = createPairRecord(raw?.id, raw?.pair ?? raw);
      if (!record || ids.has(record.id)) continue;
      ids.add(record.id);
      records.push(record);
    }
    return freezePairList(records);
  }

  function appendPair(value, recordOrId, pairValue) {
    const list = normalizePairList(value);
    const record = typeof recordOrId === "string"
      ? createPairRecord(recordOrId, pairValue)
      : createPairRecord(recordOrId?.id, recordOrId?.pair ?? recordOrId);
    if (!record) throw new TypeError("A valid saved comparison pair is required.");
    if (list.pairs.length >= MAX_SAVED_PAIRS) throw new RangeError("The saved comparison pair limit was reached.");
    if (list.pairs.some(value => value.id === record.id)) throw new RangeError("The saved comparison pair id already exists.");
    return freezePairList([...list.pairs, record]);
  }

  function reorderPairList(value, idValue, directionValue) {
    const list = normalizePairList(value);
    const id = cleanText(idValue);
    const direction = directionValue === "up" ? -1 : directionValue === "down" ? 1 : Number(directionValue);
    if (!id || (direction !== -1 && direction !== 1)) return list;
    const from = list.pairs.findIndex(value => value.id === id);
    const to = from + direction;
    if (from < 0 || to < 0 || to >= list.pairs.length) return list;
    const records = [...list.pairs];
    [records[from], records[to]] = [records[to], records[from]];
    return freezePairList(records);
  }

  function deletePair(value, idValue) {
    const list = normalizePairList(value);
    const id = cleanText(idValue);
    return id ? freezePairList(list.pairs.filter(value => value.id !== id)) : list;
  }

  function deleteAllPairs() {
    return emptyPairList();
  }

  return {
    runtimeAsset,
    marketTargets,
    marketQuoteTargets: marketTargets,
    actions,
    comparisonActions: actions,
    yieldActionIds,
    pairListSchemaVersion: PAIR_LIST_SCHEMA_VERSION,
    normalizeTarget,
    getMarketTarget,
    targetKey,
    subjectTokenForTarget,
    normalizePair,
    rotatePair,
    swapPair,
    clearPair,
    pairLabel,
    getCompatibility,
    isActionAllowed,
    buildComparisonSelection,
    krxIdentityEnvelope,
    createComparisonPlaylistEntry,
    createYieldBatchEntries,
    restoreComparisonPlaylistEntry,
    refreshComparisonPlaylistEntry: restoreComparisonPlaylistEntry,
    createPairRecord,
    normalizePairList,
    appendPair,
    reorderPairList,
    deletePair,
    deleteAllPairs
  };
});
