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

  const manualLists = typeof module === "object" && module.exports
    ? require("./manual-lists-workflow.js")
    : root.MbnManualListsWorkflow;
  const manualFinancial = typeof module === "object" && module.exports
    ? require("./manual-financial-workflow.js")
    : root.MbnManualFinancialWorkflow;
  const api = Object.freeze(factory(manualLists, manualFinancial));
  if (typeof module === "object" && module.exports) module.exports = api;
  if (root) root.MbnNamedManualRestoreWorkflow = api;
})(typeof globalThis === "object" ? globalThis : this, function (manualLists, manualFinancial) {
  "use strict";

  if (!manualLists || !manualFinancial) throw new Error("Manual restore dependencies are unavailable.");

  const identifierPattern = /^[A-Za-z0-9_-]{1,128}$/;
  const versionPattern = /^[a-f0-9]{64}$/;
  const unsafeTextPattern = /[\p{Cc}\p{Cf}\p{Cs}\p{Zl}\p{Zp}\uFFFD]/u;
  const pendingValues = new WeakSet();
  const storedPendingValues = new WeakSet();
  const legacyAudienceByGroup = Object.freeze({
    "개인 순매도 상위(수동)": "INDIVIDUAL",
    "외국인 순매도 상위(수동)": "FOREIGN",
    "기관 순매도 상위(수동)": "INSTITUTION"
  });
  const canonicalMarketByGroup = Object.freeze({
    KOSPI: "kospi", KOSDAQ: "kosdaq", NXT_KOSPI: "nxt-kospi", NXT_KOSDAQ: "nxt-kosdaq"
  });
  const historicalMarketByGroup = Object.freeze({
    "코스피": "kospi", "코스닥": "kosdaq", "코스피_NXT": "nxt-kospi", "코스닥_NXT": "nxt-kosdaq"
  });
  const canonicalFinancialByGraphic = Object.freeze(Object.fromEntries(
    manualFinancial.sourceContract.screens.map(screen => [screen.graphicType, screen])));
  const historicalFinancialByGraphic = Object.freeze(Object.fromEntries(
    manualFinancial.sourceContract.screens.map(screen => [screen.label, screen])));

  function safeText(value, maximum, allowEmpty = false) {
    return typeof value === "string" && value.length <= maximum &&
      (allowEmpty || value.length > 0) && value === value.trim() &&
      !value.includes("^") && !unsafeTextPattern.test(value);
  }

  function selection(raw) {
    if (!raw || typeof raw !== "object" || Array.isArray(raw) ||
        typeof raw.enabled !== "boolean" || !Number.isInteger(raw.itemIndex) || raw.itemIndex < 0 ||
        !safeText(raw.groupCode, 256, true) || !safeText(raw.subject, 4096, true) ||
        !safeText(raw.graphicType, 256, true) || !safeText(raw.subtype, 256, true) ||
        !safeText(raw.dataCode, 1024, true) || !safeText(raw.pageText, 9)) return null;
    return Object.freeze({
      groupCode: raw.groupCode,
      subject: raw.subject,
      graphicType: raw.graphicType,
      subtype: raw.subtype,
      dataCode: raw.dataCode,
      pageText: raw.pageText
    });
  }

  function options(value) {
    if (!value || typeof value !== "object" || Array.isArray(value) ||
        !identifierPattern.test(value.id) || !Number.isInteger(value.fadeDuration) ||
        value.fadeDuration < 0 || value.fadeDuration > 60) return null;
    return Object.freeze({ id: value.id, fadeDuration: value.fadeDuration });
  }

  function exactHistoricalViNames(subject) {
    if (!safeText(subject, 4096) || subject.startsWith("@MBN_VI_SNAPSHOT_")) return null;
    const names = subject.split(",");
    if (names.length < 1 || names.length > manualLists.maximumViItems ||
        names.some(name => !safeText(name, 200) || name !== name.trim())) return null;
    return Object.freeze(names);
  }

  function classify(raw, rawOptions) {
    const selected = selection(raw);
    const normalizedOptions = options(rawOptions);
    if (!selected || !normalizedOptions) return null;
    let descriptor = null;

    const canonicalAudience = manualLists.normalizeAudience(selected.groupCode);
    const legacyAudience = legacyAudienceByGroup[selected.groupCode] || null;
    if ((canonicalAudience && selected.subject === "INDEX" &&
         selected.graphicType === "MANUAL_NET_SELL" && selected.subtype === "MANUAL_NET_SELL" &&
         selected.pageText === "1/1") ||
        (legacyAudience && selected.subject === "지수" &&
         selected.pageText === "1/1" &&
         selected.graphicType === "순매도 상위" && selected.subtype === "순매도 상위")) {
      if (selected.dataCode !== "") return null;
      descriptor = {
        kind: "net-sell",
        audience: canonicalAudience || legacyAudience,
        historical: Boolean(legacyAudience)
      };
    } else if (selected.groupCode === "PAGED_VI" && selected.graphicType === "INPUT_ORDER" &&
               selected.subtype === "CURRENT" && /^1\/(?:[1-9]|1[0-9]|20)$/.test(selected.pageText)) {
      if (selected.subject === manualLists.viSnapshotSubject && versionPattern.test(selected.dataCode)) {
        descriptor = { kind: "vi", expectedVersion: selected.dataCode, historicalNames: null };
      }
    } else if (selected.groupCode === "" && selected.graphicType === "5단 표그래프" &&
               selected.subtype === "VI 발동(수동)" && selected.dataCode === "") {
      const names = exactHistoricalViNames(selected.subject);
      if (names && selected.pageText === `1/${Math.ceil(names.length / manualLists.viPageSize)}`) {
        descriptor = { kind: "vi", expectedVersion: null, historicalNames: names };
      }
    } else {
      const canonicalProfile = canonicalFinancialByGraphic[selected.graphicType];
      const historicalProfile = historicalFinancialByGraphic[selected.graphicType];
      const canonicalMarket = canonicalMarketByGroup[selected.groupCode];
      const historicalMarket = historicalMarketByGroup[selected.groupCode];
      const profile = canonicalProfile && canonicalMarket
        ? canonicalProfile
        : (historicalProfile && historicalMarket ? historicalProfile : null);
      const market = canonicalProfile && canonicalMarket ? canonicalMarket : historicalMarket;
      if (profile && market && safeText(selected.subject, 200) &&
          selected.subtype === "" && selected.dataCode === "" && selected.pageText === "1/1") {
        descriptor = {
          kind: "financial",
          screen: profile.screen,
          builderKey: profile.builderKey,
          code: profile.code,
          market,
          stockName: selected.subject,
          identity: Object.freeze({ screen: profile.screen, stockName: selected.subject })
        };
      }
    }

    if (!descriptor) return null;
    const pending = Object.freeze({
      ...descriptor,
      itemIndex: raw.itemIndex,
      enabled: raw.enabled,
      entry: Object.freeze({ id: normalizedOptions.id }),
      fadeDuration: normalizedOptions.fadeDuration,
      rawSelection: Object.freeze({
        groupCode: selected.groupCode,
        subject: selected.subject,
        graphicType: selected.graphicType,
        subtype: selected.subtype,
        dataCode: selected.dataCode
      }),
      pageText: selected.pageText
    });
    pendingValues.add(pending);
    return pending;
  }

  function createManualListReadRequest(requestId, pending) {
    if (!pendingValues.has(pending) || !["net-sell", "vi"].includes(pending.kind)) {
      throw new TypeError("A validated named manual-list restore is required.");
    }
    return pending.kind === "net-sell"
      ? manualLists.createNetSellReadRequest(requestId, pending.audience)
      : manualLists.createViReadRequest(requestId);
  }

  function materializeManualList(pending, payload, requestEnvelope) {
    if (!pendingValues.has(pending) || !requestEnvelope ||
        requestEnvelope.type !== (pending.kind === "net-sell"
          ? manualLists.bridgeContract.netSellReadType
          : manualLists.bridgeContract.viReadType)) return null;
    let entry;
    if (pending.kind === "net-sell") {
      const response = manualLists.normalizeNetSellDataResponse(payload, requestEnvelope.payload);
      if (!response) return null;
      entry = manualLists.createNetSellPlaylistEntry(pending.audience, {
        id: pending.entry.id,
        fadeDuration: pending.fadeDuration,
        enabled: pending.enabled
      }, response);
    } else if (pending.kind === "vi") {
      const response = manualLists.normalizeViDataResponse(payload, requestEnvelope.payload.requestId);
      if (!response || response.itemCount < 1 ||
          pending.pageText !== `1/${response.pageCount}` ||
          (pending.expectedVersion !== null && response.version !== pending.expectedVersion)) return null;
      if (pending.historicalNames !== null) {
        const names = response.items.map(item => item.name);
        if (names.length !== pending.historicalNames.length ||
            names.some((name, index) => name !== pending.historicalNames[index])) return null;
      }
      entry = manualLists.createViPlaylistEntry(response.items, response.version, {
        id: pending.entry.id,
        fadeDuration: pending.fadeDuration,
        enabled: pending.enabled
      }, response);
    }
    return manualLists.restorePlaylistEntry(entry) ? entry : null;
  }

  function isPending(value, kind) {
    return pendingValues.has(value) && (kind === undefined || value.kind === kind);
  }

  function matchesMaterializedEntry(pending, entry) {
    if (!pendingValues.has(pending) || !entry || entry.id !== pending.entry.id ||
        entry.enabled !== pending.enabled || entry.fadeDuration !== pending.fadeDuration) return false;
    if (pending.kind === "net-sell") {
      const restored = manualLists.restorePlaylistEntry(entry);
      return !!restored && restored.builderKey === "s5025" &&
        restored.operator.kind === "net-sell" && restored.operator.audience === pending.audience;
    }
    if (pending.kind === "vi") {
      const restored = manualLists.restorePlaylistEntry(entry);
      return !!restored && restored.builderKey === "s5074" && restored.operator.kind === "vi";
    }
    if (pending.kind === "financial") {
      return manualFinancial.isPlaylistEntryPlayable(entry) &&
        entry.builderKey === pending.builderKey && entry.code === pending.code &&
        entry.stockName === pending.stockName && entry.market === pending.market;
    }
    return false;
  }

  function isTrustedEntryForSave(entry, itemIndex = 0) {
    if (!entry || typeof entry !== "object" || Array.isArray(entry) ||
        !Number.isInteger(itemIndex) || itemIndex < 0 || typeof entry.enabled !== "boolean") return false;
    const source = entry.operator?.source;
    const fresh = source === "manual-financial-workflow"
      ? manualFinancial.isPlaylistEntryPlayable(entry)
      : source === "legacy-manual-lists-workflow" && manualLists.isPlaylistEntryPlayable(entry);
    if (!fresh) return false;
    const pageText = entry.operator?.kind === "vi"
      ? (Number.isInteger(entry.pageCount) ? `1/${entry.pageCount}` : "")
      : "1/1";
    const pending = classify({
      itemIndex,
      enabled: entry.enabled,
      groupCode: entry.selection?.groupCode,
      subject: entry.selection?.subject,
      graphicType: entry.selection?.graphicType,
      subtype: entry.selection?.subtype,
      dataCode: entry.selection?.dataCode,
      pageText
    }, { id: entry.id, fadeDuration: entry.fadeDuration });
    return !!pending && matchesMaterializedEntry(pending, entry);
  }

  function createStoredManualListPending(entry, itemIndex = 0) {
    const restored = manualLists.restorePlaylistEntry(entry);
    if (!restored || !Number.isInteger(itemIndex) || itemIndex < 0) return null;
    const pageText = restored.operator.kind === "vi" ? `1/${restored.pageCount}` : "1/1";
    const pending = classify({
      itemIndex,
      enabled: restored.enabled,
      groupCode: restored.selection.groupCode,
      subject: restored.selection.subject,
      graphicType: restored.selection.graphicType,
      subtype: restored.selection.subtype,
      dataCode: restored.selection.dataCode,
      pageText
    }, { id: restored.id, fadeDuration: restored.fadeDuration });
    if (!pending || !["net-sell", "vi"].includes(pending.kind) ||
        !matchesMaterializedEntry(pending, restored)) return null;
    storedPendingValues.add(pending);
    return pending;
  }

  function isStoredManualListPending(value) {
    return storedPendingValues.has(value);
  }

  function legacyFieldsForEntry(entry, itemIndex = 0) {
    if (!isTrustedEntryForSave(entry, itemIndex)) return null;
    const enabled = entry.enabled ? "1" : "0";
    let fields = null;
    if (entry.operator.source === "legacy-manual-lists-workflow" &&
        entry.operator.kind === "net-sell") {
      const groupCode = `${manualLists.audienceLabels[entry.operator.audience]} 순매도 상위(수동)`;
      fields = [enabled, groupCode, "지수", "순매도 상위", "순매도 상위", "1/1", ""];
    } else if (entry.operator.source === "legacy-manual-lists-workflow" &&
               entry.operator.kind === "vi") {
      fields = [
        enabled,
        "",
        entry.operator.items.map(item => item.name).join(","),
        "5단 표그래프",
        "VI 발동(수동)",
        `1/${entry.pageCount}`,
        ""
      ];
    } else if (entry.operator.source === "manual-financial-workflow") {
      const market = {
        kospi: "코스피",
        kosdaq: "코스닥",
        "nxt-kospi": "코스피_NXT",
        "nxt-kosdaq": "코스닥_NXT"
      }[entry.market];
      const profile = manualFinancial.getScreenDefinition(entry.operator.screen);
      if (market && profile) {
        fields = [enabled, market, entry.stockName, profile.label, "", "1/1", ""];
      }
    }
    if (!fields) return null;
    const pending = classify({
      itemIndex,
      enabled: fields[0] === "1",
      groupCode: fields[1],
      subject: fields[2],
      graphicType: fields[3],
      subtype: fields[4],
      pageText: fields[5],
      dataCode: fields[6]
    }, { id: entry.id, fadeDuration: entry.fadeDuration });
    return pending && matchesMaterializedEntry(pending, entry)
      ? Object.freeze(fields)
      : null;
  }

  return {
    classify,
    createManualListReadRequest,
    materializeManualList,
    isPending,
    matchesMaterializedEntry,
    isTrustedEntryForSave,
    createStoredManualListPending,
    isStoredManualListPending,
    legacyFieldsForEntry
  };
});
