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

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

  const DEFAULT_FADE_DURATION = 6;
  const DEFAULT_MAXIMUM_RESULTS = 100;
  const MAXIMUM_RESULTS = 500;
  const MAXIMUM_QUERY_LENGTH = 64;
  const SCHEMA_VERSION = 1;
  const identifierPattern = /^[A-Za-z0-9_-]{1,128}$/;
  const stockCodePattern = /^[A-Za-z0-9]{1,32}$/;
  const unsafeTextPattern = /[\p{Cc}\p{Cf}\p{Cs}\p{Zl}\p{Zp}\uFFFD]/u;
  const markets = Object.freeze(["kospi", "kosdaq"]);

  const bridgeContract = Object.freeze({
    requestType: "search-trading-halts",
    resultType: "trading-halt-results",
    errorType: "trading-halt-error",
    defaultMaximumResults: DEFAULT_MAXIMUM_RESULTS,
    maximumResults: MAXIMUM_RESULTS,
    maximumQueryLength: MAXIMUM_QUERY_LENGTH
  });

  const sourceContract = Object.freeze({
    originalControl: "Control/UC7.cs",
    currentCutCode: "5001",
    candleCutCode: "8051",
    candlePeriodDays: 120,
    currentResolverGroups: Object.freeze({ kospi: "KOSPI", kosdaq: "KOSDAQ" }),
    candleResolverGroups: Object.freeze({ kospi: "KOSPI_STOCK", kosdaq: "KOSDAQ_STOCK" })
  });

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

  const actions = Object.freeze([
    action("current", "\uAC70\uB798\uC815\uC9C0-\uD604\uC7AC\uAC00", "s5001", "5001", "current"),
    action("candle-120d", "\uCE94\uB4E4\uADF8\uB798\uD504-\uAC70\uB798\uC815\uC9C0 · 120\uC77C", "s8010", "8051", "candle")
  ]);
  const actionsById = new Map(actions.map(value => [value.id, value]));

  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 isSafeCanonicalText(value, maximumLength) {
    return typeof value === "string" &&
      value.length > 0 &&
      value.length <= maximumLength &&
      value === value.trim() &&
      !unsafeTextPattern.test(value);
  }

  function normalizeQuery(value) {
    if (typeof value !== "string" || value.length > MAXIMUM_QUERY_LENGTH || unsafeTextPattern.test(value)) {
      return null;
    }
    const query = value.trim();
    return query.length <= MAXIMUM_QUERY_LENGTH && !unsafeTextPattern.test(query) ? query : null;
  }

  function normalizeTradingHalt(value) {
    if (!hasExactKeys(value, ["market", "stockCode", "displayName"]) ||
        !markets.includes(value.market) ||
        !isSafeCanonicalText(value.stockCode, 32) ||
        !stockCodePattern.test(value.stockCode) ||
        !isSafeCanonicalText(value.displayName, 200)) {
      return null;
    }
    return Object.freeze({
      market: value.market,
      stockCode: value.stockCode,
      displayName: value.displayName
    });
  }

  function createTradingHaltSearchRequest(requestId, query = "", maximumResults) {
    if (!isSafeCanonicalText(requestId, 128) || !identifierPattern.test(requestId)) {
      throw new TypeError("A safe trading-halt request id is required.");
    }
    const normalizedQuery = normalizeQuery(query);
    if (normalizedQuery === null) {
      throw new TypeError(`A trading-halt query must contain at most ${MAXIMUM_QUERY_LENGTH} safe characters.`);
    }
    if (maximumResults !== undefined &&
        (!Number.isInteger(maximumResults) || maximumResults < 1 || maximumResults > MAXIMUM_RESULTS)) {
      throw new RangeError(`Trading-halt results must be limited from 1 through ${MAXIMUM_RESULTS}.`);
    }
    const request = { requestId, query: normalizedQuery };
    if (maximumResults !== undefined) request.maximumResults = maximumResults;
    return Object.freeze(request);
  }

  function compareTradingHalts(left, right) {
    const marketOrder = markets.indexOf(left.market) - markets.indexOf(right.market);
    if (marketOrder !== 0) return marketOrder;
    if (left.displayName < right.displayName) return -1;
    if (left.displayName > right.displayName) return 1;
    if (left.stockCode < right.stockCode) return -1;
    if (left.stockCode > right.stockCode) return 1;
    return 0;
  }

  function tradingHaltIdentity(value) {
    const normalized = normalizeTradingHalt(value);
    return normalized ? `${normalized.market}\u0000${normalized.stockCode}` : "";
  }

  function nextTradingHaltNameSort(direction) {
    return direction === "descending" ? "ascending" : "descending";
  }

  function compareCanonicalText(left, right) {
    if (left < right) return -1;
    if (left > right) return 1;
    return 0;
  }

  function sortTradingHaltsForDisplay(values, direction) {
    if (!Array.isArray(values)) throw new TypeError("Trading-halt display rows are required.");
    if (direction !== null && direction !== "ascending" && direction !== "descending") {
      throw new TypeError("Trading-halt name sort must be ascending, descending, or null.");
    }
    if (direction === null) return [...values];

    const nameDirection = direction === "ascending" ? 1 : -1;
    return values.map((value, sourceIndex) => {
      const normalized = normalizeTradingHalt(value);
      if (!normalized) throw new TypeError("A valid trading-halt display row is required.");
      return { value, normalized, sourceIndex };
    }).sort((left, right) => {
      const nameOrder = compareCanonicalText(
        left.normalized.displayName,
        right.normalized.displayName);
      if (nameOrder !== 0) return nameOrder * nameDirection;

      const marketOrder = compareCanonicalText(left.normalized.market, right.normalized.market);
      if (marketOrder !== 0) return marketOrder;
      const stockCodeOrder = compareCanonicalText(
        left.normalized.stockCode,
        right.normalized.stockCode);
      return stockCodeOrder !== 0 ? stockCodeOrder : left.sourceIndex - right.sourceIndex;
    }).map(item => item.value);
  }

  function normalizeTradingHaltSearchResponse(value, expectedRequest) {
    if (!hasExactKeys(value, [
      "requestId", "query", "retrievedAt", "totalRowCount", "truncated", "results"
    ]) ||
        !isSafeCanonicalText(value.requestId, 128) ||
        !identifierPattern.test(value.requestId) ||
        normalizeQuery(value.query) !== value.query ||
        !isSafeCanonicalText(value.retrievedAt, 64) ||
        !Number.isFinite(Date.parse(value.retrievedAt)) ||
        !Number.isInteger(value.totalRowCount) ||
        value.totalRowCount < 0 || value.totalRowCount > MAXIMUM_RESULTS ||
        typeof value.truncated !== "boolean" ||
        !Array.isArray(value.results) ||
        value.results.length !== value.totalRowCount) {
      return null;
    }

    if (expectedRequest !== undefined) {
      let expected;
      try {
        expected = createTradingHaltSearchRequest(
          expectedRequest?.requestId,
          expectedRequest?.query,
          expectedRequest?.maximumResults);
      } catch {
        return null;
      }
      if (value.requestId !== expected.requestId || value.query !== expected.query ||
          (expected.maximumResults !== undefined && value.results.length > expected.maximumResults)) {
        return null;
      }
    }

    const results = [];
    const identities = new Set();
    const lookupKeys = new Set();
    for (const raw of value.results) {
      const item = normalizeTradingHalt(raw);
      if (!item) return null;
      const identity = `${item.market}\u0000${item.stockCode}`;
      const lookupKey = `${item.market}\u0000${item.displayName}`;
      if (identities.has(identity) || lookupKeys.has(lookupKey)) return null;
      if (results.length && compareTradingHalts(results[results.length - 1], item) > 0) return null;
      identities.add(identity);
      lookupKeys.add(lookupKey);
      results.push(item);
    }

    return Object.freeze({
      requestId: value.requestId,
      query: value.query,
      retrievedAt: value.retrievedAt,
      totalRowCount: value.totalRowCount,
      truncated: value.truncated,
      results: Object.freeze(results)
    });
  }

  function normalizeTradingHaltError(value, expectedRequest) {
    if (!hasExactKeys(value, ["requestId", "query", "message"]) ||
        !isSafeCanonicalText(value.requestId, 128) ||
        !identifierPattern.test(value.requestId) ||
        normalizeQuery(value.query) !== value.query ||
        !isSafeCanonicalText(value.message, 512)) {
      return null;
    }
    if (expectedRequest &&
        (value.requestId !== expectedRequest.requestId || value.query !== normalizeQuery(expectedRequest.query))) {
      return null;
    }
    return Object.freeze({ requestId: value.requestId, query: value.query, message: value.message });
  }

  function getAction(actionId) {
    return typeof actionId === "string" ? actionsById.get(actionId) || null : null;
  }

  function requireOptions(options) {
    if (!options || typeof options !== "object" || Array.isArray(options)) {
      throw new TypeError("Trading-halt playlist options are required.");
    }
    if (!isSafeCanonicalText(options.id, 128) || !identifierPattern.test(options.id)) {
      throw new TypeError("A safe trading-halt playlist id is required.");
    }
    const fadeDuration = options.fadeDuration === undefined ? DEFAULT_FADE_DURATION : options.fadeDuration;
    if (!Number.isInteger(fadeDuration) || fadeDuration < 0 || fadeDuration > 60) {
      throw new RangeError("Fade duration must be an integer from 0 through 60.");
    }
    const ma5 = options.ma5 === undefined ? false : options.ma5;
    const ma20 = options.ma20 === undefined ? false : options.ma20;
    if (typeof ma5 !== "boolean" || typeof ma20 !== "boolean") {
      throw new TypeError("Moving-average flags must be boolean values.");
    }
    return Object.freeze({ id: options.id, fadeDuration, ma5, ma20 });
  }

  function buildTradingHaltSelection(actionId, tradingHalt, options = {}) {
    const selectedAction = getAction(actionId);
    const selected = normalizeTradingHalt(tradingHalt);
    if (!selectedAction) throw new RangeError("The trading-halt action is unknown.");
    if (!selected) throw new TypeError("A valid trading-halt selection is required.");
    const ma5 = options.ma5 === undefined ? false : options.ma5;
    const ma20 = options.ma20 === undefined ? false : options.ma20;
    if (typeof ma5 !== "boolean" || typeof ma20 !== "boolean") {
      throw new TypeError("Moving-average flags must be boolean values.");
    }
    const movingAverages = Object.freeze({
      ma5: selectedAction.kind === "candle" && ma5,
      ma20: selectedAction.kind === "candle" && ma20
    });
    const flags = [];
    if (movingAverages.ma5) flags.push("MA5");
    if (movingAverages.ma20) flags.push("MA20");
    const selection = selectedAction.kind === "current"
      ? Object.freeze({
        groupCode: sourceContract.currentResolverGroups[selected.market],
        subject: selected.displayName,
        graphicType: "\u0031\uC5F4\uD310\uAE30\uBCF8",
        subtype: "HALTED",
        dataCode: selected.stockCode
      })
      : Object.freeze({
        groupCode: sourceContract.candleResolverGroups[selected.market],
        subject: selected.displayName,
        graphicType: flags.join(","),
        subtype: "TRADING_HALT",
        dataCode: selected.stockCode
      });
    return Object.freeze({
      builderKey: selectedAction.builderKey,
      code: selectedAction.code,
      aliases: selectedAction.aliases,
      selection,
      movingAverages
    });
  }

  function createTradingHaltPlaylistEntry(tradingHalt, actionId, options) {
    const selected = normalizeTradingHalt(tradingHalt);
    if (!selected) throw new TypeError("A valid trading-halt selection is required.");
    const selectedAction = getAction(actionId);
    if (!selectedAction) throw new RangeError("The trading-halt action is unknown.");
    const normalizedOptions = requireOptions(options);
    const mapping = buildTradingHaltSelection(actionId, selected, normalizedOptions);
    return {
      id: normalizedOptions.id,
      builderKey: mapping.builderKey,
      code: mapping.code,
      aliases: [...mapping.aliases],
      title: selected.displayName,
      detail: selectedAction.label,
      category: selectedAction.kind === "candle" ? "chart" : "stock",
      market: selected.market,
      stockCode: selected.stockCode,
      displayName: selected.displayName,
      selection: mapping.selection,
      enabled: true,
      fadeDuration: normalizedOptions.fadeDuration,
      graphicType: mapping.selection.graphicType,
      subtype: mapping.selection.subtype,
      operator: Object.freeze({
        source: "legacy-trading-halt-workflow",
        schemaVersion: SCHEMA_VERSION,
        actionId: selectedAction.id,
        tradingHalt: selected,
        movingAverages: mapping.movingAverages,
        resolverGroup: mapping.selection.groupCode
      })
    };
  }

  function sameStringArray(left, right) {
    return Array.isArray(left) && Array.isArray(right) &&
      left.length === right.length && left.every((value, index) => value === right[index]);
  }

  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 restoreTradingHaltPlaylistEntry(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-trading-halt-workflow" ||
        operator.schemaVersion !== SCHEMA_VERSION ||
        !actionsById.has(operator.actionId) ||
        !operator.movingAverages || typeof operator.movingAverages !== "object" ||
        typeof operator.movingAverages.ma5 !== "boolean" ||
        typeof operator.movingAverages.ma20 !== "boolean") {
      return null;
    }

    let recreated;
    try {
      recreated = createTradingHaltPlaylistEntry(operator.tradingHalt, operator.actionId, {
        id: value.id,
        fadeDuration: value.fadeDuration,
        ma5: operator.movingAverages.ma5,
        ma20: operator.movingAverages.ma20
      });
    } catch {
      return null;
    }

    const scalars = [
      "id", "builderKey", "code", "title", "detail", "category", "market",
      "stockCode", "displayName", "fadeDuration", "graphicType", "subtype"
    ];
    if (!scalars.every(key => value[key] === recreated[key]) ||
        !sameStringArray(value.aliases, recreated.aliases) ||
        !sameSelection(value.selection, recreated.selection) ||
        operator.movingAverages.ma5 !== recreated.operator.movingAverages.ma5 ||
        operator.movingAverages.ma20 !== recreated.operator.movingAverages.ma20 ||
        operator.resolverGroup !== recreated.operator.resolverGroup) {
      return null;
    }
    return { ...recreated, enabled: value.enabled };
  }

  return {
    bridgeContract,
    sourceContract,
    markets,
    actions,
    tradingHaltActions: actions,
    normalizeTradingHalt,
    normalizeTradingHaltResult: normalizeTradingHalt,
    tradingHaltIdentity,
    nextTradingHaltNameSort,
    sortTradingHaltsForDisplay,
    createTradingHaltSearchRequest,
    normalizeTradingHaltSearchResponse,
    normalizeTradingHaltError,
    getAction,
    buildTradingHaltSelection,
    createTradingHaltPlaylistEntry,
    restoreTradingHaltPlaylistEntry,
    refreshTradingHaltPlaylistEntry: restoreTradingHaltPlaylistEntry
  };
});
