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

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

  const SCHEMA_VERSION = 1;
  const MAXIMUM_VI_ITEMS = 100;
  const VI_PAGE_SIZE = 5;
  const MAXIMUM_PAGE_COUNT = 20;
  const VI_SNAPSHOT_SUBJECT = "@MBN_VI_SNAPSHOT_V1";
  const identifierPattern = /^[A-Za-z0-9_-]{1,128}$/;
  const rowVersionPattern = /^[a-f0-9]{64}$/;
  const unsafeTextPattern = /[\p{Cc}\p{Cf}\p{Cs}\p{Zl}\p{Zp}\uFFFD]/u;
  const audiences = Object.freeze(["INDIVIDUAL", "FOREIGN", "INSTITUTION"]);
  const statusCodes = new Set([
    "READY",
    "IMPORT_INTERRUPTED",
    "IMPORTED_STATE_INVALID",
    "INITIALIZATION_FAILED",
    "UNSAFE_CONFIGURATION"
  ]);
  const trustedNetSellReadResponses = new WeakSet();
  const trustedViReadResponses = new WeakSet();
  const trustedPlaylistEntries = new WeakMap();
  const audienceLabels = Object.freeze({
    INDIVIDUAL: "개인",
    FOREIGN: "외국인",
    INSTITUTION: "기관"
  });

  const bridgeContract = Object.freeze({
    statusRequestType: "request-manual-operator-data-status",
    statusResultType: "manual-operator-data-status",
    legacyImportType: "import-legacy-manual-operator-data",
    legacyImportResultType: "legacy-manual-operator-import-result",
    netSellReadType: "request-manual-net-sell-data",
    netSellDataType: "manual-net-sell-data",
    netSellSaveType: "save-manual-net-sell-data",
    netSellSaveResultType: "manual-net-sell-save-result",
    viReadType: "request-vi-manual-list",
    viDataType: "vi-manual-list-data",
    viSaveType: "save-vi-manual-list",
    viSaveResultType: "vi-manual-list-save-result",
    errorType: "manual-operator-data-error"
  });

  function hasExactKeys(value, keys) {
    if (!value || typeof value !== "object" || Array.isArray(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 isRequestId(value) {
    return typeof value === "string" && identifierPattern.test(value);
  }

  function isRowVersion(value) {
    return typeof value === "string" && rowVersionPattern.test(value);
  }

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

  function normalizeAudience(value) {
    return typeof value === "string" && audiences.includes(value) ? value : null;
  }

  function normalizeNetSellRow(value) {
    if (!hasExactKeys(value, ["leftName", "leftAmount", "rightName", "rightAmount"])) return null;
    const keys = ["leftName", "leftAmount", "rightName", "rightAmount"];
    if (!keys.every(key => isSafeText(value[key], 256))) return null;
    return Object.freeze({
      leftName: value.leftName,
      leftAmount: value.leftAmount,
      rightName: value.rightName,
      rightAmount: value.rightAmount
    });
  }

  function normalizeNetSellRows(value) {
    if (!Array.isArray(value) || value.length !== 5) return null;
    const rows = value.map(normalizeNetSellRow);
    return rows.every(Boolean) ? Object.freeze(rows) : null;
  }

  function normalizeViItem(value) {
    if (!hasExactKeys(value, ["code", "name"]) ||
        !isSafeText(value.code, 64, false) ||
        !isSafeText(value.name, 200, false) ||
        value.code !== value.code.trim() || value.name !== value.name.trim() ||
        value.name.includes(",")) return null;
    return Object.freeze({ code: value.code, name: value.name });
  }

  function normalizeViItems(value, allowEmpty = true) {
    if (!Array.isArray(value) || value.length > MAXIMUM_VI_ITEMS || (!allowEmpty && value.length === 0)) {
      return null;
    }
    const items = value.map(normalizeViItem);
    return items.every(Boolean) ? Object.freeze(items) : null;
  }

  function request(type, requestId, extra = {}) {
    if (!isRequestId(requestId)) throw new TypeError("A safe manual-data request id is required.");
    return Object.freeze({ type, payload: Object.freeze({ requestId, ...extra }) });
  }

  function createStatusRequest(requestId) {
    return request(bridgeContract.statusRequestType, requestId);
  }

  function createLegacyImportRequest(requestId) {
    return request(bridgeContract.legacyImportType, requestId);
  }

  function createNetSellReadRequest(requestId, audience) {
    const normalized = normalizeAudience(audience);
    if (!normalized) throw new RangeError("The manual net-sell audience is invalid.");
    return request(bridgeContract.netSellReadType, requestId, { audience: normalized });
  }

  function createNetSellSaveRequest(requestId, audience, rows) {
    const normalizedAudience = normalizeAudience(audience);
    const normalizedRows = normalizeNetSellRows(rows);
    if (!normalizedAudience || !normalizedRows) throw new TypeError("Five valid manual net-sell rows are required.");
    return request(bridgeContract.netSellSaveType, requestId, {
      audience: normalizedAudience,
      rows: normalizedRows
    });
  }

  function createViReadRequest(requestId) {
    return request(bridgeContract.viReadType, requestId);
  }

  function createViSaveRequest(requestId, items, expectedVersion) {
    const normalized = normalizeViItems(items);
    if (!normalized || !isRowVersion(expectedVersion)) {
      throw new TypeError("A valid VI manual list and its last-read version are required.");
    }
    return request(bridgeContract.viSaveType, requestId, {
      expectedVersion,
      items: normalized
    });
  }

  function normalizeStatusResponse(value, expectedRequestId) {
    if (!hasExactKeys(value, ["requestId", "available", "code", "writeQuarantined", "message"]) ||
        !isRequestId(value.requestId) ||
        typeof value.available !== "boolean" ||
        !statusCodes.has(value.code) ||
        value.available !== (value.code === "READY") ||
        typeof value.writeQuarantined !== "boolean" ||
        !(value.message === null || isSafeText(value.message, 512, false)) ||
        (expectedRequestId !== undefined && value.requestId !== expectedRequestId)) return null;
    return Object.freeze({ ...value });
  }

  function normalizeLegacyImportResponse(value, expectedRequestId) {
    if (!hasExactKeys(value, [
      "requestId", "markerVersion", "sourceSha256", "importedAt",
      "netSellRowCount", "viItemCount"
    ]) || !isRequestId(value.requestId) || value.markerVersion !== 1 ||
        !isRowVersion(value.sourceSha256) ||
        typeof value.importedAt !== "string" || !Number.isFinite(Date.parse(value.importedAt)) ||
        value.netSellRowCount !== 15 ||
        !Number.isInteger(value.viItemCount) || value.viItemCount < 0 ||
        value.viItemCount > MAXIMUM_VI_ITEMS ||
        (expectedRequestId !== undefined && value.requestId !== expectedRequestId)) return null;
    return Object.freeze({ ...value });
  }

  function normalizeNetSellDataResponse(value, expectedRequest) {
    if (!hasExactKeys(value, ["requestId", "audience", "rows"]) ||
        !isRequestId(value.requestId) ||
        !normalizeAudience(value.audience) ||
        (expectedRequest &&
          (value.requestId !== expectedRequest.requestId || value.audience !== expectedRequest.audience))) return null;
    const rows = normalizeNetSellRows(value.rows);
    if (!rows) return null;
    const response = Object.freeze({ requestId: value.requestId, audience: value.audience, rows });
    trustedNetSellReadResponses.add(response);
    return response;
  }

  function normalizeNetSellSaveResponse(value, expectedRequest) {
    if (!hasExactKeys(value, ["requestId", "audience", "saved", "recovered"]) ||
        !isRequestId(value.requestId) ||
        !normalizeAudience(value.audience) ||
        value.saved !== true || typeof value.recovered !== "boolean" ||
        (expectedRequest &&
          (value.requestId !== expectedRequest.requestId || value.audience !== expectedRequest.audience))) return null;
    return Object.freeze({ ...value });
  }

  function normalizeViDataResponse(value, expectedRequestId) {
    if (!hasExactKeys(value, ["requestId", "itemCount", "pageCount", "items", "version"]) ||
        !isRequestId(value.requestId) ||
        !isRowVersion(value.version) ||
        !Number.isInteger(value.itemCount) || value.itemCount < 0 || value.itemCount > MAXIMUM_VI_ITEMS ||
        !Number.isInteger(value.pageCount) || value.pageCount < 0 || value.pageCount > MAXIMUM_PAGE_COUNT ||
        value.pageCount !== (value.itemCount === 0 ? 0 : Math.ceil(value.itemCount / VI_PAGE_SIZE)) ||
        (expectedRequestId !== undefined && value.requestId !== expectedRequestId)) return null;
    const items = normalizeViItems(value.items);
    if (!items || items.length !== value.itemCount) return null;
    const response = Object.freeze({
      requestId: value.requestId,
      itemCount: value.itemCount,
      pageCount: value.pageCount,
      items,
      version: value.version
    });
    trustedViReadResponses.add(response);
    return response;
  }

  function normalizeViSaveResponse(value, expectedRequestId) {
    if (!hasExactKeys(value, [
      "requestId", "saved", "recovered", "persisted", "itemCount", "pageCount", "version"
    ]) ||
        !isRequestId(value.requestId) || value.saved !== true || typeof value.recovered !== "boolean" ||
        typeof value.persisted !== "boolean" || !isRowVersion(value.version) ||
        !Number.isInteger(value.itemCount) || value.itemCount < 0 || value.itemCount > MAXIMUM_VI_ITEMS ||
        !Number.isInteger(value.pageCount) ||
        value.pageCount !== (value.itemCount === 0 ? 0 : Math.ceil(value.itemCount / VI_PAGE_SIZE)) ||
        (expectedRequestId !== undefined && value.requestId !== expectedRequestId)) return null;
    return Object.freeze({ ...value });
  }

  function normalizeError(value, expectedRequestId) {
    if (!hasExactKeys(value, ["requestId", "operation", "code", "message", "retryable", "outcomeUnknown"]) ||
        !(value.requestId === "" || isRequestId(value.requestId)) ||
        !isSafeText(value.operation, 64, false) ||
        !isSafeText(value.code, 64, false) ||
        !isSafeText(value.message, 512, false) ||
        typeof value.retryable !== "boolean" || typeof value.outcomeUnknown !== "boolean" ||
        (value.outcomeUnknown && value.retryable) ||
        (expectedRequestId !== undefined && value.requestId !== expectedRequestId)) return null;
    return Object.freeze({ ...value });
  }

  function requireOptions(options) {
    const keys = options && typeof options === "object" ? Object.keys(options).sort() : [];
    const exact = keys.length === 2 && keys[0] === "fadeDuration" && keys[1] === "id";
    const exactWithEnabled = keys.length === 3 && keys[0] === "enabled" &&
      keys[1] === "fadeDuration" && keys[2] === "id";
    if ((!exact && !exactWithEnabled) || !isRequestId(options.id) ||
        !Number.isInteger(options.fadeDuration) || options.fadeDuration < 0 || options.fadeDuration > 60 ||
        (exactWithEnabled && typeof options.enabled !== "boolean")) {
      throw new TypeError("Safe manual-list playlist options are required.");
    }
    return Object.freeze({
      id: options.id,
      fadeDuration: options.fadeDuration,
      enabled: exactWithEnabled ? options.enabled : true
    });
  }

  function createNetSellEntryObject(audience, options) {
    const normalizedAudience = normalizeAudience(audience);
    const normalizedOptions = requireOptions(options);
    if (!normalizedAudience) throw new RangeError("The manual net-sell audience is invalid.");
    const label = audienceLabels[normalizedAudience];
    const selection = Object.freeze({
      groupCode: normalizedAudience,
      subject: "INDEX",
      graphicType: "MANUAL_NET_SELL",
      subtype: "MANUAL_NET_SELL",
      dataCode: ""
    });
    return {
      id: normalizedOptions.id,
      builderKey: "s5025",
      code: "5025",
      aliases: ["5025"],
      title: `${label} 순매도 상위(수동)`,
      detail: "매매동향 · 검증된 수동 입력 5행",
      category: "market",
      market: "index",
      selection,
      enabled: normalizedOptions.enabled,
      fadeDuration: normalizedOptions.fadeDuration,
      graphicType: selection.graphicType,
      subtype: selection.subtype,
      operator: Object.freeze({
        source: "legacy-manual-lists-workflow",
        schemaVersion: SCHEMA_VERSION,
        kind: "net-sell",
        audience: normalizedAudience
      })
    };
  }

  function createViEntryObject(items, rowVersion, options) {
    const normalizedItems = normalizeViItems(items, false);
    const normalizedOptions = requireOptions(options);
    if (!normalizedItems || !isRowVersion(rowVersion)) {
      throw new TypeError("A versioned non-empty VI snapshot is required.");
    }
    const pageCount = Math.ceil(normalizedItems.length / VI_PAGE_SIZE);
    const selection = Object.freeze({
      groupCode: "PAGED_VI",
      subject: VI_SNAPSHOT_SUBJECT,
      graphicType: "INPUT_ORDER",
      subtype: "CURRENT",
      dataCode: rowVersion
    });
    return {
      id: normalizedOptions.id,
      builderKey: "s5074",
      code: "5074",
      aliases: ["5074"],
      title: normalizedItems.map(item => item.name).join(", "),
      detail: "5단 표그래프 · VI 발동(수동)",
      category: "plate",
      market: "index",
      itemCount: normalizedItems.length,
      pageSize: VI_PAGE_SIZE,
      pageCount,
      selection,
      enabled: normalizedOptions.enabled,
      fadeDuration: normalizedOptions.fadeDuration,
      graphicType: selection.graphicType,
      subtype: selection.subtype,
      operator: Object.freeze({
        source: "legacy-manual-lists-workflow",
        schemaVersion: SCHEMA_VERSION,
        kind: "vi",
        items: normalizedItems,
        rowVersion,
        pagePreview: Object.freeze({
          itemCount: normalizedItems.length,
          pageSize: VI_PAGE_SIZE,
          pageCount,
          maximumPageCount: MAXIMUM_PAGE_COUNT
        })
      })
    };
  }

  function createNetSellPlaylistEntry(audience, options, freshRead) {
    const entry = createNetSellEntryObject(audience, options);
    if (trustedNetSellReadResponses.has(freshRead) &&
        freshRead.audience === entry.operator.audience && freshRead.rows.length === 5) {
      trustedPlaylistEntries.set(entry, freshRead);
    }
    return entry;
  }

  function createViPlaylistEntry(items, rowVersion, options, freshRead) {
    const entry = createViEntryObject(items, rowVersion, options);
    if (trustedViReadResponses.has(freshRead) && freshRead.version === entry.operator.rowVersion &&
        freshRead.itemCount === entry.itemCount && freshRead.pageCount === entry.pageCount &&
        freshRead.items.length === entry.operator.items.length &&
        freshRead.items.every((item, index) => item.code === entry.operator.items[index].code &&
          item.name === entry.operator.items[index].name)) {
      trustedPlaylistEntries.set(entry, freshRead);
    }
    return entry;
  }

  function isPlaylistEntryPlayable(value) {
    if (!value) return false;
    const freshRead = trustedPlaylistEntries.get(value);
    const restored = restorePlaylistEntry(value);
    if (!freshRead || !restored) return false;
    if (restored.operator.kind === "net-sell") {
      return trustedNetSellReadResponses.has(freshRead) && freshRead.rows.length === 5 &&
        freshRead.audience === restored.operator.audience;
    }
    return trustedViReadResponses.has(freshRead) && freshRead.version === restored.operator.rowVersion &&
      freshRead.itemCount === restored.itemCount && freshRead.pageCount === restored.pageCount &&
      freshRead.items.length === restored.operator.items.length &&
      freshRead.items.every((item, index) => item.code === restored.operator.items[index].code &&
        item.name === restored.operator.items[index].name);
  }

  function sameSelection(left, right) {
    const keys = ["groupCode", "subject", "graphicType", "subtype", "dataCode"];
    return hasExactKeys(left, keys) && hasExactKeys(right, keys) && keys
      .every(key => typeof left[key] === "string" && left[key] === right[key]);
  }

  function restorePlaylistEntry(value) {
    if (!value || typeof value !== "object" || Array.isArray(value) || typeof value.enabled !== "boolean") return null;
    const operator = value.operator;
    if (!operator || operator.source !== "legacy-manual-lists-workflow" ||
        operator.schemaVersion !== SCHEMA_VERSION || !["net-sell", "vi"].includes(operator.kind)) return null;
    const topKeys = operator.kind === "net-sell"
      ? [
          "id", "builderKey", "code", "aliases", "title", "detail", "category", "market",
          "selection", "enabled", "fadeDuration", "graphicType", "subtype", "operator"
        ]
      : [
          "id", "builderKey", "code", "aliases", "title", "detail", "category", "market",
          "itemCount", "pageSize", "pageCount", "selection", "enabled", "fadeDuration",
          "graphicType", "subtype", "operator"
        ];
    const operatorKeys = operator.kind === "net-sell"
      ? ["source", "schemaVersion", "kind", "audience"]
      : ["source", "schemaVersion", "kind", "items", "rowVersion", "pagePreview"];
    if (!hasExactKeys(value, topKeys) || !hasExactKeys(operator, operatorKeys)) return null;
    if (operator.kind === "vi" &&
        (!isRowVersion(operator.rowVersion) ||
         !hasExactKeys(operator.pagePreview, ["itemCount", "pageSize", "pageCount", "maximumPageCount"]))) {
      return null;
    }
    let recreated;
    try {
      const options = { id: value.id, fadeDuration: value.fadeDuration, enabled: value.enabled };
      recreated = operator.kind === "net-sell"
        ? createNetSellEntryObject(operator.audience, options)
        : createViEntryObject(operator.items, operator.rowVersion, options);
    } catch {
      return null;
    }
    const scalars = [
      "id", "builderKey", "code", "title", "detail", "category", "market",
      "fadeDuration", "graphicType", "subtype"
    ];
    if (!scalars.every(key => value[key] === recreated[key]) ||
        !Array.isArray(value.aliases) || value.aliases.length !== 1 || value.aliases[0] !== recreated.aliases[0] ||
        !sameSelection(value.selection, recreated.selection)) return null;
    if (operator.kind === "vi" &&
        (value.itemCount !== recreated.itemCount || value.pageSize !== recreated.pageSize ||
         value.pageCount !== recreated.pageCount ||
         operator.pagePreview.itemCount !== recreated.operator.pagePreview.itemCount ||
         operator.pagePreview.pageSize !== recreated.operator.pagePreview.pageSize ||
         operator.pagePreview.pageCount !== recreated.operator.pagePreview.pageCount ||
         operator.pagePreview.maximumPageCount !== recreated.operator.pagePreview.maximumPageCount)) return null;
    return recreated;
  }

  function withEnabled(entry, enabled) {
    const freshRead = trustedPlaylistEntries.get(entry);
    if (!freshRead || typeof enabled !== "boolean") return null;
    if (entry.enabled === enabled) return entry;
    const replacement = restorePlaylistEntry({ ...entry, enabled });
    if (!replacement) return null;
    trustedPlaylistEntries.set(replacement, freshRead);
    return replacement;
  }

  return {
    bridgeContract,
    audiences,
    audienceLabels,
    maximumViItems: MAXIMUM_VI_ITEMS,
    viPageSize: VI_PAGE_SIZE,
    maximumPageCount: MAXIMUM_PAGE_COUNT,
    viSnapshotSubject: VI_SNAPSHOT_SUBJECT,
    normalizeAudience,
    normalizeNetSellRow,
    normalizeNetSellRows,
    normalizeViItem,
    normalizeViItems,
    createStatusRequest,
    createLegacyImportRequest,
    createNetSellReadRequest,
    createNetSellSaveRequest,
    createViReadRequest,
    createViSaveRequest,
    normalizeStatusResponse,
    normalizeLegacyImportResponse,
    normalizeNetSellDataResponse,
    normalizeNetSellSaveResponse,
    normalizeViDataResponse,
    normalizeViSaveResponse,
    normalizeError,
    createNetSellPlaylistEntry,
    createViPlaylistEntry,
    isPlaylistEntryPlayable,
    restorePlaylistEntry,
    withEnabled,
    refreshPlaylistEntry: restorePlaylistEntry
  };
});
