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

  let workflow = root && root.MbnOperatorCatalogWorkflow;
  if (!workflow && typeof module === "object" && module.exports) {
    workflow = require("./operator-catalog-workflow.js");
  }
  const api = Object.freeze(factory(workflow));
  if (typeof module === "object" && module.exports) module.exports = api;
  if (root) root.MbnOperatorCatalogUi = api;
})(typeof globalThis === "object" ? globalThis : this, function (workflow) {
  "use strict";

  if (!workflow) throw new Error("MbnOperatorCatalogWorkflow is required.");

  const globalScope = typeof globalThis === "object" ? globalThis : null;
  const fixedRequestIdPattern = /^[A-Za-z0-9_-]{1,128}$/;
  const unsafeTextPattern = /[\p{Cc}\p{Cf}\p{Cs}\p{Zl}\p{Zp}\uFFFD]/u;
  const themeStockMarkets = Object.freeze({
    krx: Object.freeze(["kospi", "kosdaq"]),
    nxt: Object.freeze(["nxt-kospi", "nxt-kosdaq"])
  });
  const themePrefixByStockMarket = Object.freeze({
    kospi: "P",
    kosdaq: "D",
    "nxt-kospi": "X",
    "nxt-kosdaq": "T"
  });

  function exactOptions(value) {
    if (!value || typeof value !== "object" || Array.isArray(value)) return false;
    const required = ["postNative", "isLocked", "showToast", "addLog", "createId"];
    return required.every(key => typeof value[key] === "function");
  }

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

  function themeStoredStockName(identity) {
    if (!identity || typeof identity.name !== "string") return null;
    if (identity.market !== "nxt-kospi" && identity.market !== "nxt-kosdaq") {
      return safeText(identity.name, 200) ? identity.name : null;
    }
    const suffix = "(NXT)";
    if (!identity.name.endsWith(suffix)) return null;
    const storedName = identity.name.slice(0, -suffix.length);
    return safeText(storedName, 200) ? storedName : null;
  }

  function clone(value) {
    if (value === null || value === undefined) return value;
    return JSON.parse(JSON.stringify(value));
  }

  function createController(options) {
    if (!exactOptions(options)) {
      throw new TypeError("Operator catalog UI requires closed native and operator callbacks.");
    }

    const contract = workflow.bridgeContract;
    const coordinator = workflow.createMutationCoordinator();
    const state = {
      mounted: false,
      open: false,
      entity: "theme",
      mode: "list",
      forcedLocked: false,
      processQuarantined: false,
      schema: null,
      canMutate: { theme: null, expert: null },
      activeRead: null,
      readQueue: [],
      themeQuery: "",
      expertQuery: "",
      nxtSession: "preMarket",
      themeMarketForNew: "krx",
      themeResults: [],
      expertResults: [],
      selectedCatalogIndex: -1,
      selectedDraftIndex: -1,
      edit: null,
      stockSearch: {
        query: "",
        status: "idle",
        results: [],
        selectedIndex: -1,
        error: ""
      },
      status: "대기",
      error: ""
    };
    let dom = null;

    function nextRequestId(kind) {
      const value = `catalog-${kind}-${String(options.createId())}`;
      if (!fixedRequestIdPattern.test(value)) {
        throw new TypeError("createId returned an unsafe operator-catalog identifier.");
      }
      return value;
    }

    function locked() {
      let external = true;
      try {
        external = options.isLocked() === true;
      } catch {
        external = true;
      }
      return state.forcedLocked || external;
    }

    function quarantined() {
      return state.processQuarantined || coordinator.getState().quarantined;
    }

    function notifyError(message) {
      state.error = message;
      state.status = "차단됨";
      options.showToast(message);
      render();
      return false;
    }

    function latchQuarantine(message) {
      state.processQuarantined = true;
      state.error = message || "DB 쓰기 결과를 확인할 수 없습니다. 앱 재시작 전까지 쓰기를 차단합니다.";
      state.status = "WRITE 잠금";
      options.addLog(`ThemeA/EList WRITE quarantine · ${state.error}`);
    }

    function expectedReadError(entity, operation, requestId) {
      return Object.freeze({ requestId, entity, operation, identityCode: "" });
    }

    function enqueueRead(key, type, request, expectedError) {
      state.readQueue = state.readQueue.filter(item => item.key !== key);
      if (state.activeRead?.key === key) state.activeRead.superseded = true;
      state.readQueue.push({ key, type, request, expectedError, superseded: false });
      pumpReadQueue();
      render();
    }

    function pumpReadQueue() {
      if (state.activeRead || state.readQueue.length === 0) return;
      const pending = state.readQueue.shift();
      state.activeRead = pending;
      state.status = "DB 조회 중";
      state.error = "";
      try {
        options.postNative(pending.type, pending.request);
      } catch {
        state.activeRead = null;
        state.error = "Native Bridge로 DB 조회 요청을 전달하지 못했습니다.";
        state.status = "조회 실패";
        pumpReadQueue();
      }
    }

    function completeRead(pending, apply) {
      if (state.activeRead !== pending) return false;
      if (!pending.superseded) apply();
      state.activeRead = null;
      if (!state.error) state.status = "준비됨";
      pumpReadQueue();
      render();
      return true;
    }

    function requestSchemaPreflight() {
      if (state.schema || state.activeRead?.key === "schema" ||
          state.readQueue.some(item => item.key === "schema")) return;
      const request = workflow.createSchemaPreflightRequest(nextRequestId("schema"));
      enqueueRead(
        "schema",
        contract.requests.schemaPreflight,
        request,
        expectedReadError("catalog", "schemaPreflight", request.requestId));
    }

    function requestCatalogList(query) {
      if (state.entity === "theme") {
        const request = workflow.createThemeCatalogListRequest(
          nextRequestId("theme-list"),
          query ?? state.themeQuery,
          state.nxtSession,
          500);
        state.themeQuery = request.query;
        enqueueRead(
          "catalog-list",
          contract.requests.themeList,
          request,
          expectedReadError("theme", "list", request.requestId));
        return request;
      }
      const request = workflow.createExpertCatalogListRequest(
        nextRequestId("expert-list"),
        query ?? state.expertQuery,
        500);
      state.expertQuery = request.query;
      enqueueRead(
        "catalog-list",
        contract.requests.expertList,
        request,
        expectedReadError("expert", "list", request.requestId));
      return request;
    }

    function normalizeEditContext(context, entity) {
      if (!context || typeof context !== "object" || Array.isArray(context) ||
          Object.keys(context).sort().join("|") !== "preview|selection") {
        throw new TypeError("Editing requires exactly one read-only selection and complete preview.");
      }
      return entity === "theme"
        ? workflow.createThemeEditDraft(context.selection, context.preview)
        : workflow.createExpertEditDraft(context.selection, context.preview);
    }

    function resetStockSearch() {
      state.stockSearch = {
        query: "",
        status: "idle",
        results: [],
        selectedIndex: -1,
        error: ""
      };
    }

    function openEntity(entity, context) {
      state.open = true;
      state.entity = entity;
      state.selectedCatalogIndex = -1;
      state.selectedDraftIndex = -1;
      state.error = "";
      resetStockSearch();
      if (context !== undefined && context !== null) {
        state.edit = normalizeEditContext(context, entity);
        state.mode = "edit";
        state.status = "편집 준비";
      } else {
        state.edit = null;
        state.mode = "list";
        state.status = "목록 준비";
      }
      requestSchemaPreflight();
      if (state.mode === "list") requestCatalogList();
      render();
      return snapshot();
    }

    function openTheme(context) {
      return openEntity("theme", context);
    }

    function openExpert(context) {
      return openEntity("expert", context);
    }

    function beginNewTheme(market) {
      if (market !== "krx" && market !== "nxt") return notifyError("KRX 또는 NXT 시장을 선택하세요.");
      if (locked() || quarantined()) return notifyError("현재 ThemeA 쓰기를 시작할 수 없습니다.");
      state.themeMarketForNew = market;
      const request = workflow.createThemeNextCodeRequest(nextRequestId("theme-next"), market);
      enqueueRead(
        "next-code",
        contract.requests.themeNextCode,
        request,
        expectedReadError("theme", "nextCode", request.requestId));
      return true;
    }

    function beginNewExpert() {
      if (locked() || quarantined()) return notifyError("현재 EList 쓰기를 시작할 수 없습니다.");
      const request = workflow.createExpertNextCodeRequest(nextRequestId("expert-next"));
      enqueueRead(
        "next-code",
        contract.requests.expertNextCode,
        request,
        expectedReadError("expert", "nextCode", request.requestId));
      return true;
    }

    function currentDraft() {
      return state.edit?.draft || null;
    }

    function selectedStockIdentity() {
      return state.stockSearch.results[state.stockSearch.selectedIndex] || null;
    }

    function stockIsCompatibleWithDraft(identity, draft = currentDraft()) {
      if (!identity || !draft) return false;
      if (state.entity === "expert") return true;
      return themeStockMarkets[draft.market]?.includes(identity.market) === true;
    }

    function updateStockQuery(value) {
      if (typeof value !== "string") return false;
      if (value !== state.stockSearch.query) {
        state.stockSearch.query = value;
        state.stockSearch.status = "idle";
        state.stockSearch.results = [];
        state.stockSearch.selectedIndex = -1;
        state.stockSearch.error = "";
        render();
      }
      return true;
    }

    function searchStocks(query) {
      if (!currentDraft() || (state.mode !== "new" && state.mode !== "edit")) {
        return notifyError("먼저 ThemeA/EList 새 정의 또는 편집을 여세요.");
      }
      try {
        const request = workflow.createStockMasterSearchRequest(
          nextRequestId("stock-master"),
          query,
          workflow.limits.maximumResults);
        state.stockSearch.query = request.query;
        state.stockSearch.status = "loading";
        state.stockSearch.results = [];
        state.stockSearch.selectedIndex = -1;
        state.stockSearch.error = "";
        state.error = "";
        enqueueRead(
          "stock-master-search",
          workflow.stockMasterContract.request,
          request,
          null);
        return true;
      } catch (error) {
        return notifyError(error?.message || "종목 master 검색어가 올바르지 않습니다.");
      }
    }

    function selectStockResult(index) {
      const identity = state.stockSearch.results[index];
      if (!Number.isInteger(index) || !identity) return false;
      if (!stockIsCompatibleWithDraft(identity)) {
        return notifyError("현재 ThemeA 시장과 일치하는 종목을 선택하세요.");
      }
      state.stockSearch.selectedIndex = index;
      state.stockSearch.error = "";
      state.stockSearch.status = "selected";
      render();
      return true;
    }

    function updateDraft(values) {
      const draft = currentDraft();
      if (!draft || !values || typeof values !== "object" || Array.isArray(values)) return false;
      if (state.entity === "theme" && Object.keys(values).every(key => key === "themeTitle") &&
          typeof values.themeTitle === "string") {
        draft.themeTitle = values.themeTitle;
      } else if (state.entity === "expert" &&
          Object.keys(values).every(key => key === "expertName") &&
          typeof values.expertName === "string") {
        draft.expertName = values.expertName;
      } else {
        return false;
      }
      state.error = "";
      render();
      return true;
    }

    function addThemeItem() {
      const draft = currentDraft();
      const identity = selectedStockIdentity();
      if (!draft || state.entity !== "theme" || !stockIsCompatibleWithDraft(identity, draft) ||
          draft.items.length >= workflow.limits.maximumThemeItems) {
        return notifyError("검색 결과에서 현재 ThemeA 시장의 종목 하나를 선택하세요.");
      }
      const storedName = themeStoredStockName(identity);
      if (!storedName) {
        return notifyError("NXT 표시명의 끝 (NXT) suffix를 안전하게 변환할 수 없습니다.");
      }
      const itemCode = `${themePrefixByStockMarket[identity.market]}${identity.code}`;
      const candidate = {
        market: draft.market,
        themeCode: draft.themeCode,
        themeTitle: safeText(draft.themeTitle, 128) ? draft.themeTitle : "검증",
        items: [...draft.items, {
          inputIndex: draft.items.length,
          itemCode,
          itemName: storedName
        }]
      };
      if (!workflow.normalizeThemeDraft(candidate)) {
        return notifyError("중복되거나 허용되지 않은 ThemeA 종목입니다.");
      }
      draft.items.push({ inputIndex: draft.items.length, itemCode, itemName: storedName });
      resetStockSearch();
      state.error = "";
      render();
      return true;
    }

    function addExpertRecommendation(buyAmount) {
      const draft = currentDraft();
      const identity = selectedStockIdentity();
      const amount = typeof buyAmount === "number" ? buyAmount : Number(buyAmount);
      if (!draft || state.entity !== "expert" || !identity ||
          !Number.isSafeInteger(amount) || amount <= 0 ||
          draft.recommendations.length >= workflow.limits.maximumRecommendations) {
        return notifyError("검색 결과에서 종목을 선택하고 1 이상의 정수 매수가를 입력하세요.");
      }
      const candidate = {
        expertCode: draft.expertCode,
        expertName: safeText(draft.expertName, 128) ? draft.expertName : "검증",
        recommendations: [
          ...draft.recommendations,
          {
            playIndex: draft.recommendations.length,
            stockCode: identity.code,
            stockName: identity.name,
            buyAmount: amount
          }
        ]
      };
      if (!workflow.normalizeExpertDraft(candidate)) {
        return notifyError("중복되거나 허용되지 않은 EList 추천 종목입니다.");
      }
      draft.recommendations.push({
        playIndex: draft.recommendations.length,
        stockCode: identity.code,
        stockName: identity.name,
        buyAmount: amount
      });
      resetStockSearch();
      state.error = "";
      render();
      return true;
    }

    function reindexDraftRows(rows, key) {
      rows.forEach((item, index) => { item[key] = index; });
    }

    function moveDraftItem(index, direction) {
      const draft = currentDraft();
      const rows = state.entity === "theme" ? draft?.items : draft?.recommendations;
      const target = index + direction;
      if (!rows || !Number.isInteger(index) || ![-1, 1].includes(direction) ||
          index < 0 || target < 0 || index >= rows.length || target >= rows.length) return false;
      [rows[index], rows[target]] = [rows[target], rows[index]];
      if (state.selectedDraftIndex === index) state.selectedDraftIndex = target;
      else if (state.selectedDraftIndex === target) state.selectedDraftIndex = index;
      reindexDraftRows(rows, state.entity === "theme" ? "inputIndex" : "playIndex");
      render();
      return true;
    }

    function removeDraftItem(index) {
      const draft = currentDraft();
      const rows = state.entity === "theme" ? draft?.items : draft?.recommendations;
      if (locked() || coordinator.getState().active || !rows || !Number.isInteger(index) ||
          index < 0 || index >= rows.length) return false;
      rows.splice(index, 1);
      reindexDraftRows(rows, state.entity === "theme" ? "inputIndex" : "playIndex");
      if (rows.length === 0) state.selectedDraftIndex = -1;
      else if (state.selectedDraftIndex === index) {
        state.selectedDraftIndex = Math.min(index, rows.length - 1);
      } else if (state.selectedDraftIndex > index) {
        state.selectedDraftIndex -= 1;
      }
      render();
      return true;
    }

    function selectDraftItem(index) {
      const draft = currentDraft();
      const rows = state.entity === "theme" ? draft?.items : draft?.recommendations;
      if (!rows || !Number.isInteger(index) || index < 0 || index >= rows.length) return false;
      state.selectedDraftIndex = index;
      for (const [rowIndex, row] of [...(dom?.draftList?.children || [])].entries()) {
        row.className = rowIndex === index
          ? "mbn-operator-catalog-draft-row selected"
          : "mbn-operator-catalog-draft-row";
        row.setAttribute("aria-selected", String(rowIndex === index));
      }
      return true;
    }

    function editableDeleteTarget(target) {
      let current = target;
      while (current && current !== dom?.dialog) {
        const tagName = String(current.tagName || "").toUpperCase();
        const contentEditable = current.getAttribute?.("contenteditable");
        if (tagName === "INPUT" || tagName === "TEXTAREA" || current.isContentEditable === true ||
            (contentEditable !== null && contentEditable !== undefined &&
              String(contentEditable).toLowerCase() !== "false")) return true;
        current = current.parentElement;
      }
      return false;
    }

    function draftIndexFromTarget(target) {
      let current = target;
      while (current && current !== dom?.dialog) {
        const value = current.getAttribute?.("data-draft-index");
        if (value !== null && value !== undefined && /^\d+$/u.test(String(value))) {
          return Number(value);
        }
        current = current.parentElement;
      }
      return -1;
    }

    function handleDraftDeleteKey(event) {
      if (!dom?.dialog || !state.open || event?.key !== "Delete" ||
          editableDeleteTarget(event.target) ||
          (typeof dom.dialog.contains === "function" && !dom.dialog.contains(event.target))) return false;
      const focusedIndex = draftIndexFromTarget(event.target);
      const index = focusedIndex >= 0 ? focusedIndex : state.selectedDraftIndex;
      const draft = currentDraft();
      const rows = state.entity === "theme" ? draft?.items : draft?.recommendations;
      if (!rows || index < 0 || index >= rows.length) return false;
      event.preventDefault();
      event.stopPropagation();
      if (locked() || coordinator.getState().active) return false;
      return removeDraftItem(index);
    }

    function canWrite() {
      if (locked() || quarantined() || coordinator.getState().active || state.activeRead ||
          state.readQueue.length > 0 || !state.schema) return false;
      return state.entity === "theme"
        ? state.schema.themeCanMutate === true && state.canMutate.theme !== false
        : state.schema.expertCanMutate === true && state.canMutate.expert !== false;
    }

    function sendMutation(type, payload) {
      if (!canWrite()) return notifyError("송출 IDLE 및 schema preflight 완료 후 DB 쓰기를 실행하세요.");
      try {
        coordinator.begin(type, payload);
        options.postNative(type, payload);
        state.status = "DB 반영 확인 중";
        state.error = "";
        options.addLog(`${type} 요청 · 자동 재시도 없음`);
        render();
        return true;
      } catch (error) {
        if (coordinator.getState().active) {
          coordinator.loseCorrelation();
          latchQuarantine("DB 쓰기 요청 전달 여부를 확인할 수 없습니다. 앱을 재시작하세요.");
        }
        return notifyError(error?.message || "DB 쓰기 요청을 만들 수 없습니다.");
      }
    }

    function submit() {
      if (!state.edit || (state.mode !== "new" && state.mode !== "edit")) return false;
      try {
        const id = nextRequestId(`${state.entity}-${state.mode}`);
        if (state.entity === "theme") {
          const request = state.mode === "new"
            ? workflow.createThemeCreateRequest(id, state.edit.draft)
            : workflow.createThemeReplaceRequest(id, state.edit);
          const type = state.mode === "new"
            ? contract.requests.themeCreate
            : contract.requests.themeReplace;
          return sendMutation(type, request);
        }
        const request = state.mode === "new"
          ? workflow.createExpertCreateRequest(id, state.edit.draft)
          : workflow.createExpertReplaceRequest(id, state.edit);
        const type = state.mode === "new"
          ? contract.requests.expertCreate
          : contract.requests.expertReplace;
        return sendMutation(type, request);
      } catch (error) {
        return notifyError(error?.message || "편집 내용을 검증할 수 없습니다.");
      }
    }

    function selectedCatalog() {
      const rows = state.entity === "theme" ? state.themeResults : state.expertResults;
      return rows[state.selectedCatalogIndex] || null;
    }

    function selectCatalog(index) {
      const rows = state.entity === "theme" ? state.themeResults : state.expertResults;
      if (!Number.isInteger(index) || index < 0 || index >= rows.length) return false;
      state.selectedCatalogIndex = index;
      render();
      return true;
    }

    function deleteCurrent() {
      try {
        const id = nextRequestId(`${state.entity}-delete`);
        if (state.entity === "theme") {
          const source = state.mode === "edit"
            ? state.edit?.expectedIdentity
            : selectedCatalog();
          if (!source) return notifyError("삭제할 ThemeA 정의를 선택하세요.");
          const identity = {
            market: source.market,
            session: source.session,
            themeCode: source.themeCode,
            themeTitle: source.themeTitle
          };
          return sendMutation(
            contract.requests.themeDelete,
            workflow.createThemeDeleteRequest(id, identity));
        }
        const source = state.mode === "edit"
          ? state.edit?.expectedIdentity
          : selectedCatalog();
        if (!source) return notifyError("삭제할 EList 정의를 선택하세요.");
        return sendMutation(
          contract.requests.expertDelete,
          workflow.createExpertDeleteRequest(id, {
            expertCode: source.expertCode,
            expertName: source.expertName
          }));
      } catch (error) {
        return notifyError(error?.message || "삭제 요청을 검증할 수 없습니다.");
      }
    }

    function handleSchemaResult(payload) {
      const pending = state.activeRead;
      if (!pending || pending.type !== contract.requests.schemaPreflight ||
          payload?.requestId !== pending.request.requestId) return;
      const result = workflow.normalizeSchemaResult(payload, pending.request);
      if (!result) return failCurrentRead(pending, "schema preflight 응답이 현재 요청과 일치하지 않습니다.");
      completeRead(pending, () => {
        state.schema = result;
        state.canMutate.theme = result.themeCanMutate;
        state.canMutate.expert = result.expertCanMutate;
        if (result.writeQuarantined) latchQuarantine("Native catalog 쓰기가 이미 격리되었습니다.");
      });
    }

    function handleThemeListResult(payload) {
      const pending = state.activeRead;
      if (!pending || pending.type !== contract.requests.themeList ||
          payload?.requestId !== pending.request.requestId) return;
      const result = workflow.normalizeThemeListResult(payload, pending.request);
      if (!result) return failCurrentRead(pending, "ThemeA 목록 응답이 현재 검색과 일치하지 않습니다.");
      completeRead(pending, () => {
        state.themeResults = [...result.results];
        state.canMutate.theme = state.canMutate.theme !== false && result.canMutate;
        state.selectedCatalogIndex = -1;
        if (result.writeQuarantined) latchQuarantine("ThemeA 쓰기가 Native에서 격리되었습니다.");
      });
    }

    function handleExpertListResult(payload) {
      const pending = state.activeRead;
      if (!pending || pending.type !== contract.requests.expertList ||
          payload?.requestId !== pending.request.requestId) return;
      const result = workflow.normalizeExpertListResult(payload, pending.request);
      if (!result) return failCurrentRead(pending, "EList 목록 응답이 현재 검색과 일치하지 않습니다.");
      completeRead(pending, () => {
        state.expertResults = [...result.results];
        state.canMutate.expert = state.canMutate.expert !== false && result.canMutate;
        state.selectedCatalogIndex = -1;
        if (result.writeQuarantined) latchQuarantine("EList 쓰기가 Native에서 격리되었습니다.");
      });
    }

    function handleThemeNextCodeResult(payload) {
      const pending = state.activeRead;
      if (!pending || pending.type !== contract.requests.themeNextCode ||
          payload?.requestId !== pending.request.requestId) return;
      const result = workflow.normalizeThemeNextCodeResult(payload, pending.request);
      if (!result) return failCurrentRead(pending, "ThemeA 다음 코드 응답이 현재 시장과 일치하지 않습니다.");
      completeRead(pending, () => {
        if (result.writeQuarantined) latchQuarantine("ThemeA 쓰기가 Native에서 격리되었습니다.");
        state.canMutate.theme = state.canMutate.theme !== false && result.canMutate;
        state.entity = "theme";
        state.mode = "new";
        state.selectedDraftIndex = -1;
        state.edit = {
          expectedIdentity: null,
          draft: workflow.createBlankThemeDraft(result.market, result.themeCode)
        };
      });
    }

    function handleExpertNextCodeResult(payload) {
      const pending = state.activeRead;
      if (!pending || pending.type !== contract.requests.expertNextCode ||
          payload?.requestId !== pending.request.requestId) return;
      const result = workflow.normalizeExpertNextCodeResult(payload, pending.request);
      if (!result) return failCurrentRead(pending, "EList 다음 코드 응답이 현재 요청과 일치하지 않습니다.");
      completeRead(pending, () => {
        if (result.writeQuarantined) latchQuarantine("EList 쓰기가 Native에서 격리되었습니다.");
        state.canMutate.expert = state.canMutate.expert !== false && result.canMutate;
        state.entity = "expert";
        state.mode = "new";
        state.selectedDraftIndex = -1;
        state.edit = {
          expectedIdentity: null,
          draft: workflow.createBlankExpertDraft(result.expertCode)
        };
      });
    }

    function failCurrentRead(pending, message) {
      return completeRead(pending, () => {
        state.error = message;
        state.status = "응답 차단";
      });
    }

    function handleStockMasterSearchResult(payload) {
      const pending = state.activeRead;
      if (!pending || pending.type !== workflow.stockMasterContract.request ||
          payload?.requestId !== pending.request.requestId) return false;
      const result = workflow.normalizeStockMasterSearchResult(payload, pending.request);
      if (!result) {
        completeRead(pending, () => {
          state.stockSearch.status = "error";
          state.stockSearch.results = [];
          state.stockSearch.selectedIndex = -1;
          state.stockSearch.error =
            "잘렸거나 중복·동명이인·형식 오류가 있는 종목 응답을 차단했습니다. 검색어를 좁히세요.";
          state.error = state.stockSearch.error;
          state.status = "응답 차단";
        });
        return true;
      }
      completeRead(pending, () => {
        state.stockSearch.query = result.query;
        state.stockSearch.status = "ready";
        state.stockSearch.results = [...result.results];
        state.stockSearch.selectedIndex = -1;
        state.stockSearch.error = "";
        state.error = "";
      });
      return true;
    }

    function handleStockMasterSearchError(payload) {
      const pending = state.activeRead;
      if (!pending || pending.type !== workflow.stockMasterContract.request ||
          payload?.requestId !== pending.request.requestId) return false;
      const error = workflow.normalizeStockMasterSearchError(payload, pending.request);
      completeRead(pending, () => {
        state.stockSearch.status = "error";
        state.stockSearch.results = [];
        state.stockSearch.selectedIndex = -1;
        state.stockSearch.error = error?.message ||
          "현재 종목 검색 요청과 연결할 수 없는 오류 응답을 차단했습니다.";
        state.error = state.stockSearch.error;
        state.status = "조회 실패";
      });
      return true;
    }

    function handleMutationResult(payload) {
      const active = coordinator.getState().active;
      if (!active || payload?.requestId !== active.requestId) return;
      const result = coordinator.acceptResult(payload);
      if (!result) {
        coordinator.loseCorrelation();
        latchQuarantine("DB 쓰기 결과 형식이 현재 요청과 일치하지 않습니다. 앱을 재시작하세요.");
        render();
        return;
      }
      state.status = "DB 반영 완료";
      state.error = "";
      state.mode = "list";
      state.edit = null;
      state.selectedDraftIndex = -1;
      options.addLog(`${result.entity} ${result.operation} 완료 · ${result.identityCode} · ${result.operationId}`);
      options.showToast("ThemeA/EList DB 작업이 완료되었습니다.");
      render();
    }

    function handleCatalogError(payload) {
      const activeMutation = coordinator.getState().active;
      if (activeMutation) {
        if (payload?.requestId !== activeMutation.requestId) return;
        const error = coordinator.acceptError(payload);
        if (!error) {
          coordinator.loseCorrelation();
          latchQuarantine("DB 쓰기 오류 응답을 현재 요청과 연결할 수 없습니다. 앱을 재시작하세요.");
        } else {
          if (error.outcomeUnknown || error.writeQuarantined) latchQuarantine(error.message);
          else {
            state.error = `${error.message} [${error.code}]`;
            state.status = "DB 반영 실패";
          }
        }
        render();
        return;
      }

      const pending = state.activeRead;
      if (!pending || payload?.requestId !== pending.request.requestId) return;
      const error = workflow.normalizeCatalogError(payload, pending.expectedError);
      if (!error) return failCurrentRead(pending, "DB 조회 오류 응답을 현재 요청과 연결할 수 없습니다.");
      completeRead(pending, () => {
        if (error.writeQuarantined || error.outcomeUnknown) latchQuarantine(error.message);
        state.error = `${error.message} [${error.code}]`;
        state.status = "조회 실패";
      });
    }

    function handleMessage(type, payload) {
      if (type === workflow.stockMasterContract.result) {
        return handleStockMasterSearchResult(payload);
      }
      if (type === workflow.stockMasterContract.error) {
        return handleStockMasterSearchError(payload);
      }
      switch (type) {
        case contract.events.schemaPreflight:
          handleSchemaResult(payload);
          return true;
        case contract.events.themeList:
          handleThemeListResult(payload);
          return true;
        case contract.events.expertList:
          handleExpertListResult(payload);
          return true;
        case contract.events.themeNextCode:
          handleThemeNextCodeResult(payload);
          return true;
        case contract.events.expertNextCode:
          handleExpertNextCodeResult(payload);
          return true;
        case contract.events.mutation:
          handleMutationResult(payload);
          return true;
        case contract.events.error:
          handleCatalogError(payload);
          return true;
        default:
          return false;
      }
    }

    function search(query, nxtSession) {
      if (state.entity === "theme" && nxtSession !== undefined) {
        if (nxtSession !== "preMarket" && nxtSession !== "afterMarket") {
          return notifyError("NXT 세션을 명시적으로 선택하세요.");
        }
        state.nxtSession = nxtSession;
      }
      try {
        requestCatalogList(query);
        return true;
      } catch (error) {
        return notifyError(error?.message || "카탈로그 검색 요청이 올바르지 않습니다.");
      }
    }

    function loseCorrelation() {
      if (coordinator.getState().active) {
        coordinator.loseCorrelation();
        latchQuarantine("Browser 상관관계를 잃어 DB 쓰기 결과를 확인할 수 없습니다.");
      }
      state.activeRead = null;
      state.readQueue = [];
      render();
    }

    function setLocked(value = true) {
      state.forcedLocked = value === true;
      render();
      return locked();
    }

    function close() {
      state.open = false;
      render();
    }

    function snapshot() {
      return Object.freeze({
        mounted: state.mounted,
        open: state.open,
        entity: state.entity,
        mode: state.mode,
        locked: locked(),
        quarantined: quarantined(),
        status: state.status,
        error: state.error,
        schema: clone(state.schema),
        canMutate: clone(state.canMutate),
        activeRead: clone(state.activeRead && {
          key: state.activeRead.key,
          type: state.activeRead.type,
          request: state.activeRead.request,
          superseded: state.activeRead.superseded
        }),
        queuedReadCount: state.readQueue.length,
        mutation: clone(coordinator.getState().active),
        themeResults: clone(state.themeResults),
        expertResults: clone(state.expertResults),
        selectedCatalogIndex: state.selectedCatalogIndex,
        selectedDraftIndex: state.selectedDraftIndex,
        stockSearch: clone({
          ...state.stockSearch,
          selected: selectedStockIdentity()
        }),
        edit: clone(state.edit),
        draft: clone(currentDraft())
      });
    }

    function makeElement(documentValue, tagName, className, text) {
      const element = documentValue.createElement(tagName);
      if (className) element.className = className;
      if (text !== undefined) element.textContent = text;
      return element;
    }

    function assignRole(element, role) {
      element.setAttribute("data-role", role);
      return element;
    }

    function addStylesheet(documentValue) {
      if (!documentValue.head || documentValue.getElementById?.("mbn-operator-catalog-ui-css")) return;
      const link = documentValue.createElement("link");
      link.id = "mbn-operator-catalog-ui-css";
      link.rel = "stylesheet";
      link.href = "operator-catalog-ui.css";
      documentValue.head.appendChild(link);
    }

    function mount(host) {
      if (state.mounted) return snapshot();
      const target = host ?? (typeof document === "object" ? document.body : null);
      const documentValue = target?.ownerDocument ?? (typeof document === "object" ? document : null);
      if (!target || !documentValue || typeof documentValue.createElement !== "function" ||
          typeof target.appendChild !== "function") {
        throw new TypeError("mount requires a DOM host.");
      }
      addStylesheet(documentValue);

      const dialog = makeElement(documentValue, "dialog", "mbn-operator-catalog-dialog");
      dialog.setAttribute("aria-labelledby", "mbnOperatorCatalogTitle");
      const shell = makeElement(documentValue, "section", "mbn-operator-catalog-shell");
      const header = makeElement(documentValue, "header", "mbn-operator-catalog-header");
      const title = makeElement(documentValue, "h2", "", "ThemeA / EList 운영 관리");
      title.id = "mbnOperatorCatalogTitle";
      const status = assignRole(makeElement(documentValue, "span", "mbn-operator-catalog-status"), "status");
      status.setAttribute("aria-live", "polite");
      const closeButton = assignRole(makeElement(documentValue, "button", "", "닫기"), "close");
      closeButton.type = "button";
      header.append(title, status, closeButton);

      const tabs = makeElement(documentValue, "nav", "mbn-operator-catalog-tabs");
      const themeTab = assignRole(makeElement(documentValue, "button", "", "ThemeA"), "theme-tab");
      const expertTab = assignRole(makeElement(documentValue, "button", "", "EList"), "expert-tab");
      themeTab.type = expertTab.type = "button";
      tabs.append(themeTab, expertTab);

      const toolbar = makeElement(documentValue, "form", "mbn-operator-catalog-toolbar");
      const query = assignRole(makeElement(documentValue, "input"), "query");
      query.type = "search";
      query.maxLength = workflow.limits.maximumQueryLength;
      query.autocomplete = "off";
      const session = assignRole(makeElement(documentValue, "select"), "nxt-session");
      for (const [value, label] of [["preMarket", "NXT 프리마켓"], ["afterMarket", "NXT 애프터마켓"]]) {
        const option = makeElement(documentValue, "option", "", label);
        option.value = value;
        session.appendChild(option);
      }
      const searchButton = assignRole(makeElement(documentValue, "button", "", "검색"), "search");
      searchButton.type = "submit";
      const market = assignRole(makeElement(documentValue, "select"), "new-market");
      for (const [value, label] of [["krx", "KRX"], ["nxt", "NXT"]]) {
        const option = makeElement(documentValue, "option", "", label);
        option.value = value;
        market.appendChild(option);
      }
      const newButton = assignRole(makeElement(documentValue, "button", "", "새 정의"), "new");
      newButton.type = "button";
      const listDeleteButton = assignRole(
        makeElement(documentValue, "button", "mbn-danger", "선택 정의 삭제"),
        "delete-selected");
      listDeleteButton.type = "button";
      toolbar.append(query, session, searchButton, market, newButton, listDeleteButton);

      const body = makeElement(documentValue, "div", "mbn-operator-catalog-body");
      const catalog = assignRole(makeElement(documentValue, "ol", "mbn-operator-catalog-list"), "catalog-list");
      const editor = assignRole(makeElement(documentValue, "section", "mbn-operator-catalog-editor"), "editor");
      const identity = makeElement(documentValue, "div", "mbn-operator-catalog-identity");
      const code = assignRole(makeElement(documentValue, "output"), "identity-code");
      const name = assignRole(makeElement(documentValue, "input"), "identity-name");
      name.type = "text";
      name.maxLength = 128;
      name.autocomplete = "off";
      identity.append(code, name);

      const stockSearchForm = makeElement(
        documentValue,
        "form",
        "mbn-operator-catalog-stock-search");
      const stockQuery = assignRole(makeElement(documentValue, "input"), "stock-query");
      stockQuery.type = "search";
      stockQuery.maxLength = workflow.limits.maximumQueryLength;
      stockQuery.autocomplete = "off";
      stockQuery.placeholder = "종목명 검색 (직접 입력값은 저장되지 않음)";
      const stockSearchButton = assignRole(
        makeElement(documentValue, "button", "", "종목 검색"),
        "stock-search");
      stockSearchButton.type = "submit";
      stockSearchForm.append(stockQuery, stockSearchButton);
      const stockStatus = assignRole(
        makeElement(documentValue, "output", "mbn-operator-catalog-stock-status"),
        "stock-status");
      const stockResults = assignRole(
        makeElement(documentValue, "ol", "mbn-operator-catalog-stock-results"),
        "stock-results");

      const itemForm = makeElement(documentValue, "form", "mbn-operator-catalog-item-form");
      const selectedStock = assignRole(
        makeElement(documentValue, "output", "mbn-operator-catalog-selected-stock"),
        "selected-stock");
      const buyAmount = assignRole(makeElement(documentValue, "input"), "buy-amount");
      buyAmount.type = "number";
      buyAmount.min = "1";
      buyAmount.step = "1";
      const addButton = assignRole(makeElement(documentValue, "button", "", "추가"), "add-item");
      addButton.type = "submit";
      itemForm.append(selectedStock, buyAmount, addButton);
      const draftList = assignRole(makeElement(documentValue, "ol", "mbn-operator-catalog-draft"), "draft-list");
      const actions = makeElement(documentValue, "footer", "mbn-operator-catalog-actions");
      const saveButton = assignRole(makeElement(documentValue, "button", "", "DB 반영"), "save");
      const deleteButton = assignRole(makeElement(documentValue, "button", "mbn-danger", "정의 삭제"), "delete");
      const cancelButton = assignRole(makeElement(documentValue, "button", "", "목록으로"), "cancel-edit");
      saveButton.type = deleteButton.type = cancelButton.type = "button";
      actions.append(saveButton, deleteButton, cancelButton);
      editor.append(
        identity,
        stockSearchForm,
        stockStatus,
        stockResults,
        itemForm,
        draftList,
        actions);
      body.append(catalog, editor);

      const error = assignRole(makeElement(documentValue, "p", "mbn-operator-catalog-error"), "error");
      shell.append(header, tabs, toolbar, body, error);
      dialog.appendChild(shell);
      target.appendChild(dialog);

      dom = {
        document: documentValue,
        dialog,
        status,
        closeButton,
        themeTab,
        expertTab,
        query,
        session,
        market,
        newButton,
        listDeleteButton,
        toolbar,
        catalog,
        editor,
        code,
        name,
        stockSearchForm,
        stockQuery,
        stockSearchButton,
        stockStatus,
        stockResults,
        selectedStock,
        itemForm,
        buyAmount,
        addButton,
        draftList,
        saveButton,
        deleteButton,
        cancelButton,
        error
      };

      closeButton.addEventListener("click", close);
      themeTab.addEventListener("click", () => openTheme());
      expertTab.addEventListener("click", () => openExpert());
      toolbar.addEventListener("submit", event => {
        event.preventDefault();
        search(query.value, session.value);
      });
      newButton.addEventListener("click", () => state.entity === "theme"
        ? beginNewTheme(market.value)
        : beginNewExpert());
      listDeleteButton.addEventListener("click", () => {
        const confirmDelete = typeof globalScope?.confirm !== "function" ||
          globalScope.confirm("선택한 ThemeA/EList 정의를 DB에서 삭제할까요?");
        if (confirmDelete) deleteCurrent();
      });
      name.addEventListener("input", () => updateDraft(state.entity === "theme"
        ? { themeTitle: name.value }
        : { expertName: name.value }));
      stockQuery.addEventListener("input", () => updateStockQuery(stockQuery.value));
      stockSearchForm.addEventListener("submit", event => {
        event.preventDefault();
        searchStocks(stockQuery.value);
      });
      itemForm.addEventListener("submit", event => {
        event.preventDefault();
        const added = state.entity === "theme"
          ? addThemeItem()
          : addExpertRecommendation(buyAmount.value);
        if (added) {
          buyAmount.value = "";
        }
      });
      saveButton.addEventListener("click", submit);
      deleteButton.addEventListener("click", () => {
        const confirmDelete = typeof globalScope?.confirm !== "function" ||
          globalScope.confirm("선택한 ThemeA/EList 정의를 DB에서 삭제할까요?");
        if (confirmDelete) deleteCurrent();
      });
      cancelButton.addEventListener("click", () => {
        state.mode = "list";
        state.edit = null;
        state.selectedDraftIndex = -1;
        resetStockSearch();
        render();
      });
      dialog.addEventListener("keydown", handleDraftDeleteKey);

      state.mounted = true;
      render();
      return snapshot();
    }

    function renderCatalogRows() {
      if (!dom) return;
      dom.catalog.replaceChildren();
      const rows = state.entity === "theme" ? state.themeResults : state.expertResults;
      rows.forEach((item, index) => {
        const row = makeElement(dom.document, "li", "mbn-operator-catalog-row");
        const button = makeElement(
          dom.document,
          "button",
          index === state.selectedCatalogIndex ? "selected" : "",
          state.entity === "theme"
            ? `${item.themeCode} · ${item.themeTitle} · ${item.market.toUpperCase()}`
            : `${item.expertCode} · ${item.expertName}`);
        button.type = "button";
        button.addEventListener("click", () => selectCatalog(index));
        row.appendChild(button);
        dom.catalog.appendChild(row);
      });
    }

    function renderDraftRows() {
      if (!dom) return;
      dom.draftList.replaceChildren();
      const draft = currentDraft();
      const rows = state.entity === "theme" ? draft?.items : draft?.recommendations;
      (rows || []).forEach((item, index) => {
        const selected = index === state.selectedDraftIndex;
        const row = makeElement(
          dom.document,
          "li",
          selected ? "mbn-operator-catalog-draft-row selected" : "mbn-operator-catalog-draft-row");
        row.tabIndex = 0;
        row.setAttribute("data-draft-index", String(index));
        row.setAttribute("aria-selected", String(selected));
        const label = makeElement(
          dom.document,
          "span",
          "",
          state.entity === "theme"
            ? `${item.itemCode} · ${item.itemName}`
            : `${item.stockCode} · ${item.stockName} · ${item.buyAmount}`);
        const up = makeElement(dom.document, "button", "", "↑");
        const down = makeElement(dom.document, "button", "", "↓");
        const remove = makeElement(dom.document, "button", "mbn-danger", "삭제");
        up.type = down.type = remove.type = "button";
        up.disabled = index === 0 || locked();
        down.disabled = index + 1 === rows.length || locked();
        remove.disabled = locked() || Boolean(coordinator.getState().active);
        row.addEventListener("focus", () => selectDraftItem(index));
        row.addEventListener("click", event => {
          if (event.target === row || event.target === label) selectDraftItem(index);
        });
        up.addEventListener("click", () => moveDraftItem(index, -1));
        down.addEventListener("click", () => moveDraftItem(index, 1));
        remove.addEventListener("click", event => {
          event.stopPropagation();
          removeDraftItem(index);
        });
        row.append(label, up, down, remove);
        dom.draftList.appendChild(row);
      });
    }

    function renderStockRows() {
      if (!dom) return;
      dom.stockResults.replaceChildren();
      state.stockSearch.results.forEach((identity, index) => {
        const compatible = stockIsCompatibleWithDraft(identity);
        const row = makeElement(dom.document, "li", "mbn-operator-catalog-stock-row");
        const button = makeElement(
          dom.document,
          "button",
          index === state.stockSearch.selectedIndex ? "selected" : "",
          `${identity.name} · ${identity.code} · ${identity.market} · ${identity.source}`);
        button.type = "button";
        button.disabled = !compatible || locked();
        button.title = compatible
          ? "이 DB 종목 identity를 선택"
          : "현재 ThemeA 시장과 다른 종목";
        button.addEventListener("click", () => selectStockResult(index));
        row.appendChild(button);
        dom.stockResults.appendChild(row);
      });
    }

    function render() {
      if (!dom) return snapshot();
      const isLocked = locked();
      const isQuarantined = quarantined();
      const mutationActive = Boolean(coordinator.getState().active);
      dom.status.textContent = isQuarantined ? "WRITE 잠금" : state.status;
      dom.error.textContent = state.error;
      dom.error.hidden = !state.error;
      dom.themeTab.setAttribute("aria-pressed", String(state.entity === "theme"));
      dom.expertTab.setAttribute("aria-pressed", String(state.entity === "expert"));
      dom.query.value = state.entity === "theme" ? state.themeQuery : state.expertQuery;
      dom.session.value = state.nxtSession;
      dom.session.hidden = state.entity !== "theme";
      dom.market.hidden = state.entity !== "theme";
      dom.market.value = state.themeMarketForNew;
      dom.newButton.textContent = state.entity === "theme" ? "새 ThemeA" : "새 EList";
      dom.newButton.disabled = isLocked || isQuarantined || mutationActive;
      dom.listDeleteButton.disabled = !canWrite() || !selectedCatalog();
      dom.catalog.hidden = state.mode !== "list";
      dom.editor.hidden = state.mode === "list";
      const draft = currentDraft();
      dom.code.textContent = draft
        ? (state.entity === "theme" ? draft.themeCode : draft.expertCode)
        : "";
      dom.name.value = draft
        ? (state.entity === "theme" ? draft.themeTitle : draft.expertName)
        : "";
      dom.name.placeholder = state.entity === "theme" ? "ThemeA 이름" : "전문가 이름";
      dom.name.disabled = isLocked || mutationActive;
      dom.buyAmount.hidden = state.entity !== "expert";
      dom.stockQuery.value = state.stockSearch.query;
      dom.stockQuery.disabled = isLocked || mutationActive || !draft;
      dom.stockSearchButton.disabled = isLocked || mutationActive || !draft ||
        state.stockSearch.status === "loading";
      dom.stockStatus.textContent = state.stockSearch.error ||
        (state.stockSearch.status === "loading"
          ? "종목 master 조회 중…"
          : (state.stockSearch.status === "selected"
            ? "DB에서 확인된 종목 identity를 선택했습니다."
            : (state.stockSearch.status === "ready"
              ? `${state.stockSearch.results.length}건 · 반드시 한 종목을 선택하세요.`
              : "검색어는 조회에만 사용되며 코드·이름으로 직접 저장되지 않습니다.")));
      const selectedStock = selectedStockIdentity();
      dom.selectedStock.textContent = selectedStock
        ? `${selectedStock.name} · ${selectedStock.code} · ${selectedStock.market}`
        : "선택된 DB 종목 없음";
      dom.buyAmount.disabled = isLocked || mutationActive || state.entity !== "expert";
      dom.addButton.disabled = isLocked || mutationActive || !draft || !selectedStock ||
        !stockIsCompatibleWithDraft(selectedStock, draft);
      dom.saveButton.disabled = !canWrite() || !draft;
      dom.deleteButton.disabled = !canWrite() ||
        (state.mode !== "edit" && !selectedCatalog());
      dom.cancelButton.disabled = mutationActive;
      dom.closeButton.disabled = mutationActive;
      renderCatalogRows();
      renderStockRows();
      renderDraftRows();

      if (state.open) {
        if (typeof dom.dialog.showModal === "function" && !dom.dialog.open) dom.dialog.showModal();
        else {
          dom.dialog.hidden = false;
          dom.dialog.setAttribute("open", "");
        }
      } else if (typeof dom.dialog.close === "function" && dom.dialog.open) {
        dom.dialog.close();
      } else {
        dom.dialog.hidden = true;
        dom.dialog.removeAttribute("open");
      }
      return snapshot();
    }

    return Object.freeze({
      mount,
      openTheme,
      openExpert,
      handleMessage,
      render,
      setLocked,
      close,
      search,
      beginNewTheme,
      beginNewExpert,
      updateDraft,
      updateStockQuery,
      searchStocks,
      selectStockResult,
      addThemeItem,
      addExpertRecommendation,
      moveDraftItem,
      removeDraftItem,
      selectCatalog,
      submit,
      deleteCurrent,
      loseCorrelation
    });
  }

  return Object.freeze({ createController });
});
