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

  const stockWorkflow = typeof module === "object" && module.exports
    ? require("./operator-workflow.js")
    : root.MbnOperatorWorkflow;
  const fixedWorkflow = typeof module === "object" && module.exports
    ? require("./legacy-fixed-workflow.js")
    : root.MbnLegacyFixedWorkflow;
  const themeWorkflow = typeof module === "object" && module.exports
    ? require("./theme-workflow.js")
    : root.MbnThemeWorkflow;
  const expertWorkflow = typeof module === "object" && module.exports
    ? require("./expert-workflow.js")
    : root.MbnExpertWorkflow;
  const comparisonWorkflow = typeof module === "object" && module.exports
    ? require("./comparison-workflow.js")
    : root.MbnComparisonWorkflow;
  const industryWorkflow = typeof module === "object" && module.exports
    ? require("./industry-workflow.js")
    : root.MbnIndustryWorkflow;
  const foreignIndexCandleWorkflow = typeof module === "object" && module.exports
    ? require("./legacy-foreign-index-candle-workflow.js")
    : root.MbnLegacyForeignIndexCandleWorkflow;
  const foreignStockWorkflow = typeof module === "object" && module.exports
    ? require("./named-foreign-stock-restore-workflow.js")
    : root.MbnNamedForeignStockRestoreWorkflow;
  const api = Object.freeze(factory(
    stockWorkflow,
    fixedWorkflow,
    themeWorkflow,
    expertWorkflow,
    comparisonWorkflow,
    industryWorkflow,
    foreignIndexCandleWorkflow,
    foreignStockWorkflow));
  if (typeof module === "object" && module.exports) module.exports = api;
  if (root) root.MbnLegacyNamedRowWorkflow = api;
})(typeof globalThis === "object" ? globalThis : this, function (
  stockWorkflow,
  fixedWorkflow,
  themeWorkflow,
  expertWorkflow,
  comparisonWorkflow,
  industryWorkflow,
  foreignIndexCandleWorkflow,
  foreignStockWorkflow) {
  "use strict";

  if (!stockWorkflow || !fixedWorkflow || !themeWorkflow || !expertWorkflow ||
      !comparisonWorkflow || !industryWorkflow || !foreignIndexCandleWorkflow ||
      !foreignStockWorkflow) {
    throw new Error("Legacy named-row dependencies are unavailable.");
  }

  const selectionKeys = Object.freeze([
    "groupCode", "subject", "graphicType", "subtype", "dataCode"
  ]);
  const unsafeTextPattern = /[\u0000-\u001f\u007f\uFFFD]/;
  const themeCodePattern = /^[0-9]{3,8}$/;
  const expertCodePattern = /^[0-9]{4}$/;

  function isPlainObject(value) {
    return value !== null && typeof value === "object" && !Array.isArray(value);
  }

  function isSafeField(value) {
    return typeof value === "string" && value.length <= 256 &&
      !unsafeTextPattern.test(value) && !value.includes("^");
  }

  function isSafeRawRow(value) {
    return isPlainObject(value) && selectionKeys.every(key => isSafeField(value[key]));
  }

  function sameSelection(left, right) {
    return isPlainObject(left) && isSafeRawRow(right) &&
      selectionKeys.every(key => isSafeField(left[key]) && left[key] === right[key]);
  }

  function splitLegacyLabel(value) {
    if (!isSafeField(value) || !value) return null;
    const separator = value.indexOf("_");
    return Object.freeze(separator < 0
      ? { first: value, remainder: "" }
      : { first: value.slice(0, separator), remainder: value.slice(separator + 1) });
  }

  function stockLegacySignature(operator) {
    const stock = operator?.stock;
    const label = splitLegacyLabel(operator?.legacyLabel);
    if (operator?.source !== "legacy-stock-workflow" || !isPlainObject(stock) || !label ||
        !["kospi", "kosdaq"].includes(stock.market) || typeof stock.isNxt !== "boolean" ||
        !isSafeField(stock.displayName) || !stock.displayName ||
        !isSafeField(stock.stockCode) || !stock.stockCode) return null;
    const baseGroup = stock.market === "kospi" ? "코스피" : "코스닥";
    return Object.freeze({
      groupCode: stock.isNxt ? `${baseGroup}_NXT` : baseGroup,
      subject: stock.displayName,
      graphicType: label.first,
      subtype: label.remainder,
      dataCode: stock.stockCode
    });
  }

  function fixedIndexGroup(label) {
    if (label.includes("코스피_NXT")) return "코스피_NXT";
    if (label.includes("코스닥_NXT")) return "코스닥_NXT";
    if (label.includes("코스피")) return "코스피";
    if (label.includes("코스닥")) return "코스닥";
    return "전체";
  }

  function fixedLegacySignature(value) {
    const tab = value?.legacyTab ?? value?.market;
    const section = value?.legacySection ?? value?.section;
    const labelText = value?.legacyLabel ?? value?.label;
    const label = splitLegacyLabel(labelText);
    if (!label || !isSafeField(section) || !section ||
        !["overseas", "exchange", "index"].includes(tab)) return null;

    if (tab === "overseas" || tab === "exchange") {
      return Object.freeze({
        groupCode: tab === "overseas" ? "해외지수" : "환율",
        subject: label.first,
        graphicType: section,
        subtype: label.remainder,
        dataCode: ""
      });
    }

    if (["5단 표그래프", "6종목 현재가", "12종목 현재가"].includes(section)) {
      return Object.freeze({
        groupCode: fixedIndexGroup(labelText),
        subject: "지수",
        graphicType: section,
        subtype: labelText,
        dataCode: ""
      });
    }

    if (section === "매매동향") {
      return Object.freeze({
        groupCode: fixedIndexGroup(labelText),
        subject: "지수",
        graphicType: label.first,
        subtype: label.remainder,
        dataCode: ""
      });
    }

    return Object.freeze({
      groupCode: section,
      subject: "지수",
      graphicType: label.first,
      subtype: label.remainder,
      dataCode: ""
    });
  }

  function themeLegacyDescriptor(rawRow, options = {}) {
    const isNxt = rawRow?.groupCode === "테마_NXT";
    if (!isSafeRawRow(rawRow) || (rawRow.groupCode !== "테마" && !isNxt) ||
        !rawRow.subject || !themeCodePattern.test(rawRow.dataCode)) return null;
    const match = /^테마-(현재가|예상체결가)(?:\((입력순|상승률순|하락률순)\))?$/.exec(rawRow.subtype);
    if (!match) return null;
    const valueKind = match[1] === "현재가" ? "CURRENT" : "EXPECTED";
    if (isNxt && valueKind !== "CURRENT") return null;
    // The original s5074/s5077/s5088 code falls through to its final
    // "하락률순" branch when the persisted subtype has no parenthesized sort.
    const sort = match[2]
      ? ({ 입력순: "INPUT_ORDER", 상승률순: "GAIN_DESC", 하락률순: "GAIN_ASC" })[match[2]]
      : "GAIN_ASC";
    const actionId = ({
      "5단 표그래프|CURRENT": "five-row-current",
      "5단 표그래프|EXPECTED": "five-row-expected",
      "6종목 현재가|CURRENT": "six-row-current",
      "12종목 현재가|CURRENT": "twelve-row-current"
    })[`${rawRow.graphicType}|${valueKind}`];
    if (!actionId) return null;
    let title = rawRow.subject;
    let session = null;
    if (isNxt) {
      const suffix = "(NXT)";
      if (!title.endsWith(suffix) || title.length === suffix.length) return null;
      title = title.slice(0, -suffix.length);
      if (!isSafeField(title) || !title || title.includes(suffix)) return null;
      const suppliedNow = options?.now;
      const now = suppliedNow instanceof Date && Number.isFinite(suppliedNow.getTime())
        ? suppliedNow
        : new Date();
      // Original s5074/s5077/s5088 selects the NXT mark at scene construction:
      // local hour 00..12 is pre-market and 13..23 is after-market.
      session = now.getHours() <= 12 ? "PRE_MARKET" : "AFTER_MARKET";
    }
    return Object.freeze({
      actionId,
      sort,
      theme: Object.freeze({
        market: isNxt ? "nxt" : "krx",
        themeCode: rawRow.dataCode,
        title,
        session,
        itemCount: 1
      })
    });
  }

  function themeLegacySignature(operator) {
    const theme = operator?.theme;
    if (operator?.source !== "legacy-theme-workflow" || !isPlainObject(theme) ||
        theme.market !== "krx" || !themeCodePattern.test(theme.themeCode) ||
        !isSafeField(theme.title) || !theme.title) return null;
    const action = ({
      "five-row-current": ["5단 표그래프", "현재가"],
      "five-row-expected": ["5단 표그래프", "예상체결가"],
      "six-row-current": ["6종목 현재가", "현재가"],
      "twelve-row-current": ["12종목 현재가", "현재가"]
    })[operator.actionId];
    const sortLabel = ({
      INPUT_ORDER: "입력순",
      GAIN_DESC: "상승률순",
      GAIN_ASC: "하락률순"
    })[operator.sort];
    if (!action || !sortLabel) return null;
    return Object.freeze({
      groupCode: "테마",
      subject: theme.title,
      graphicType: action[0],
      subtype: `테마-${action[1]}(${sortLabel})`,
      dataCode: theme.themeCode
    });
  }

  function expertLegacyDescriptor(rawRow) {
    if (!isSafeRawRow(rawRow) || rawRow.groupCode !== "전문가 추천" ||
        !rawRow.subject || rawRow.graphicType !== "5단 표그래프" ||
        rawRow.subtype !== "현재가" || !expertCodePattern.test(rawRow.dataCode)) return null;
    return Object.freeze({
      expertCode: rawRow.dataCode,
      expertName: rawRow.subject,
      source: "oracle",
      itemCount: 1,
      previewTruncated: false
    });
  }

  function expertLegacySignature(operator) {
    const expert = operator?.expert;
    if (operator?.source !== "legacy-expert-workflow" || !isPlainObject(expert) ||
        !expertCodePattern.test(expert.expertCode) ||
        !isSafeField(expert.expertName) || !expert.expertName) return null;
    return Object.freeze({
      groupCode: "전문가 추천",
      subject: expert.expertName,
      graphicType: "5단 표그래프",
      subtype: "현재가",
      dataCode: expert.expertCode
    });
  }

  function comparisonLegacySignature(operator) {
    if (operator?.source !== "legacy-comparison-workflow" ||
        !Array.isArray(operator.pair) || operator.pair.length !== 2) return null;
    const pair = comparisonWorkflow.normalizePair(operator.pair);
    if (!pair) return null;
    const subjects = pair.map(target => {
      if (target.kind === "market-target") return target.displayName;
      if (target.kind === "krx-stock") return target.stockName;
      return null;
    });
    if (subjects.some(value => !isSafeField(value) || !value || value.includes(","))) return null;

    if (["two-column-current", "two-column-expected"].includes(operator.actionId)) {
      // The original "종목" row discarded each target's market. Only a pair of
      // closed fixed selectors can be projected without creating a lossy row.
      if (pair.some(target => target.kind !== "market-target")) return null;
      const subject = subjects.join(",");
      // UC1 persisted the generic "종목" group when neither label contained
      // "지수" (for example an exchange-only pair). That row cannot prove it
      // was not a same-name world stock, so do not create that lossy signature.
      if (!subject.includes("지수")) return null;
      return Object.freeze({
        groupCode: "지수",
        subject,
        graphicType: "2열판",
        subtype: operator.actionId === "two-column-expected" ? "예상체결" : "",
        dataCode: ""
      });
    }

    if (pair.some(target => target.kind !== "krx-stock")) return null;
    const action = ({
      "comparison-candle": ["종목별 비교분석_캔들 그래프", ""],
      "comparison-line": ["종목별 비교분석_라인 그래프", ""],
      "return-5d": ["종목별 수익률 비교", "5일"],
      "return-1m": ["종목별 수익률 비교", "1개월"],
      "return-3m": ["종목별 수익률 비교", "3개월"],
      "return-6m": ["종목별 수익률 비교", "6개월"],
      "return-12m": ["종목별 수익률 비교", "12개월"]
    })[operator.actionId];
    if (!action) return null;
    return Object.freeze({
      groupCode: pair.map(target => target.market === "kospi" ? "코스피" : "코스닥").join(","),
      subject: subjects.join(","),
      graphicType: action[0],
      subtype: action[1],
      dataCode: ""
    });
  }

  function industryFixedDescriptor(rawRow) {
    if (!isSafeRawRow(rawRow) || rawRow.subtype !== "" || rawRow.dataCode !== "") return null;
    const market = ({
      "업종_코스피": "kospi",
      "업종_코스닥": "kosdaq"
    })[rawRow.groupCode];
    if (!market || rawRow.subject !== (market === "kospi" ? "코스피" : "코스닥")) return null;
    const cutId = ({
      "5단 표그래프": "five-row",
      "네모그래프": "square",
      "섹터지수": "sector"
    })[rawRow.graphicType];
    return cutId ? Object.freeze({ market, cutId }) : null;
  }

  function industryFixedLegacySignature(operator) {
    if (operator?.source !== "legacy-industry-workflow" ||
        !["kospi", "kosdaq"].includes(operator.market)) return null;
    const graphicType = ({
      "five-row": "5단 표그래프",
      square: "네모그래프",
      sector: "섹터지수"
    })[operator.cutId];
    if (!graphicType) return null;
    return Object.freeze({
      groupCode: operator.market === "kospi" ? "업종_코스피" : "업종_코스닥",
      subject: operator.market === "kospi" ? "코스피" : "코스닥",
      graphicType,
      subtype: "",
      dataCode: ""
    });
  }

  function matchesFixedActionRaw(rawRow, action) {
    return Boolean(action?.available && sameSelection(fixedLegacySignature(action), rawRow));
  }

  function matchesCanonicalEntry(entry, rawRow, options = {}) {
    if (!isPlainObject(entry) || !isSafeRawRow(rawRow)) return false;
    if (sameSelection(entry.selection, rawRow)) return true;
    let signature = null;
    switch (entry.operator?.source) {
      case "legacy-stock-workflow":
        signature = stockLegacySignature(entry.operator);
        break;
      case "legacy-fixed-workflow":
        signature = fixedLegacySignature(entry.operator);
        break;
      case "legacy-theme-workflow":
        if (entry.operator?.theme?.market === "nxt") {
          const descriptor = themeLegacyDescriptor(rawRow, options);
          return Boolean(descriptor && descriptor.theme.market === "nxt" &&
            descriptor.actionId === entry.operator.actionId &&
            descriptor.sort === entry.operator.sort &&
            descriptor.theme.themeCode === entry.operator.theme.themeCode &&
            descriptor.theme.title === entry.operator.theme.title &&
            descriptor.theme.session === entry.operator.theme.session);
        }
        signature = themeLegacySignature(entry.operator);
        if (sameSelection(signature, rawRow)) return true;
        if (entry.operator.sort === "GAIN_ASC" && signature) {
          const originalFallback = {
            ...signature,
            subtype: signature.subtype.replace(/\(하락률순\)$/, "")
          };
          return sameSelection(originalFallback, rawRow);
        }
        break;
      case "legacy-expert-workflow":
        signature = expertLegacySignature(entry.operator);
        break;
      case "legacy-comparison-workflow":
        signature = comparisonLegacySignature(entry.operator);
        break;
      case "legacy-industry-workflow":
        signature = industryFixedLegacySignature(entry.operator);
        break;
      case "legacy-foreign-index-candle-workflow":
        return foreignIndexCandleWorkflow.matchesRaw(entry, rawRow);
      case "legacy-named-foreign-stock-workflow":
        return foreignStockWorkflow.matchesRaw(entry, rawRow);
      default:
        return false;
    }
    return sameSelection(signature, rawRow);
  }

  function fixedRestoreNow(entry) {
    const dateMatch = /^(\d{4})-(\d{2})-(\d{2})$/.exec(entry?.selection?.dataCode || "");
    const now = dateMatch
      ? new Date(Number(dateMatch[1]), Number(dateMatch[2]) - 1, Number(dateMatch[3]), 9, 0, 0, 0)
      : new Date();
    if (entry?.selection?.graphicType === "PRE_MARKET") now.setHours(9, 0, 0, 0);
    if (entry?.selection?.graphicType === "AFTER_MARKET") now.setHours(14, 0, 0, 0);
    return now;
  }

  function restoreTrustedEntry(entry) {
    try {
      switch (entry?.operator?.source) {
        case "legacy-stock-workflow":
          return stockWorkflow.restoreStockPlaylistEntry(entry);
        case "legacy-fixed-workflow":
          return fixedWorkflow.restoreFixedPlaylistEntry(entry, { now: fixedRestoreNow(entry) });
        case "legacy-theme-workflow":
          return themeWorkflow.restoreThemePlaylistEntry(entry);
        case "legacy-expert-workflow":
          return expertWorkflow.restoreExpertPlaylistEntry(entry);
        case "legacy-comparison-workflow":
          return comparisonWorkflow.restoreComparisonPlaylistEntry(entry);
        case "legacy-industry-workflow":
          return industryWorkflow.restoreIndustryPlaylistEntry(entry);
        case "legacy-foreign-index-candle-workflow":
          return foreignIndexCandleWorkflow.restorePlaylistEntry(entry);
        case "legacy-named-foreign-stock-workflow":
          return foreignStockWorkflow.restorePlaylistEntry(entry);
        default:
          return null;
      }
    } catch {
      return null;
    }
  }

  function legacySelectionForEntry(entry) {
    if (!isPlainObject(entry) || !isPlainObject(entry.selection) ||
        !selectionKeys.every(key => isSafeField(entry.selection[key]))) return null;
    const trusted = restoreTrustedEntry(entry);
    if (!trusted || trusted.enabled !== entry.enabled) return null;
    switch (trusted.operator?.source) {
      case "legacy-stock-workflow":
        return stockLegacySignature(trusted.operator);
      case "legacy-fixed-workflow":
        return fixedLegacySignature(trusted.operator);
      case "legacy-theme-workflow":
        // The original NXT row did not persist PRE/AFTER session. Do not create
        // a new lossy row that this closed restore contract must later block.
        return themeLegacySignature(trusted.operator);
      case "legacy-expert-workflow":
        return expertLegacySignature(trusted.operator);
      case "legacy-comparison-workflow":
        return comparisonLegacySignature(trusted.operator);
      case "legacy-industry-workflow":
        return industryFixedLegacySignature(trusted.operator);
      case "legacy-foreign-index-candle-workflow":
        return foreignIndexCandleWorkflow.legacySelectionForEntry(trusted);
      case "legacy-named-foreign-stock-workflow":
        return foreignStockWorkflow.legacySelectionForEntry(trusted);
      default:
        return null;
    }
  }

  function semanticKey(entry) {
    const operator = entry?.operator;
    return JSON.stringify({
      builderKey: entry?.builderKey,
      code: entry?.code,
      selection: entry?.selection,
      source: operator?.source ?? "",
      actionId: operator?.actionId ?? "",
      cutId: operator?.cutId ?? ""
    });
  }

  function selectUniqueCanonicalEntry(candidates, rawRow, options = {}) {
    if (!Array.isArray(candidates) || !isSafeRawRow(rawRow)) return null;
    const matches = new Map();
    for (const candidate of candidates) {
      if (!matchesCanonicalEntry(candidate, rawRow, options)) continue;
      const key = semanticKey(candidate);
      if (!matches.has(key)) matches.set(key, candidate);
    }
    return matches.size === 1 ? [...matches.values()][0] : null;
  }

  return {
    selectionKeys,
    fixedLegacySignature,
    comparisonLegacySignature,
    industryFixedDescriptor,
    industryFixedLegacySignature,
    matchesFixedActionRaw,
    themeLegacyDescriptor,
    expertLegacyDescriptor,
    matchesCanonicalEntry,
    legacySelectionForEntry,
    selectUniqueCanonicalEntry
  };
});
