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

  const themes = typeof module === "object" && module.exports
    ? require("./theme-workflow.js")
    : root.MbnThemeWorkflow;
  const api = Object.freeze(factory(themes));
  if (typeof module === "object" && module.exports) module.exports = api;
  if (root) root.MbnNamedNxtThemeRestoreWorkflow = api;
})(typeof globalThis === "object" ? globalThis : this, function (themes) {
  "use strict";

  if (!themes) throw new Error("Theme workflow is unavailable.");

  const SCHEMA_VERSION = 1;
  const requestIdPattern = /^[A-Za-z0-9_-]{1,128}$/;
  const themeCodePattern = /^[0-9]{3,8}$/;
  const isoTimestampPattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,7})?(?:Z|[+-]\d{2}:\d{2})$/;
  const unsafeTextPattern = /[\p{Cc}\p{Cf}\p{Cs}\p{Zl}\p{Zp}\uFFFD]/u;
  const pendingValues = new WeakSet();
  const materializedValues = new WeakSet();
  const failureCodes = new Set([
    "INVALID_ROW",
    "UNSUPPORTED_ACTION",
    "IDENTITY_NOT_FOUND",
    "IDENTITY_AMBIGUOUS",
    "PREVIEW_EMPTY",
    "LIVE_ITEMS_EMPTY",
    "DATABASE_DATA_INVALID",
    "RESTORE_FAILED"
  ]);
  const profiles = Object.freeze({
    "five-row-current": Object.freeze({ builderKey: "s5074", cutCode: "5074", pageSize: 5 }),
    "six-row-current": Object.freeze({ builderKey: "s5077", cutCode: "5077", pageSize: 6 }),
    "twelve-row-current": Object.freeze({ builderKey: "s5088", cutCode: "5088", pageSize: 12 })
  });

  const bridgeContract = Object.freeze({
    resultType: "named-nxt-theme-restore-results",
    errorType: "named-nxt-theme-restore-error"
  });

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

  function hasExactKeys(value, keys) {
    if (!isPlainObject(value)) return false;
    const actual = Object.keys(value).sort();
    const expected = [...keys].sort();
    return actual.length === expected.length && actual.every((key, index) => key === expected[index]);
  }

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

  function safeRaw(raw) {
    return isPlainObject(raw) && typeof raw.enabled === "boolean" &&
      Number.isInteger(raw.itemIndex) && raw.itemIndex >= 0 && raw.itemIndex < 1000 &&
      safeText(raw.groupCode, 256) && safeText(raw.subject, 256, true) &&
      safeText(raw.graphicType, 256, true) && safeText(raw.subtype, 256, true) &&
      safeText(raw.pageText, 9, true) && safeText(raw.dataCode, 256, true);
  }

  // Route every exact group row. Malformed or unsupported grammar is still a
  // correlated pending row so the native verifier can return a closed reason.
  function classify(raw, options = {}) {
    if (!safeRaw(raw) || raw.groupCode !== "테마_NXT" ||
        !requestIdPattern.test(options.id) || !Number.isInteger(options.fadeDuration) ||
        options.fadeDuration < 0 || options.fadeDuration > 60) return null;
    const pending = Object.freeze({
      itemIndex: raw.itemIndex,
      enabled: raw.enabled,
      fadeDuration: options.fadeDuration,
      entry: Object.freeze({ id: options.id }),
      rawSelection: Object.freeze({
        groupCode: raw.groupCode,
        subject: raw.subject,
        graphicType: raw.graphicType,
        subtype: raw.subtype,
        dataCode: raw.dataCode
      })
    });
    pendingValues.add(pending);
    return pending;
  }

  function normalizeOutcome(value, pending) {
    if (!pendingValues.has(pending) || !isPlainObject(value) ||
        value.itemIndex !== pending.itemIndex) return null;
    if (value.status === "failed") {
      if (!hasExactKeys(value, ["itemIndex", "status", "failure"]) ||
          !failureCodes.has(value.failure)) return null;
      return Object.freeze({ ...value });
    }
    const keys = [
      "itemIndex", "status", "actionId", "builderKey", "cutCode", "pageSize", "sort",
      "session", "themeTitle", "storedThemeCode", "currentThemeCode", "previewItemCount",
      "liveItemCount", "previewIsTruncated", "codeWasRemapped"
    ];
    if (value.status !== "resolved" || !hasExactKeys(value, keys)) return null;
    const profile = profiles[value.actionId];
    if (!profile || value.builderKey !== profile.builderKey ||
        value.cutCode !== profile.cutCode || value.pageSize !== profile.pageSize ||
        !["INPUT_ORDER", "GAIN_DESC", "GAIN_ASC"].includes(value.sort) ||
        !["PRE_MARKET", "AFTER_MARKET"].includes(value.session) ||
        !safeText(value.themeTitle, 128) || value.themeTitle.includes("(NXT)") ||
        pending.rawSelection.subject !== `${value.themeTitle}(NXT)` ||
        !themeCodePattern.test(value.storedThemeCode) ||
        value.storedThemeCode !== pending.rawSelection.dataCode ||
        !themeCodePattern.test(value.currentThemeCode) ||
        value.codeWasRemapped !== (value.storedThemeCode !== value.currentThemeCode) ||
        !Number.isInteger(value.previewItemCount) || value.previewItemCount < 1 ||
        value.previewItemCount > 1000000 ||
        !Number.isInteger(value.liveItemCount) || value.liveItemCount < 1 ||
        value.liveItemCount > value.previewItemCount ||
        typeof value.previewIsTruncated !== "boolean" ||
        value.previewIsTruncated !== (value.previewItemCount > 240)) return null;
    return Object.freeze({ ...value });
  }

  function normalizeResponse(value, expectedRequestId, pendingRows) {
    if (!requestIdPattern.test(expectedRequestId) || !Array.isArray(pendingRows) ||
        pendingRows.length < 1 || pendingRows.length > 1000 ||
        pendingRows.some(pending => !pendingValues.has(pending)) ||
        !hasExactKeys(value, ["requestId", "retrievedAt", "totalRowCount", "rows"]) ||
        value.requestId !== expectedRequestId || typeof value.retrievedAt !== "string" ||
        !isoTimestampPattern.test(value.retrievedAt) || !Number.isFinite(Date.parse(value.retrievedAt)) ||
        value.totalRowCount !== pendingRows.length || !Array.isArray(value.rows) ||
        value.rows.length !== pendingRows.length) return null;
    const rows = [];
    for (let index = 0; index < pendingRows.length; index += 1) {
      const outcome = normalizeOutcome(value.rows[index], pendingRows[index]);
      if (!outcome) return null;
      rows.push(outcome);
    }
    return Object.freeze({
      requestId: value.requestId,
      retrievedAt: value.retrievedAt,
      rows: Object.freeze(rows)
    });
  }

  function normalizeError(value, expectedRequestId) {
    return requestIdPattern.test(expectedRequestId) &&
      hasExactKeys(value, ["requestId", "code", "message", "retryable"]) &&
      value.requestId === expectedRequestId && safeText(value.code, 64) &&
      safeText(value.message, 1024) && value.retryable === false
      ? Object.freeze({ ...value })
      : null;
  }

  function materialize(pending, outcome) {
    if (!pendingValues.has(pending) || outcome?.status !== "resolved" ||
        outcome.itemIndex !== pending.itemIndex) return null;
    try {
      const created = themes.createThemePlaylistEntry(
        outcome.actionId,
        {
          market: "nxt",
          themeCode: outcome.currentThemeCode,
          title: outcome.themeTitle,
          session: outcome.session,
          itemCount: outcome.liveItemCount
        },
        outcome.sort,
        { id: pending.entry.id, fadeDuration: pending.fadeDuration });
      const entry = themes.restoreThemePlaylistEntry({
        ...created,
        enabled: pending.enabled,
        operator: Object.freeze({
          ...created.operator,
          namedNxtRestore: Object.freeze({
            schemaVersion: SCHEMA_VERSION,
            rawSelection: pending.rawSelection,
            currentThemeCode: outcome.currentThemeCode,
            previewItemCount: outcome.previewItemCount,
            liveItemCount: outcome.liveItemCount,
            codeWasRemapped: outcome.codeWasRemapped
          })
        })
      });
      if (!entry) return null;
      // restoreThemePlaylistEntry recreates its operator, so retain the native
      // proof only after the closed theme entry itself has round-tripped.
      const materialized = Object.freeze({
        ...entry,
        operator: Object.freeze({
          ...entry.operator,
          namedNxtRestore: Object.freeze({
            schemaVersion: SCHEMA_VERSION,
            rawSelection: pending.rawSelection,
            currentThemeCode: outcome.currentThemeCode,
            previewItemCount: outcome.previewItemCount,
            liveItemCount: outcome.liveItemCount,
            codeWasRemapped: outcome.codeWasRemapped
          })
        })
      });
      materializedValues.add(materialized);
      return materialized;
    } catch {
      return null;
    }
  }

  function refreshDynamicSession(entry, now = new Date()) {
    if (!materializedValues.has(entry) || !(now instanceof Date) ||
        !Number.isFinite(now.getTime())) return null;
    const proof = entry.operator.namedNxtRestore;
    const session = now.getHours() <= 12 ? "PRE_MARKET" : "AFTER_MARKET";
    const recreated = themes.createThemePlaylistEntry(
      entry.operator.actionId,
      { ...entry.operator.theme, session },
      entry.operator.sort,
      { id: entry.id, fadeDuration: entry.fadeDuration });
    const refreshed = Object.freeze({
      ...recreated,
      enabled: entry.enabled,
      pageText: entry.pageText,
      operator: Object.freeze({ ...recreated.operator, namedNxtRestore: proof })
    });
    materializedValues.add(refreshed);
    return refreshed;
  }

  function matchesRaw(entry, raw) {
    if (!materializedValues.has(entry) || !safeRaw(raw)) return false;
    const saved = entry.operator?.namedNxtRestore?.rawSelection;
    return saved && raw.enabled === entry.enabled &&
      ["groupCode", "subject", "graphicType", "subtype", "dataCode"]
        .every(key => saved[key] === raw[key]);
  }

  function legacySelectionForEntry(entry) {
    if (!materializedValues.has(entry)) return null;
    return entry.operator.namedNxtRestore.rawSelection;
  }

  function withEnabled(entry, enabled) {
    if (!materializedValues.has(entry) || typeof enabled !== "boolean") return null;
    if (entry.enabled === enabled) return entry;
    const replacement = Object.freeze({ ...entry, enabled });
    materializedValues.add(replacement);
    return replacement;
  }

  function isPending(value) {
    return pendingValues.has(value);
  }

  function isMaterializedEntry(value) {
    return materializedValues.has(value);
  }

  return {
    bridgeContract,
    classify,
    normalizeResponse,
    normalizeError,
    materialize,
    refreshDynamicSession,
    withEnabled,
    matchesRaw,
    legacySelectionForEntry,
    isPending,
    isMaterializedEntry
  };
});
