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

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

  if (!overseas) throw new Error("Overseas workflow is unavailable.");

  const SCHEMA_VERSION = 1;
  const SOURCE = "legacy-named-foreign-stock-workflow";
  const requestIdPattern = /^[A-Za-z0-9_-]{1,128}$/;
  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 resolvedValues = new WeakSet();
  const materializedEntries = new WeakSet();
  const failureCodes = new Set([
    "INVALID_ROW",
    "IDENTITY_NOT_FOUND",
    "IDENTITY_AMBIGUOUS",
    "DATABASE_DATA_INVALID",
    "RESTORE_FAILED"
  ]);

  const actionProfiles = Object.freeze([
    Object.freeze({
      actionId: "stock-current",
      graphicType: "1열판기본",
      subtype: "",
      builderKey: "s5001",
      cutCode: "5001",
      valueType: "CURRENT",
      periodDays: null
    }),
    Object.freeze({
      actionId: "stock-candle-5d",
      graphicType: "캔들그래프",
      subtype: "5일",
      builderKey: "s8010",
      cutCode: "8061",
      valueType: "PRICE",
      periodDays: 5
    }),
    Object.freeze({
      actionId: "stock-candle-20d",
      graphicType: "캔들그래프",
      subtype: "20일",
      builderKey: "s8010",
      cutCode: "8040",
      valueType: "PRICE",
      periodDays: 20
    }),
    Object.freeze({
      actionId: "stock-candle-60d",
      graphicType: "캔들그래프",
      subtype: "60일",
      builderKey: "s8010",
      cutCode: "8046",
      valueType: "PRICE",
      periodDays: 60
    }),
    Object.freeze({
      actionId: "stock-candle-120d",
      graphicType: "캔들그래프",
      subtype: "120일",
      builderKey: "s8010",
      cutCode: "8051",
      valueType: "PRICE",
      periodDays: 120
    }),
    Object.freeze({
      actionId: "stock-candle-240d",
      graphicType: "캔들그래프",
      subtype: "240일",
      builderKey: "s8010",
      cutCode: "8056",
      valueType: "PRICE",
      periodDays: 240
    })
  ]);
  const profilesByAction = new Map(actionProfiles.map(value => [value.actionId, value]));
  const profilesByRaw = new Map(actionProfiles.map(value => [
    `${value.graphicType}\u0000${value.subtype}`,
    value
  ]));

  const bridgeContract = Object.freeze({
    resultType: "named-foreign-stock-restore-results",
    errorType: "named-foreign-stock-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 requireOptions(options) {
    if (!hasExactKeys(options, ["id", "fadeDuration", "ma5", "ma20"]) ||
        !requestIdPattern.test(options.id) ||
        !Number.isInteger(options.fadeDuration) ||
        options.fadeDuration < 0 || options.fadeDuration > 60 ||
        typeof options.ma5 !== "boolean" || typeof options.ma20 !== "boolean") return null;
    return Object.freeze({ ...options });
  }

  function rawSelection(raw) {
    return Object.freeze({
      groupCode: raw.groupCode,
      subject: raw.subject,
      graphicType: raw.graphicType,
      subtype: raw.subtype,
      dataCode: raw.dataCode
    });
  }

  function profileForRaw(raw) {
    return profilesByRaw.get(`${raw?.graphicType}\u0000${raw?.subtype}`) || null;
  }

  function safeRaw(raw) {
    return isPlainObject(raw) && Number.isInteger(raw.itemIndex) &&
      raw.itemIndex >= 0 && raw.itemIndex < 1000 &&
      typeof raw.enabled === "boolean" && raw.groupCode === "해외종목" &&
      safeText(raw.subject, 200) && raw.pageText === "1/1" && raw.dataCode === "" &&
      safeText(raw.graphicType, 256) && safeText(raw.subtype, 256, true) &&
      safeText(raw.pageText, 9) && safeText(raw.dataCode, 1024, true) &&
      Boolean(profileForRaw(raw));
  }

  function classify(raw, options) {
    const normalizedOptions = requireOptions(options);
    const profile = profileForRaw(raw);
    if (!normalizedOptions || !safeRaw(raw) || !profile) return null;
    const movingAverages = Object.freeze(profile.builderKey === "s8010"
      ? { ma5: normalizedOptions.ma5, ma20: normalizedOptions.ma20 }
      : { ma5: false, ma20: false });
    const pending = Object.freeze({
      itemIndex: raw.itemIndex,
      enabled: raw.enabled,
      actionId: profile.actionId,
      builderKey: profile.builderKey,
      cutCode: profile.cutCode,
      valueType: profile.valueType,
      periodDays: profile.periodDays,
      inputName: raw.subject,
      entry: Object.freeze({ id: normalizedOptions.id }),
      fadeDuration: normalizedOptions.fadeDuration,
      movingAverages,
      rawSelection: rawSelection(raw)
    });
    pendingValues.add(pending);
    return pending;
  }

  function normalizeOutcome(value, pending, verifiedAt) {
    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 });
    }
    if (value.status !== "resolved" || !hasExactKeys(value, [
      "itemIndex", "status", "actionId", "inputName", "symbol", "nationCode",
      "foreignDomesticCode", "builderKey", "cutCode", "valueType", "periodDays"
    ]) || value.actionId !== pending.actionId || value.inputName !== pending.inputName ||
        value.foreignDomesticCode !== "1" || value.builderKey !== pending.builderKey ||
        value.cutCode !== pending.cutCode || value.valueType !== pending.valueType ||
        value.periodDays !== pending.periodDays) return null;
    const identity = overseas.normalizeStock({
      inputName: value.inputName,
      symbol: value.symbol,
      nationCode: value.nationCode,
      fdtc: value.foreignDomesticCode
    });
    if (!identity) return null;
    const resolved = Object.freeze({
      itemIndex: value.itemIndex,
      status: "resolved",
      actionId: value.actionId,
      inputName: identity.inputName,
      symbol: identity.symbol,
      nationCode: identity.nationCode,
      foreignDomesticCode: identity.fdtc,
      builderKey: value.builderKey,
      cutCode: value.cutCode,
      valueType: value.valueType,
      periodDays: value.periodDays,
      verifiedAt
    });
    resolvedValues.add(resolved);
    return resolved;
  }

  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], value.retrievedAt);
      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 &&
      ["PLAYOUT_PRIORITY", "DATABASE_UNAVAILABLE", "RESTORE_FAILED"].includes(value.code) &&
      safeText(value.message, 1024) && value.retryable === false
      ? Object.freeze({ ...value })
      : null;
  }

  function customOperator(canonical, raw, verifiedAt) {
    return Object.freeze({
      source: SOURCE,
      schemaVersion: SCHEMA_VERSION,
      kind: "stock",
      actionId: canonical.operator.actionId,
      identity: canonical.operator.identity,
      movingAverages: canonical.operator.movingAverages,
      rawSelection: raw,
      verifiedAt
    });
  }

  function buildEntry(pending, outcome) {
    const canonical = overseas.createOverseasStockPlaylistEntry({
      inputName: outcome.inputName,
      symbol: outcome.symbol,
      nationCode: outcome.nationCode,
      fdtc: outcome.foreignDomesticCode
    }, outcome.actionId, {
      id: pending.entry.id,
      fadeDuration: pending.fadeDuration,
      ma5: pending.movingAverages.ma5,
      ma20: pending.movingAverages.ma20
    });
    if (canonical.builderKey !== outcome.builderKey || canonical.code !== outcome.cutCode) return null;
    return Object.freeze({
      ...canonical,
      enabled: pending.enabled,
      operator: customOperator(canonical, pending.rawSelection, outcome.verifiedAt)
    });
  }

  function materialize(pending, outcome) {
    if (!pendingValues.has(pending) || !resolvedValues.has(outcome) ||
        outcome.itemIndex !== pending.itemIndex || outcome.actionId !== pending.actionId) return null;
    let entry;
    try {
      entry = buildEntry(pending, outcome);
    } catch {
      return null;
    }
    if (!entry) return null;
    materializedEntries.add(entry);
    return entry;
  }

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

  function restorePlaylistEntry(value) {
    if (!hasExactKeys(value, [
          "id", "builderKey", "code", "aliases", "title", "detail", "category", "market",
          "symbol", "nationCode", "fdtc", "selection", "enabled", "fadeDuration",
          "graphicType", "subtype", "operator"
        ]) || typeof value.enabled !== "boolean" ||
        !isPlainObject(value.operator) || !hasExactKeys(value.operator, [
          "source", "schemaVersion", "kind", "actionId", "identity", "movingAverages",
          "rawSelection", "verifiedAt"
        ]) || value.operator.source !== SOURCE ||
        value.operator.schemaVersion !== SCHEMA_VERSION || value.operator.kind !== "stock" ||
        !profilesByAction.has(value.operator.actionId) ||
        !hasExactKeys(value.operator.identity, ["inputName", "symbol", "nationCode", "fdtc"]) ||
        !hasExactKeys(value.operator.movingAverages, ["ma5", "ma20"]) ||
        typeof value.operator.movingAverages.ma5 !== "boolean" ||
        typeof value.operator.movingAverages.ma20 !== "boolean" ||
        typeof value.operator.verifiedAt !== "string" ||
        !isoTimestampPattern.test(value.operator.verifiedAt) ||
        !Number.isFinite(Date.parse(value.operator.verifiedAt))) return null;
    const identity = overseas.normalizeStock(value.operator.identity);
    const raw = value.operator.rawSelection;
    const profile = profileForRaw(raw);
    if (!identity || !profile || profile.actionId !== value.operator.actionId ||
        !hasExactKeys(raw, ["groupCode", "subject", "graphicType", "subtype", "dataCode"]) ||
        raw.groupCode !== "해외종목" || raw.subject !== identity.inputName || raw.dataCode !== "" ||
        !safeText(raw.subject, 200) ||
        (profile.builderKey !== "s8010" &&
          (value.operator.movingAverages.ma5 || value.operator.movingAverages.ma20))) return null;

    let canonical;
    try {
      canonical = overseas.createOverseasStockPlaylistEntry(
        identity,
        profile.actionId,
        {
          id: value.id,
          fadeDuration: value.fadeDuration,
          ma5: value.operator.movingAverages.ma5,
          ma20: value.operator.movingAverages.ma20
        });
    } catch {
      return null;
    }
    const rebuilt = Object.freeze({
      ...canonical,
      enabled: value.enabled,
      operator: customOperator(canonical, raw, value.operator.verifiedAt)
    });
    const scalarKeys = [
      "id", "builderKey", "code", "title", "detail", "category", "market", "symbol",
      "nationCode", "fdtc", "enabled", "fadeDuration", "graphicType", "subtype"
    ];
    if (!scalarKeys.every(key => value[key] === rebuilt[key]) ||
        !Array.isArray(value.aliases) || value.aliases.length !== 1 ||
        value.aliases[0] !== rebuilt.code || !sameSelection(value.selection, rebuilt.selection) ||
        JSON.stringify(value.operator) !== JSON.stringify(rebuilt.operator)) return null;
    materializedEntries.add(rebuilt);
    return rebuilt;
  }

  function legacySelectionForEntry(entry) {
    const restored = restorePlaylistEntry(entry);
    return restored ? restored.operator.rawSelection : null;
  }

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

  function matchesRaw(entry, raw) {
    const signature = legacySelectionForEntry(entry);
    return Boolean(signature && safeRaw(raw) && entry.enabled === raw.enabled &&
      ["groupCode", "subject", "graphicType", "subtype", "dataCode"]
        .every(key => signature[key] === raw[key]));
  }

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

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

  return {
    source: SOURCE,
    bridgeContract,
    actionProfiles,
    classify,
    normalizeResponse,
    normalizeError,
    materialize,
    restorePlaylistEntry,
    withEnabled,
    legacySelectionForEntry,
    matchesRaw,
    isPending,
    isMaterializedEntry
  };
});
