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

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

  const DEFAULT_FADE_DURATION = 6;
  const SCHEMA_VERSION = 1;
  const MAXIMUM_PAGE_COUNT = 20;
  const MAXIMUM_PREVIEW_ITEMS = 240;
  const identifierPattern = /^[A-Za-z0-9_-]{1,128}$/;
  // ThemeA always generated new codes as D8, but the production SB_LIST and
  // historical PLAY_LIST still contain original three- and four-digit codes.
  const themeCodePattern = /^[0-9]{3,8}$/;
  const controlCharacterPattern = /[\u0000-\u001f\u007f]/;

  const sourceContract = Object.freeze({
    originalControl: "Control/UC4.cs",
    krxResolverGroup: "PAGED_THEME",
    nxtResolverGroup: "PAGED_NXT_THEME",
    maximumPageCount: MAXIMUM_PAGE_COUNT,
    maximumPreviewItems: MAXIMUM_PREVIEW_ITEMS
  });

  const themeSorts = Object.freeze([
    Object.freeze({ id: "INPUT_ORDER", label: "입력순" }),
    Object.freeze({ id: "GAIN_DESC", label: "상승률순" }),
    Object.freeze({ id: "GAIN_ASC", label: "하락률순" })
  ]);
  const themeSortsById = new Map(themeSorts.map(value => [value.id, value]));

  const nxtSessions = Object.freeze([
    Object.freeze({ id: "PRE_MARKET", label: "프리마켓" }),
    Object.freeze({ id: "AFTER_MARKET", label: "애프터마켓" })
  ]);

  function action(id, label, builderKey, code, pageSize, valueKind) {
    return Object.freeze({
      id,
      label,
      builderKey,
      code,
      aliases: Object.freeze([code]),
      pageSize,
      valueKind
    });
  }

  const actions = Object.freeze([
    action("five-row-current", "5단 표그래프 · 현재가", "s5074", "5074", 5, "CURRENT"),
    action("five-row-expected", "5단 표그래프 · 예상체결가", "s5074", "5074", 5, "EXPECTED"),
    action("six-row-current", "6종목 현재가", "s5077", "5077", 6, "CURRENT"),
    action("twelve-row-current", "12종목 현재가", "s5088", "5088", 12, "CURRENT")
  ]);
  const actionsById = new Map(actions.map(value => [value.id, value]));

  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 normalizeMarket(value) {
    const text = String(value || "").trim().toLocaleLowerCase("en-US");
    return text === "krx" || text === "nxt" ? text : null;
  }

  function normalizeNxtSession(value) {
    const text = String(value || "")
      .trim()
      .replace(/([a-z])([A-Z])/g, "$1_$2")
      .replace(/-/g, "_")
      .toLocaleUpperCase("en-US");
    return text === "PRE_MARKET" || text === "AFTER_MARKET" ? text : null;
  }

  function isKrxEmptySession(value) {
    if (value === undefined || value === null || value === "") return true;
    const text = String(value)
      .trim()
      .replace(/([a-z])([A-Z])/g, "$1_$2")
      .replace(/-/g, "_")
      .toLocaleUpperCase("en-US");
    return text === "NOT_APPLICABLE";
  }

  function themeValidationReason(value) {
    if (!value || typeof value !== "object" || Array.isArray(value)) return "invalid-theme";
    const market = normalizeMarket(value.market ?? value.Market);
    if (!market) return "invalid-theme-market";
    const rawThemeCode = value.themeCode ?? value.ThemeCode ?? value.code;
    const themeCode = cleanText(rawThemeCode, 8);
    if (!themeCode || themeCode !== rawThemeCode || !themeCodePattern.test(themeCode)) {
      return "invalid-theme-code";
    }
    const title = cleanText(value.title ?? value.themeTitle ?? value.ThemeTitle);
    if (!title || title !== String(value.title ?? value.themeTitle ?? value.ThemeTitle) ||
        title.includes("(NXT)")) return "invalid-theme-title";
    const sessionValue = value.session ?? value.Session;
    if (market === "nxt" && !normalizeNxtSession(sessionValue)) return "nxt-session-required";
    if (market === "krx" && !isKrxEmptySession(sessionValue)) return "krx-session-must-be-empty";
    const itemCountValue = value.itemCount ?? value.previewItemCount ??
      (Array.isArray(value.items) ? value.items.length : null);
    if (itemCountValue !== null && itemCountValue !== undefined &&
        (!Number.isInteger(itemCountValue) || itemCountValue < 0 || itemCountValue > MAXIMUM_PREVIEW_ITEMS)) {
      return "invalid-theme-item-count";
    }
    const canonicalDisplay = market === "nxt" ? `${title}(NXT)` : title;
    if (value.displayTitle !== undefined && value.displayTitle !== canonicalDisplay) {
      return "invalid-theme-display-title";
    }
    return null;
  }

  function normalizeTheme(value) {
    if (themeValidationReason(value)) return null;
    const market = normalizeMarket(value.market ?? value.Market);
    const themeCode = value.themeCode ?? value.ThemeCode ?? value.code;
    const title = value.title ?? value.themeTitle ?? value.ThemeTitle;
    const itemCountValue = value.itemCount ?? value.previewItemCount ??
      (Array.isArray(value.items) ? value.items.length : null);
    const session = market === "nxt" ? normalizeNxtSession(value.session ?? value.Session) : null;
    return Object.freeze({
      market,
      themeCode,
      title,
      displayTitle: market === "nxt" ? `${title}(NXT)` : title,
      session,
      itemCount: itemCountValue === undefined ? null : itemCountValue
    });
  }

  function normalizeThemeSort(value) {
    if (value && typeof value === "object" && !Array.isArray(value)) value = value.id;
    const id = String(value || "").trim().toLocaleUpperCase("en-US");
    return themeSortsById.get(id) ?? null;
  }

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

  function getCompatibility(actionId, themeValue, sortValue) {
    const selectedAction = actionsById.get(actionId);
    if (!selectedAction) return unsupported(actionId, "unknown-action");
    const reason = themeValidationReason(themeValue);
    if (reason) return unsupported(actionId, reason);
    const theme = normalizeTheme(themeValue);
    if (!normalizeThemeSort(sortValue)) return unsupported(actionId, "invalid-theme-sort");
    if (theme.itemCount === 0) return unsupported(actionId, "theme-preview-is-empty");
    if (selectedAction.valueKind === "EXPECTED" && theme.market !== "krx") {
      return unsupported(actionId, "expected-theme-is-krx-only");
    }
    return Object.freeze({ actionId, supported: true, reason: null });
  }

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

  function calculatePagePreview(actionId, themeValue) {
    const selectedAction = actionsById.get(actionId);
    const theme = normalizeTheme(themeValue);
    if (!selectedAction || !theme) return null;
    const maximumItems = selectedAction.pageSize * MAXIMUM_PAGE_COUNT;
    if (theme.itemCount === null) {
      return Object.freeze({
        itemCount: null,
        accessibleItemCount: null,
        pageSize: selectedAction.pageSize,
        pageCount: null,
        maximumPageCount: MAXIMUM_PAGE_COUNT,
        isTruncated: false
      });
    }
    const accessibleItemCount = Math.min(theme.itemCount, maximumItems);
    return Object.freeze({
      itemCount: theme.itemCount,
      accessibleItemCount,
      pageSize: selectedAction.pageSize,
      pageCount: Math.ceil(accessibleItemCount / selectedAction.pageSize),
      maximumPageCount: MAXIMUM_PAGE_COUNT,
      isTruncated: theme.itemCount > maximumItems
    });
  }

  function buildThemeSelection(actionId, themeValue, sortValue) {
    const selectedAction = actionsById.get(actionId);
    const theme = normalizeTheme(themeValue);
    const sort = normalizeThemeSort(sortValue);
    const compatibility = getCompatibility(actionId, themeValue, sortValue);
    if (!selectedAction || !theme || !sort || !compatibility.supported) {
      throw new RangeError(`The theme action is unavailable: ${compatibility.reason || "invalid-selection"}.`);
    }
    const selection = theme.market === "krx"
      ? Object.freeze({
        groupCode: "PAGED_THEME",
        subject: theme.title,
        graphicType: sort.id,
        subtype: selectedAction.valueKind,
        dataCode: theme.themeCode
      })
      : Object.freeze({
        groupCode: "PAGED_NXT_THEME",
        subject: theme.displayTitle,
        graphicType: theme.session,
        subtype: sort.id,
        dataCode: theme.themeCode
      });
    return Object.freeze({
      builderKey: selectedAction.builderKey,
      code: selectedAction.code,
      aliases: selectedAction.aliases,
      selection,
      pageSize: selectedAction.pageSize,
      pagePreview: calculatePagePreview(actionId, theme)
    });
  }

  function requireOptions(options) {
    if (!options || typeof options !== "object" || Array.isArray(options)) {
      throw new TypeError("Theme playlist options are required.");
    }
    const id = cleanText(options.id);
    if (!id || !identifierPattern.test(id)) throw new TypeError("A safe theme 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 createThemePlaylistEntry(actionId, themeValue, sortValue, options) {
    const selectedAction = actionsById.get(actionId);
    if (!selectedAction) throw new RangeError("The theme action is unknown.");
    const theme = normalizeTheme(themeValue);
    if (!theme) throw new RangeError(`The theme selection is unavailable: ${themeValidationReason(themeValue)}.`);
    const sort = normalizeThemeSort(sortValue);
    if (!sort) throw new RangeError("The theme selection is unavailable: invalid-theme-sort.");
    const normalized = requireOptions(options);
    const mapping = buildThemeSelection(actionId, theme, sort);
    return {
      id: normalized.id,
      builderKey: mapping.builderKey,
      code: mapping.code,
      aliases: mapping.aliases,
      title: theme.displayTitle,
      detail: `${selectedAction.label} · ${sort.label}`,
      category: "plate",
      market: "theme",
      selection: mapping.selection,
      enabled: true,
      fadeDuration: normalized.fadeDuration,
      graphicType: mapping.selection.graphicType,
      subtype: mapping.selection.subtype,
      pageSize: mapping.pageSize,
      pagePreview: mapping.pagePreview,
      operator: Object.freeze({
        source: "legacy-theme-workflow",
        schemaVersion: SCHEMA_VERSION,
        actionId,
        theme,
        sort: sort.id,
        resolverGroup: mapping.selection.groupCode
      })
    };
  }

  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 samePagePreview(left, right) {
    if (!left || typeof left !== "object" || !right || typeof right !== "object") return false;
    return ["itemCount", "accessibleItemCount", "pageSize", "pageCount", "maximumPageCount", "isTruncated"]
      .every(key => left[key] === right[key]);
  }

  function restoreThemePlaylistEntry(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-theme-workflow" ||
        operator.schemaVersion !== SCHEMA_VERSION || !actionsById.has(operator.actionId)) return null;
    let recreated;
    try {
      recreated = createThemePlaylistEntry(operator.actionId, operator.theme, operator.sort, {
        id: value.id,
        fadeDuration: value.fadeDuration
      });
    } catch {
      return null;
    }
    const scalars = [
      "id", "builderKey", "code", "title", "detail", "category", "market",
      "fadeDuration", "graphicType", "subtype", "pageSize"
    ];
    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) ||
        !samePagePreview(value.pagePreview, recreated.pagePreview) ||
        operator.resolverGroup !== recreated.operator.resolverGroup) return null;
    return { ...recreated, enabled: value.enabled };
  }

  return {
    sourceContract,
    themeSorts,
    nxtSessions,
    actions,
    themeActions: actions,
    normalizeTheme,
    normalizeThemeSort,
    getCompatibility,
    isActionAllowed,
    calculatePagePreview,
    buildThemeSelection,
    createThemePlaylistEntry,
    restoreThemePlaylistEntry,
    refreshThemePlaylistEntry: restoreThemePlaylistEntry
  };
});
