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

  const api = Object.freeze(factory());
  if (typeof module === "object" && module.exports) module.exports = api;
  if (root) root.MbnExpertWorkflow = 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 DEFAULT_MAXIMUM_PREVIEW_ITEMS = 100;
  const MAXIMUM_PREVIEW_ITEMS = 100;
  const MAXIMUM_PAGE_COUNT = 20;
  const PAGE_SIZE = 5;
  const SCHEMA_VERSION = 1;
  const identifierPattern = /^[A-Za-z0-9_-]{1,128}$/;
  const expertCodePattern = /^[0-9]{4}$/;
  const stockCodePattern = /^[A-Za-z0-9]{1,32}$/;
  const unsafeTextPattern = /[\p{Cc}\p{Cf}\p{Cs}\p{Zl}\p{Zp}\uFFFD]/u;

  const bridgeContract = Object.freeze({
    searchRequestType: "search-experts",
    searchResultType: "expert-search-results",
    searchErrorType: "expert-search-error",
    previewRequestType: "request-expert-preview",
    previewResultType: "expert-preview-results",
    previewErrorType: "expert-preview-error",
    defaultMaximumResults: DEFAULT_MAXIMUM_RESULTS,
    maximumResults: MAXIMUM_RESULTS,
    maximumQueryLength: MAXIMUM_QUERY_LENGTH,
    defaultMaximumPreviewItems: DEFAULT_MAXIMUM_PREVIEW_ITEMS,
    maximumPreviewItems: MAXIMUM_PREVIEW_ITEMS
  });

  const sourceContract = Object.freeze({
    originalControl: "Control/UC6.cs",
    provider: "oracle",
    expertTable: "EXPERT_LIST",
    recommendationTable: "RECOMMEND_LIST",
    originalGroupLabel: "전문가 추천",
    originalCutLabel: "5단 표그래프",
    originalValueLabel: "현재가",
    builderKey: "s5074",
    cutCode: "5074",
    resolverGroup: "PAGED_EXPERT",
    pageSize: PAGE_SIZE,
    maximumPageCount: MAXIMUM_PAGE_COUNT
  });

  const actions = Object.freeze([
    Object.freeze({
      id: "five-row-current",
      label: "전문가 추천 · 5단 표그래프 · 현재가",
      builderKey: "s5074",
      code: "5074",
      aliases: Object.freeze(["5074"]),
      pageSize: PAGE_SIZE
    })
  ]);
  const action = actions[0];
  const trustedPreviewResponses = new WeakSet();

  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 hasOnlyKeys(value, keys) {
    return value && typeof value === "object" && !Array.isArray(value) &&
      Object.keys(value).every(key => keys.includes(key));
  }

  function isSafeCanonicalText(value, maximumLength) {
    return typeof value === "string" &&
      value.length > 0 && value.length <= maximumLength &&
      value === value.trim() && !unsafeTextPattern.test(value);
  }

  function isSafeRequestId(value) {
    return isSafeCanonicalText(value, 128) && identifierPattern.test(value);
  }

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

  function normalizeExpertIdentity(value) {
    if (!hasExactKeys(value, ["expertCode", "expertName"]) ||
        !expertCodePattern.test(value.expertCode) ||
        !isSafeCanonicalText(value.expertName, 128)) return null;
    return Object.freeze({
      expertCode: value.expertCode,
      expertName: value.expertName
    });
  }

  function normalizeExpert(value) {
    if (!hasExactKeys(value, ["expertCode", "expertName", "source"]) ||
        value.source !== "oracle") return null;
    const identity = normalizeExpertIdentity({
      expertCode: value.expertCode,
      expertName: value.expertName
    });
    return identity && Object.freeze({ ...identity, source: "oracle" });
  }

  function createExpertSearchRequest(requestId, query = "", maximumResults) {
    if (!isSafeRequestId(requestId)) {
      throw new TypeError("A safe expert-search request id is required.");
    }
    const normalizedQuery = normalizeQuery(query);
    if (normalizedQuery === null) {
      throw new TypeError(`An expert query must contain at most ${MAXIMUM_QUERY_LENGTH} safe characters.`);
    }
    if (maximumResults !== undefined &&
        (!Number.isInteger(maximumResults) || maximumResults < 1 || maximumResults > MAXIMUM_RESULTS)) {
      throw new RangeError(`Expert 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 compareExperts(left, right) {
    if (left.expertName < right.expertName) return -1;
    if (left.expertName > right.expertName) return 1;
    if (left.expertCode < right.expertCode) return -1;
    if (left.expertCode > right.expertCode) return 1;
    return 0;
  }

  function normalizeExpertSearchResponse(value, expectedRequest) {
    if (!hasExactKeys(value, [
      "requestId", "query", "retrievedAt", "totalRowCount", "truncated", "results"
    ]) || !isSafeRequestId(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;

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

    const results = [];
    const codes = new Set();
    const names = new Set();
    for (const raw of value.results) {
      const expert = normalizeExpert(raw);
      if (!expert || codes.has(expert.expertCode) || names.has(expert.expertName) ||
          (results.length && compareExperts(results[results.length - 1], expert) > 0)) return null;
      codes.add(expert.expertCode);
      names.add(expert.expertName);
      results.push(expert);
    }
    return Object.freeze({
      requestId: value.requestId,
      query: value.query,
      retrievedAt: value.retrievedAt,
      totalRowCount: value.totalRowCount,
      truncated: value.truncated,
      results: Object.freeze(results)
    });
  }

  function normalizeExpertSearchError(value, expectedRequest) {
    if (!hasExactKeys(value, ["requestId", "query", "message"]) ||
        !isSafeRequestId(value.requestId) || normalizeQuery(value.query) !== value.query ||
        !isSafeCanonicalText(value.message, 512)) return null;
    if (expectedRequest) {
      let expected;
      try {
        expected = createExpertSearchRequest(
          expectedRequest.requestId,
          expectedRequest.query,
          expectedRequest.maximumResults);
      } catch {
        return null;
      }
      if (value.requestId !== expected.requestId || value.query !== expected.query) return null;
    }
    return Object.freeze({ requestId: value.requestId, query: value.query, message: value.message });
  }

  function createExpertPreviewRequest(requestId, expertValue, maximumItems) {
    if (!isSafeRequestId(requestId)) {
      throw new TypeError("A safe expert-preview request id is required.");
    }
    const expert = normalizeExpert(expertValue);
    if (!expert) throw new TypeError("A typed Oracle expert is required.");
    if (maximumItems !== undefined &&
        (!Number.isInteger(maximumItems) || maximumItems < 1 || maximumItems > MAXIMUM_PREVIEW_ITEMS)) {
      throw new RangeError(`Expert preview items must be limited from 1 through ${MAXIMUM_PREVIEW_ITEMS}.`);
    }
    const request = {
      requestId,
      expertCode: expert.expertCode,
      expertName: expert.expertName
    };
    if (maximumItems !== undefined) request.maximumItems = maximumItems;
    return Object.freeze(request);
  }

  function normalizeRecommendation(value) {
    if (!hasExactKeys(value, ["playIndex", "stockCode", "stockName", "buyAmount"]) ||
        !Number.isInteger(value.playIndex) || value.playIndex < 0 || value.playIndex > 9999 ||
        typeof value.stockCode !== "string" || !stockCodePattern.test(value.stockCode) ||
        !isSafeCanonicalText(value.stockName, 200) ||
        !Number.isSafeInteger(value.buyAmount) || value.buyAmount <= 0) return null;
    return Object.freeze({
      playIndex: value.playIndex,
      stockCode: value.stockCode,
      stockName: value.stockName,
      buyAmount: value.buyAmount
    });
  }

  function normalizeExpertPreviewResponse(value, expectedRequest) {
    if (!hasExactKeys(value, [
      "requestId", "selection", "retrievedAt", "totalRowCount", "truncated", "items"
    ]) || !isSafeRequestId(value.requestId) ||
        !isSafeCanonicalText(value.retrievedAt, 64) || !Number.isFinite(Date.parse(value.retrievedAt)) ||
        !Number.isInteger(value.totalRowCount) || value.totalRowCount < 0 ||
        value.totalRowCount > MAXIMUM_PREVIEW_ITEMS || typeof value.truncated !== "boolean" ||
        !Array.isArray(value.items) || value.items.length !== value.totalRowCount) return null;
    const selection = normalizeExpertIdentity(value.selection);
    if (!selection) return null;

    if (expectedRequest !== undefined) {
      let expected;
      try {
        expected = createExpertPreviewRequest(
          expectedRequest?.requestId,
          {
            expertCode: expectedRequest?.expertCode,
            expertName: expectedRequest?.expertName,
            source: "oracle"
          },
          expectedRequest?.maximumItems);
      } catch {
        return null;
      }
      const bound = expected.maximumItems ?? DEFAULT_MAXIMUM_PREVIEW_ITEMS;
      if (value.requestId !== expected.requestId ||
          selection.expertCode !== expected.expertCode ||
          selection.expertName !== expected.expertName ||
          value.items.length > bound || (value.truncated && value.items.length !== bound)) return null;
    }

    const items = [];
    const indexes = new Set();
    const codes = new Set();
    const names = new Set();
    for (const raw of value.items) {
      const item = normalizeRecommendation(raw);
      if (!item || indexes.has(item.playIndex) || codes.has(item.stockCode) || names.has(item.stockName) ||
          (items.length && items[items.length - 1].playIndex >= item.playIndex)) return null;
      indexes.add(item.playIndex);
      codes.add(item.stockCode);
      names.add(item.stockName);
      items.push(item);
    }
    const result = Object.freeze({
      requestId: value.requestId,
      selection,
      retrievedAt: value.retrievedAt,
      totalRowCount: value.totalRowCount,
      truncated: value.truncated,
      items: Object.freeze(items)
    });
    trustedPreviewResponses.add(result);
    return result;
  }

  function normalizeExpertPreviewError(value, expectedRequest) {
    if (!hasExactKeys(value, ["requestId", "selection", "message"]) ||
        !isSafeRequestId(value.requestId) || !isSafeCanonicalText(value.message, 512)) return null;
    const selection = normalizeExpertIdentity(value.selection);
    if (!selection) return null;
    if (expectedRequest) {
      let expected;
      try {
        expected = createExpertPreviewRequest(
          expectedRequest.requestId,
          {
            expertCode: expectedRequest.expertCode,
            expertName: expectedRequest.expertName,
            source: "oracle"
          },
          expectedRequest.maximumItems);
      } catch {
        return null;
      }
      if (value.requestId !== expected.requestId ||
          selection.expertCode !== expected.expertCode ||
          selection.expertName !== expected.expertName) return null;
    }
    return Object.freeze({ requestId: value.requestId, selection, message: value.message });
  }

  function createSelectableExpert(expertValue, previewValue) {
    const expert = normalizeExpert(expertValue);
    if (!expert || !previewValue || typeof previewValue !== "object" ||
        !trustedPreviewResponses.has(previewValue) ||
        !normalizeExpertIdentity(previewValue.selection) ||
        previewValue.selection.expertCode !== expert.expertCode ||
        previewValue.selection.expertName !== expert.expertName ||
        !Number.isInteger(previewValue.totalRowCount) || previewValue.totalRowCount < 0 ||
        previewValue.totalRowCount > MAXIMUM_PREVIEW_ITEMS ||
        typeof previewValue.truncated !== "boolean") return null;
    return Object.freeze({
      ...expert,
      itemCount: previewValue.totalRowCount,
      previewTruncated: previewValue.truncated
    });
  }

  function normalizeSelectableExpert(value) {
    if (!hasExactKeys(value, [
      "expertCode", "expertName", "source", "itemCount", "previewTruncated"
    ]) || !Number.isInteger(value.itemCount) || value.itemCount < 0 ||
        value.itemCount > MAXIMUM_PREVIEW_ITEMS || typeof value.previewTruncated !== "boolean") return null;
    const expert = normalizeExpert({
      expertCode: value.expertCode,
      expertName: value.expertName,
      source: value.source
    });
    return expert && Object.freeze({
      ...expert,
      itemCount: value.itemCount,
      previewTruncated: value.previewTruncated
    });
  }

  function calculatePagePreview(expertValue) {
    const expert = normalizeSelectableExpert(expertValue);
    if (!expert) return null;
    return Object.freeze({
      itemCount: expert.itemCount,
      accessibleItemCount: expert.itemCount,
      pageSize: PAGE_SIZE,
      pageCount: Math.ceil(expert.itemCount / PAGE_SIZE),
      maximumPageCount: MAXIMUM_PAGE_COUNT,
      isTruncated: expert.previewTruncated
    });
  }

  function buildExpertSelection(expertValue) {
    const expert = normalizeSelectableExpert(expertValue);
    if (!expert) throw new TypeError("A previewed expert selection is required.");
    return Object.freeze({
      builderKey: action.builderKey,
      code: action.code,
      aliases: action.aliases,
      selection: Object.freeze({
        groupCode: "PAGED_EXPERT",
        subject: expert.expertName,
        graphicType: "INPUT_ORDER",
        subtype: "CURRENT",
        dataCode: expert.expertCode
      })
    });
  }

  function requireOptions(options) {
    if (!hasOnlyKeys(options, ["id", "fadeDuration"]) || !isSafeRequestId(options.id)) {
      throw new TypeError("Safe expert playlist options are 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.");
    }
    return Object.freeze({ id: options.id, fadeDuration });
  }

  function createExpertPlaylistEntry(expertValue, options) {
    const expert = normalizeSelectableExpert(expertValue);
    if (!expert) throw new TypeError("A previewed expert selection is required.");
    if (expert.itemCount === 0) throw new RangeError("An expert with no recommendation rows cannot create a cut.");
    const normalizedOptions = requireOptions(options);
    const mapping = buildExpertSelection(expert);
    const pagePreview = calculatePagePreview(expert);
    return {
      id: normalizedOptions.id,
      builderKey: mapping.builderKey,
      code: mapping.code,
      aliases: [...mapping.aliases],
      title: expert.expertName,
      detail: action.label,
      category: "expert",
      source: expert.source,
      expertCode: expert.expertCode,
      expertName: expert.expertName,
      itemCount: expert.itemCount,
      previewTruncated: expert.previewTruncated,
      pageSize: pagePreview.pageSize,
      pageCount: pagePreview.pageCount,
      selection: mapping.selection,
      enabled: true,
      fadeDuration: normalizedOptions.fadeDuration,
      graphicType: mapping.selection.graphicType,
      subtype: mapping.selection.subtype,
      operator: Object.freeze({
        source: "legacy-expert-workflow",
        schemaVersion: SCHEMA_VERSION,
        actionId: action.id,
        expert,
        pagePreview
      })
    };
  }

  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) {
    return left && right && ["groupCode", "subject", "graphicType", "subtype", "dataCode"]
      .every(key => typeof left[key] === "string" && left[key] === right[key]);
  }

  function samePagePreview(left, right) {
    return left && right && [
      "itemCount", "accessibleItemCount", "pageSize", "pageCount",
      "maximumPageCount", "isTruncated"
    ].every(key => left[key] === right[key]);
  }

  function restoreExpertPlaylistEntry(value) {
    if (!value || typeof value !== "object" || Array.isArray(value) ||
        typeof value.enabled !== "boolean") return null;
    const operator = value.operator;
    if (!hasExactKeys(operator, ["source", "schemaVersion", "actionId", "expert", "pagePreview"]) ||
        operator.source !== "legacy-expert-workflow" || operator.schemaVersion !== SCHEMA_VERSION ||
        operator.actionId !== action.id || !normalizeSelectableExpert(operator.expert)) return null;

    let recreated;
    try {
      recreated = createExpertPlaylistEntry(operator.expert, {
        id: value.id,
        fadeDuration: value.fadeDuration
      });
    } catch {
      return null;
    }
    const scalars = [
      "id", "builderKey", "code", "title", "detail", "category", "source",
      "expertCode", "expertName", "itemCount", "previewTruncated", "pageSize",
      "pageCount", "fadeDuration", "graphicType", "subtype"
    ];
    if (!scalars.every(key => value[key] === recreated[key]) ||
        !sameStringArray(value.aliases, recreated.aliases) ||
        !sameSelection(value.selection, recreated.selection) ||
        !samePagePreview(operator.pagePreview, recreated.operator.pagePreview)) return null;
    return { ...recreated, enabled: value.enabled };
  }

  return {
    bridgeContract,
    sourceContract,
    actions,
    normalizeExpertIdentity,
    normalizeExpert,
    createExpertSearchRequest,
    normalizeExpertSearchResponse,
    normalizeExpertSearchError,
    createExpertPreviewRequest,
    normalizeRecommendation,
    normalizeExpertPreviewResponse,
    normalizeExpertPreviewError,
    createSelectableExpert,
    calculatePagePreview,
    buildExpertSelection,
    createExpertPlaylistEntry,
    restoreExpertPlaylistEntry,
    refreshExpertPlaylistEntry: restoreExpertPlaylistEntry
  };
});
