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

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

  if (!workflow || !workflow.bridgeContract) {
    throw new Error("MbnManualFinancialWorkflow must be loaded before manual-financial-ui.js.");
  }

  const contract = workflow.bridgeContract;
  const STOCK_SEARCH_REQUEST = "search-stocks";
  const STOCK_SEARCH_RESULT = "stock-search-results";
  const STOCK_SEARCH_ERROR = "stock-search-error";
  const STOCK_SEARCH_MAXIMUM_QUERY_LENGTH = 64;
  const DISPLAY_SORT_ASCENDING = "ascending";
  const DISPLAY_SORT_DESCENDING = "descending";
  const REQUEST_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
  const UNSAFE_TEXT_PATTERN = /[\p{Cc}\p{Cf}\p{Cs}\p{Zl}\p{Zp}\uFFFD]/u;
  const LEGACY_ACTION_SCREENS = Object.freeze({
    "manual-revenue-mix": "revenue-composition",
    "manual-growth-metrics": "growth-metrics",
    "manual-sales": "sales",
    "manual-operating-profit": "operating-profit"
  });
  const SCREEN_LABELS = Object.freeze({
    "revenue-composition": "주요매출 구성",
    "growth-metrics": "성장성 지표",
    sales: "매출액",
    "operating-profit": "영업이익"
  });
  const GROWTH_SERIES = Object.freeze([
    ["salesGrowth", "매출액 증가율"],
    ["operatingProfitGrowth", "영업이익 증가율"],
    ["netAssetGrowth", "순자산 증가율"],
    ["netIncomeGrowth", "순이익 증가율"]
  ]);

  let processWriteQuarantine = null;
  let controllerOrdinal = 0;

  function hasExactKeys(value, expectedKeys) {
    if (!value || typeof value !== "object" || Array.isArray(value)) return false;
    const actual = Object.keys(value).sort();
    const expected = [...expectedKeys].sort();
    return actual.length === expected.length &&
      actual.every((key, index) => key === expected[index]);
  }

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

  function textValue(value) {
    return value === undefined || value === null ? "" : String(value).trim();
  }

  function compareDisplayStockNames(left, right) {
    const foldedLeft = left.toUpperCase();
    const foldedRight = right.toUpperCase();
    if (foldedLeft < foldedRight) return -1;
    if (foldedLeft > foldedRight) return 1;
    if (left < right) return -1;
    if (left > right) return 1;
    return 0;
  }

  function sortDisplaySnapshots(items, direction = DISPLAY_SORT_ASCENDING) {
    if (!Array.isArray(items) ||
        ![DISPLAY_SORT_ASCENDING, DISPLAY_SORT_DESCENDING].includes(direction) ||
        items.some(item => !item || typeof item.stockName !== "string")) {
      throw new TypeError("Closed GraphE display rows and sort direction are required.");
    }
    const sign = direction === DISPLAY_SORT_ASCENDING ? 1 : -1;
    return Object.freeze(items
      .map((item, sourceIndex) => ({ item, sourceIndex }))
      .sort((left, right) => {
        const byName = compareDisplayStockNames(left.item.stockName, right.item.stockName);
        return byName === 0 ? left.sourceIndex - right.sourceIndex : byName * sign;
      })
      .map(value => value.item));
  }

  function freezeObject(value) {
    return Object.freeze({ ...value });
  }

  function resolveScreen(value) {
    if (typeof value === "string") {
      if (workflow.getScreenDefinition(value)) return value;
      return LEGACY_ACTION_SCREENS[value] || null;
    }
    if (!value || typeof value !== "object" || Array.isArray(value)) return null;
    if (typeof value.screen === "string" && workflow.getScreenDefinition(value.screen)) {
      return value.screen;
    }
    return typeof value.id === "string" ? LEGACY_ACTION_SCREENS[value.id] || null : null;
  }

  function emptyForm(screen, stockName = "") {
    if (!workflow.getScreenDefinition(screen)) return null;
    const result = { stockName: textValue(stockName) };
    if (screen === "revenue-composition") {
      result.baseDate = "";
      for (let index = 1; index <= 5; index += 1) {
        result[`slice${index}Label`] = "";
        result[`slice${index}Percentage`] = "";
      }
    } else if (screen === "growth-metrics") {
      for (let index = 1; index <= 4; index += 1) {
        result[`period${index}`] = "";
        for (const [series] of GROWTH_SERIES) result[`${series}${index}`] = "";
      }
    } else {
      for (let index = 1; index <= 6; index += 1) {
        result[`quarter${index}`] = "";
        result[`value${index}`] = "";
      }
    }
    return freezeObject(result);
  }

  function recordToForm(screen, value) {
    const record = workflow.normalizeRecord(screen, value);
    if (!record) return null;
    const result = { stockName: record.stockName };
    if (screen === "revenue-composition") {
      result.baseDate = record.baseDate;
      record.slices.forEach((slice, index) => {
        result[`slice${index + 1}Label`] = slice ? slice.label : "";
        result[`slice${index + 1}Percentage`] = slice ? slice.percentageText : "";
      });
    } else if (screen === "growth-metrics") {
      record.periods.forEach((period, index) => {
        result[`period${index + 1}`] = period;
      });
      for (const [series] of GROWTH_SERIES) {
        record[series].forEach((item, index) => {
          result[`${series}${index + 1}`] = item === null ? "" : String(item);
        });
      }
    } else {
      record.quarters.forEach((item, index) => {
        result[`quarter${index + 1}`] = item.quarter;
        result[`value${index + 1}`] = String(item.value);
      });
    }
    return freezeObject(result);
  }

  function optionalNumber(value) {
    const raw = textValue(value);
    if (raw === "") return null;
    if (!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(raw)) return undefined;
    const parsed = Number(raw);
    return Number.isFinite(parsed) ? parsed : undefined;
  }

  function requiredInteger(value) {
    const raw = textValue(value);
    if (!/^[+-]?\d+$/.test(raw)) return undefined;
    const parsed = Number(raw);
    return Number.isSafeInteger(parsed) ? parsed : undefined;
  }

  function formToRecord(screen, form) {
    if (!workflow.getScreenDefinition(screen) ||
        !form || typeof form !== "object" || Array.isArray(form)) return null;
    const stockName = textValue(form.stockName);
    let candidate;
    if (screen === "revenue-composition") {
      const slices = [];
      for (let index = 1; index <= 5; index += 1) {
        const label = textValue(form[`slice${index}Label`]);
        const percentageText = textValue(form[`slice${index}Percentage`]);
        if (!label && !percentageText) {
          slices.push(null);
          continue;
        }
        const percentage = percentageText === "-" ? 0 : optionalNumber(percentageText);
        if (!label || !percentageText || percentage === undefined || percentage === null) return null;
        slices.push({ label, percentageText, percentage });
      }
      candidate = {
        kind: screen,
        stockName,
        baseDate: textValue(form.baseDate),
        slices
      };
    } else if (screen === "growth-metrics") {
      candidate = { kind: screen, stockName };
      for (const [series] of GROWTH_SERIES) {
        const values = [];
        for (let index = 1; index <= 4; index += 1) {
          const parsed = optionalNumber(form[`${series}${index}`]);
          if (parsed === undefined) return null;
          values.push(parsed);
        }
        candidate[series] = values;
      }
      candidate.periods = [];
      for (let index = 1; index <= 4; index += 1) {
        candidate.periods.push(textValue(form[`period${index}`]));
      }
    } else {
      const quarters = [];
      for (let index = 1; index <= 6; index += 1) {
        const quarter = textValue(form[`quarter${index}`]);
        const value = requiredInteger(form[`value${index}`]);
        if (!quarter || value === undefined) return null;
        quarters.push({ quarter, value });
      }
      candidate = { kind: screen, stockName, quarters };
    }
    return workflow.normalizeRecord(screen, candidate);
  }

  function latchWriteQuarantine(message) {
    if (!processWriteQuarantine) {
      processWriteQuarantine = freezeObject({
        active: true,
        message: safeText(message, 512)
          ? message
          : "수동 재무 DB 쓰기 결과를 확인할 수 없습니다. 앱을 다시 시작하고 DB를 대조하세요."
      });
    }
    return processWriteQuarantine;
  }

  function getProcessWriteQuarantine() {
    return processWriteQuarantine || Object.freeze({ active: false, message: "" });
  }

  function createPendingCoordinator() {
    const slots = Object.create(null);

    function set(slot, request, context = {}) {
      if (typeof slot !== "string" || !request || !REQUEST_ID_PATTERN.test(request.requestId)) {
        throw new TypeError("A correlated request is required.");
      }
      slots[slot] = Object.freeze({ request, context: Object.freeze({ ...context }) });
      return slots[slot];
    }

    function get(slot) {
      return slots[slot] || null;
    }

    function matches(slot, requestId) {
      const pending = get(slot);
      return !!pending && pending.request.requestId === requestId;
    }

    function clear(slot, requestId) {
      if (!Object.prototype.hasOwnProperty.call(slots, slot)) return false;
      if (requestId !== undefined && slots[slot].request.requestId !== requestId) return false;
      delete slots[slot];
      return true;
    }

    function clearReads() {
      for (const slot of ["list", "load", "stock", "selection"]) delete slots[slot];
    }

    function snapshot() {
      const result = {};
      for (const [slot, pending] of Object.entries(slots)) {
        result[slot] = pending.request.requestId;
      }
      return Object.freeze(result);
    }

    function acceptMutation(kind, payload) {
      const pending = get("write");
      if (!pending) return Object.freeze({ status: "stale", value: null, pending: null });
      const requestId = payload && typeof payload === "object" ? payload.requestId : null;
      if (typeof requestId === "string" && REQUEST_ID_PATTERN.test(requestId) &&
          requestId !== pending.request.requestId) {
        return Object.freeze({ status: "stale", value: null, pending: null });
      }
      const value = kind === "result"
        ? workflow.normalizeMutationResult(payload, pending.request)
        : workflow.normalizeMutationError(payload, pending.request);
      clear("write");
      if (!value) {
        const quarantine = latchWriteQuarantine(
          "수동 재무 DB 쓰기 응답의 상관관계 또는 결과 형식을 확인할 수 없습니다.");
        return Object.freeze({ status: "invalid", value: null, pending, quarantine });
      }
      if (kind === "error" && value.outcomeUnknown) {
        latchWriteQuarantine(value.message);
      }
      return Object.freeze({ status: "accepted", value, pending });
    }

    return Object.freeze({
      set,
      get,
      matches,
      clear,
      clearReads,
      snapshot,
      acceptMutationResult: payload => acceptMutation("result", payload),
      acceptMutationError: payload => acceptMutation("error", payload)
    });
  }

  function normalizeStockSearchError(value, expectedRequest) {
    if (!hasExactKeys(value, ["requestId", "query", "message"]) ||
        !REQUEST_ID_PATTERN.test(value.requestId) ||
        !safeText(value.query, contract.maximumQueryLength, true) ||
        !safeText(value.message, 512) || !expectedRequest ||
        value.requestId !== expectedRequest.requestId ||
        value.query !== expectedRequest.query) return null;
    return freezeObject(value);
  }

  function preferredStockIdentity(raw, response, stockName) {
    const exact = response.results.filter(item => item.name === stockName);
    if (exact.length !== 1) return null;
    const only = exact[0];
    if (raw && typeof raw === "object" && !Array.isArray(raw) &&
        raw.name === only.name && raw.market === only.market && raw.code === only.code &&
        (raw.source === undefined || raw.source === only.source)) return only;
    return only;
  }

  function createController(options) {
    if (!options || typeof options !== "object" || Array.isArray(options)) {
      throw new TypeError("Manual-financial UI dependencies are required.");
    }
    const required = [
      "postNative", "appendPlaylistEntry", "isLocked", "getSelectedStock",
      "showToast", "addLog", "createId"
    ];
    for (const name of required) {
      if (typeof options[name] !== "function") {
        throw new TypeError(`Manual-financial UI dependency ${name} must be a function.`);
      }
    }

    const pending = createPendingCoordinator();
    const state = {
      mounted: false,
      visible: false,
      screen: null,
      profile: null,
      query: "",
      findQuery: "",
      hasLoadedList: false,
      sortDirection: DISPLAY_SORT_ASCENDING,
      sourceItems: Object.freeze([]),
      status: "idle",
      message: "",
      items: [],
      snapshot: null,
      form: null,
      stockStatus: "idle",
      verifiedStock: null,
      selectedStockIdentity: null,
      locked: false
    };
    const refs = Object.create(null);
    const instanceId = `manual-financial-${++controllerOrdinal}`;

    function externallyLocked() {
      let applicationLocked = true;
      try {
        applicationLocked = options.isLocked() !== false;
      } catch {
        applicationLocked = true;
      }
      return state.locked || applicationLocked;
    }

    function writeBlocked() {
      return externallyLocked() || !!pending.get("write") || !!pending.get("load") ||
        !!pending.get("stock") || !!pending.get("selection") ||
        getProcessWriteQuarantine().active;
    }

    function notify(message) {
      if (safeText(message, 512)) options.showToast(message);
    }

    function log(message) {
      if (safeText(message, 512)) options.addLog(message);
    }

    function nextRequestId() {
      const value = String(options.createId());
      if (!REQUEST_ID_PATTERN.test(value)) {
        throw new TypeError("createId must return a safe unique request id.");
      }
      return value;
    }

    function post(type, payload) {
      options.postNative(type, payload);
    }

    function seedStockName() {
      try {
        const stock = options.getSelectedStock();
        return stock && typeof stock.name === "string" ? stock.name : "";
      } catch {
        return "";
      }
    }

    function stateView() {
      const quarantine = getProcessWriteQuarantine();
      return Object.freeze({
        mounted: state.mounted,
        visible: state.visible,
        screen: state.screen,
        status: state.status,
        message: state.message,
        query: state.query,
        findQuery: state.findQuery,
        hasLoadedList: state.hasLoadedList,
        sortDirection: state.sortDirection,
        itemCount: state.items.length,
        selectedStockName: state.snapshot?.stockName || state.form?.stockName || "",
        hasSnapshot: !!state.snapshot,
        stockStatus: state.stockStatus,
        verifiedStock: state.verifiedStock
          ? freezeObject(state.verifiedStock)
          : null,
        pending: pending.snapshot(),
        locked: externallyLocked(),
        writeQuarantined: quarantine.active,
        quarantineMessage: quarantine.message,
        canWrite: !writeBlocked()
      });
    }

    function setStatus(status, message = "") {
      state.status = status;
      state.message = safeText(message, 512, true) ? message : "";
    }

    function requestList(query = state.query) {
      if (!state.screen) return false;
      if (pending.get("write")) {
        notify("DB 쓰기 결과를 기다리는 동안 목록을 다시 조회할 수 없습니다.");
        return false;
      }
      try {
        const request = workflow.createListRequest(
          nextRequestId(), state.screen, textValue(query), contract.defaultMaximumResults);
        state.query = request.query;
        state.findQuery = request.query;
        state.hasLoadedList = false;
        pending.set("list", request);
        setStatus("loading", "수동 재무 목록을 조회하고 있습니다.");
        post(contract.listRequestType, request);
        render();
        return true;
      } catch {
        setStatus("error", "목록 검색 조건이 올바르지 않습니다.");
        render();
        return false;
      }
    }

    function requestLoad(stockName) {
      if (!state.screen) return false;
      if (pending.get("write")) {
        notify("DB 쓰기 결과를 기다리는 동안 행을 다시 조회할 수 없습니다.");
        return false;
      }
      try {
        const request = workflow.createLoadRequest(nextRequestId(), state.screen, stockName);
        pending.set("load", request);
        setStatus("loading", `${stockName} 데이터를 다시 확인하고 있습니다.`);
        post(contract.loadRequestType, request);
        render();
        return true;
      } catch {
        setStatus("error", "선택한 수동 재무 행을 조회할 수 없습니다.");
        render();
        return false;
      }
    }

    function toggleDisplaySort() {
      state.sortDirection = state.sortDirection === DISPLAY_SORT_ASCENDING
        ? DISPLAY_SORT_DESCENDING
        : DISPLAY_SORT_ASCENDING;
      state.items = sortDisplaySnapshots(state.sourceItems, state.sortDirection);
      render();
      return true;
    }

    function findBlocked() {
      return !state.visible || !state.screen || !state.hasLoadedList ||
        externallyLocked() || !!pending.get("list") || !!pending.get("load") ||
        !!pending.get("write") || !!pending.get("stock") || !!pending.get("selection");
    }

    function requestNextMatchingLoad(query) {
      if (findBlocked()) return false;
      state.findQuery = textValue(query);
      const needle = state.findQuery.toUpperCase();
      if (!needle) return false;

      const selectedName = state.snapshot?.stockName || null;
      const selectedIndex = selectedName === null
        ? -1
        : state.items.findIndex(item => item.stockName === selectedName);
      for (let index = selectedIndex + 1; index < state.items.length; index += 1) {
        const candidate = state.items[index];
        if (candidate.stockName.toUpperCase().includes(needle)) {
          return requestLoad(candidate.stockName);
        }
      }
      return false;
    }

    function requestPreviousMatchingLoad(query) {
      if (findBlocked() || !state.snapshot) return false;
      state.findQuery = textValue(query);
      const needle = state.findQuery.toUpperCase();
      if (!needle) return false;

      const selectedIndex = state.items.findIndex(
        item => item.stockName === state.snapshot.stockName);
      if (selectedIndex < 0) return false;
      for (let index = selectedIndex - 1; index >= 0; index -= 1) {
        const candidate = state.items[index];
        if (candidate.stockName.toUpperCase().includes(needle)) {
          return requestLoad(candidate.stockName);
        }
      }
      return false;
    }

    function syncFormFromDom() {
      if (!state.form || !refs.formInputs) return;
      const next = { ...state.form };
      for (const [key, input] of refs.formInputs.entries()) next[key] = input.value;
      state.form = freezeObject(next);
    }

    function currentRecord() {
      syncFormFromDom();
      return formToRecord(state.screen, state.form);
    }

    function beginStockVerification(intent) {
      if (intent === "verify-only" &&
          (pending.get("write") || pending.get("load") || pending.get("selection"))) {
        notify("진행 중인 GraphE 요청이 끝난 뒤 종목을 다시 확인하세요.");
        return false;
      }
      if (!state.screen || ["create", "update", "delete", "playlist"].includes(intent) &&
          writeBlocked()) {
        notify(getProcessWriteQuarantine().active
          ? getProcessWriteQuarantine().message
          : "송출 또는 다른 쓰기 작업 중에는 수동 재무 데이터를 변경할 수 없습니다.");
        return false;
      }
      if ((intent === "update" || intent === "delete" || intent === "playlist") && !state.snapshot) {
        notify("DB 목록에서 먼저 행을 선택하세요.");
        return false;
      }
      const record = currentRecord();
      if (!record) {
        notify("화면 입력값과 필수 개수를 확인하세요.");
        return false;
      }
      if (state.snapshot && record.stockName !== state.snapshot.stockName) {
        notify("기존 행의 종목명은 변경할 수 없습니다.");
        return false;
      }
      if (record.stockName.length > STOCK_SEARCH_MAXIMUM_QUERY_LENGTH) {
        notify(`종목명은 재검증을 위해 ${STOCK_SEARCH_MAXIMUM_QUERY_LENGTH}자 이하여야 합니다.`);
        return false;
      }
      let preferred = null;
      try {
        preferred = options.getSelectedStock();
      } catch {
        preferred = null;
      }
      let request;
      try {
        request = Object.freeze({
          requestId: nextRequestId(),
          query: record.stockName,
          maximumResults: contract.defaultMaximumResults
        });
      } catch {
        notify("종목 검색 요청 ID를 만들 수 없습니다.");
        return false;
      }
      pending.set("stock", request, {
        intent,
        record,
        snapshot: state.snapshot,
        preferred
      });
      state.verifiedStock = null;
      state.stockStatus = "loading";
      setStatus("loading", `${record.stockName} 종목 identity를 DB에서 다시 확인하고 있습니다.`);
      post(STOCK_SEARCH_REQUEST, request);
      render();
      return true;
    }

    function postMutation(operation, record, snapshot) {
      if (writeBlocked()) return false;
      let request;
      let type;
      try {
        if (operation === "create") {
          request = workflow.createCreateRequest(nextRequestId(), record, state.verifiedStock);
          type = contract.createRequestType;
        } else if (operation === "update") {
          request = workflow.createUpdateRequest(nextRequestId(), snapshot, record);
          type = contract.updateRequestType;
        } else if (operation === "delete-one") {
          request = workflow.createDeleteRequest(nextRequestId(), snapshot);
          type = contract.deleteRequestType;
        } else {
          return false;
        }
      } catch {
        notify("DB 쓰기 요청을 안전하게 만들 수 없습니다.");
        return false;
      }
      pending.set("write", request, { operation });
      setStatus("writing", "DB 쓰기 결과를 기다리고 있습니다. 자동 재시도하지 않습니다.");
      post(type, request);
      render();
      return true;
    }

    function postSelection(snapshot, verifiedStock) {
      if (externallyLocked() || !snapshot || snapshot !== state.snapshot) return false;
      try {
        const request = workflow.createSelectionRequest(nextRequestId(), snapshot, verifiedStock);
        pending.set("selection", request, { snapshot, verifiedStock });
        setStatus("loading", "플레이리스트 장면 identity를 네이티브에서 확인하고 있습니다.");
        post(contract.selectionRequestType, request);
        render();
        return true;
      } catch {
        notify("플레이리스트 선택 요청을 만들 수 없습니다.");
        return false;
      }
    }

    function continueAfterStockVerification(context, verifiedStock) {
      state.verifiedStock = verifiedStock;
      state.selectedStockIdentity = freezeObject(verifiedStock);
      state.stockStatus = "verified";
      if (context.intent === "verify-only") {
        setStatus("ready", `${verifiedStock.name} 종목 identity를 확인했습니다.`);
        notify("종목 identity를 DB에서 확인했습니다.");
        render();
        return true;
      }
      if (context.snapshot !== state.snapshot) {
        setStatus("error", "행 선택이 변경되어 작업을 중단했습니다.");
        render();
        return false;
      }
      if (context.intent === "create") return postMutation("create", context.record, null);
      if (context.intent === "update") return postMutation("update", context.record, context.snapshot);
      if (context.intent === "delete") return postMutation("delete-one", context.record, context.snapshot);
      if (context.intent === "playlist") return postSelection(context.snapshot, verifiedStock);
      return false;
    }

    function deleteAll() {
      if (!state.screen || writeBlocked()) {
        notify(getProcessWriteQuarantine().active
          ? getProcessWriteQuarantine().message
          : "현재는 전체 삭제를 실행할 수 없습니다.");
        return false;
      }
      const profile = workflow.getScreenDefinition(state.screen);
      const confirmFunction = root && typeof root.confirm === "function" ? root.confirm.bind(root) : null;
      if (!confirmFunction || !confirmFunction(`${SCREEN_LABELS[state.screen]} 전체 행을 삭제할까요?`)) {
        return false;
      }
      try {
        const request = workflow.createDeleteAllRequest(
          nextRequestId(), state.screen, profile.deleteAllConfirmation);
        pending.set("write", request, { operation: "delete-all" });
        setStatus("writing", "전체 삭제 결과를 기다리고 있습니다. 자동 재시도하지 않습니다.");
        post(contract.deleteAllRequestType, request);
        render();
        return true;
      } catch {
        notify("전체 삭제 요청을 안전하게 만들 수 없습니다.");
        return false;
      }
    }

    function consumeRead(slot, payload, normalizer, onAccepted) {
      const requestId = payload && typeof payload === "object" ? payload.requestId : null;
      const active = pending.get(slot);
      if (!active || active.request.requestId !== requestId) return false;
      const normalized = normalizer(payload, active.request);
      pending.clear(slot);
      if (!normalized) {
        setStatus("error", "네이티브 응답 형식 또는 상관관계가 올바르지 않습니다.");
        render();
        return true;
      }
      onAccepted(normalized, active);
      render();
      return true;
    }

    function handleListResult(payload) {
      return consumeRead("list", payload, workflow.normalizeListResponse, normalized => {
        state.sourceItems = normalized.items;
        state.sortDirection = DISPLAY_SORT_ASCENDING;
        state.items = sortDisplaySnapshots(state.sourceItems, state.sortDirection);
        state.hasLoadedList = true;
        setStatus("ready", `${normalized.totalRowCount}개 행을 조회했습니다.`);
      });
    }

    function handleLoadResult(payload) {
      return consumeRead("load", payload, workflow.normalizeLoadResponse, normalized => {
        state.snapshot = normalized.snapshot;
        state.form = recordToForm(state.screen, normalized.snapshot.record);
        state.verifiedStock = null;
        state.stockStatus = "idle";
        setStatus("ready", `${normalized.snapshot.stockName} 행을 불러왔습니다.`);
      });
    }

    function handleStockResult(payload) {
      const active = pending.get("stock");
      const requestId = payload && typeof payload === "object" ? payload.requestId : null;
      if (!active || requestId !== active.request.requestId) return false;
      const normalized = workflow.normalizeStockSearchResponse(payload);
      pending.clear("stock");
      if (!normalized || normalized.query !== active.request.query || normalized.truncated ||
          normalized.totalRowCount > active.request.maximumResults) {
        state.stockStatus = "error";
        setStatus("error", "종목 검색 응답 형식 또는 상관관계가 올바르지 않습니다.");
        render();
        return true;
      }
      const identity = preferredStockIdentity(
        active.context.preferred,
        normalized,
        active.context.record.stockName);
      const verified = identity && workflow.verifyStockForRecord(
        active.context.record,
        normalized,
        identity.market,
        identity.code);
      if (!verified) {
        state.stockStatus = "error";
        setStatus("error", "같은 이름의 종목이 없거나 시장 identity가 모호합니다.");
        render();
        return true;
      }
      continueAfterStockVerification(active.context, verified);
      return true;
    }

    function handleSelectionResult(payload) {
      return consumeRead("selection", payload, workflow.normalizeSelectionResponse,
        (normalized, active) => {
          try {
            const entry = workflow.createPlaylistEntry(
              active.context.snapshot,
              active.context.verifiedStock,
              normalized,
              { id: nextRequestId(), fadeDuration: 6 });
            if (!workflow.isPlaylistEntryPlayable(entry)) throw new TypeError("Untrusted entry.");
            if (options.appendPlaylistEntry(entry) === false) {
              throw new Error("The host rejected the playlist entry.");
            }
            setStatus("ready", `${entry.title} ${SCREEN_LABELS[state.screen]} 항목을 추가했습니다.`);
            notify("플레이리스트에 추가했습니다.");
            log(`GraphE playlist add · ${entry.code} · ${entry.title}`);
          } catch {
            setStatus("error", "네이티브 확인 결과로 플레이리스트 항목을 만들 수 없습니다.");
          }
        });
    }

    function handleReadError(slot, payload, normalizer) {
      const active = pending.get(slot);
      const requestId = payload && typeof payload === "object" ? payload.requestId : null;
      if (!active || requestId !== active.request.requestId) return false;
      const normalized = normalizer(payload, active.request);
      pending.clear(slot);
      setStatus("error", normalized?.message || "네이티브 오류 응답 형식이 올바르지 않습니다.");
      render();
      return true;
    }

    function handleStockError(payload) {
      const active = pending.get("stock");
      const requestId = payload && typeof payload === "object" ? payload.requestId : null;
      if (!active || requestId !== active.request.requestId) return false;
      const normalized = normalizeStockSearchError(payload, active.request);
      pending.clear("stock");
      state.stockStatus = "error";
      setStatus("error", normalized?.message || "종목 검색 오류 응답 형식이 올바르지 않습니다.");
      render();
      return true;
    }

    function handleMutation(kind, payload) {
      const outcome = kind === "result"
        ? pending.acceptMutationResult(payload)
        : pending.acceptMutationError(payload);
      if (outcome.status === "stale") return false;
      if (outcome.status === "invalid") {
        setStatus("quarantined", getProcessWriteQuarantine().message);
        notify(getProcessWriteQuarantine().message);
        render();
        return true;
      }
      if (kind === "error") {
        setStatus(outcome.value.outcomeUnknown ? "quarantined" : "error", outcome.value.message);
        notify(outcome.value.message);
        render();
        return true;
      }
      const result = outcome.value;
      log(`GraphE DB ${result.operation} committed · ${result.screen} · ${result.affectedRows}`);
      notify("수동 재무 DB 변경이 커밋되었습니다.");
      if (result.operation === "delete-one" || result.operation === "delete-all") {
        state.snapshot = null;
        state.form = emptyForm(state.screen, seedStockName());
        state.verifiedStock = null;
      }
      setStatus("ready", "DB 변경을 확인했습니다.");
      requestList(state.query);
      if (result.operation === "create" || result.operation === "update") {
        requestLoad(result.stockName);
      }
      render();
      return true;
    }

    function handleGenericRequestError(payload) {
      const operation = payload && typeof payload === "object" ? payload.operation : null;
      const slot = operation === "list" ? "list"
        : operation === "load" ? "load"
          : operation === "selection" ? "selection"
            : ["create", "update", "delete-one", "delete-all"].includes(operation)
              ? "write"
              : null;
      const active = slot && pending.get(slot);
      if (!active || payload.requestId !== active.request.requestId) return false;
      const expectedOperation = slot === "write" ? active.context.operation : operation;
      const normalized = workflow.normalizeRequestError(payload, expectedOperation);
      pending.clear(slot);
      if (slot === "write" && !normalized) {
        latchWriteQuarantine(
          "수동 재무 DB 쓰기 요청 오류의 상관관계를 확인할 수 없습니다.");
      }
      const quarantined = slot === "write" && !normalized;
      setStatus(
        quarantined ? "quarantined" : "error",
        quarantined
          ? getProcessWriteQuarantine().message
          : normalized?.message || "네이티브 요청 오류 형식이 올바르지 않습니다.");
      render();
      return true;
    }

    function handleMessage(type, payload) {
      switch (type) {
        case contract.listResultType:
          return handleListResult(payload);
        case contract.listErrorType:
          return handleReadError("list", payload, workflow.normalizeListError);
        case contract.loadResultType:
          return handleLoadResult(payload);
        case contract.loadErrorType:
          return handleReadError("load", payload, workflow.normalizeLoadError);
        case STOCK_SEARCH_RESULT:
          return handleStockResult(payload);
        case STOCK_SEARCH_ERROR:
          return handleStockError(payload);
        case contract.selectionResultType:
          return handleSelectionResult(payload);
        case contract.selectionErrorType:
          return handleReadError("selection", payload, workflow.normalizeSelectionError);
        case contract.mutationResultType:
          return handleMutation("result", payload);
        case contract.mutationErrorType:
          return handleMutation("error", payload);
        case contract.requestErrorType:
          return handleGenericRequestError(payload);
        default:
          return false;
      }
    }

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

    function makeButton(documentObject, text, className, handler) {
      const button = makeElement(documentObject, "button", className, text);
      button.type = "button";
      button.addEventListener("click", handler);
      return button;
    }

    function mount(host = typeof document === "object" ? document.body : null) {
      if (state.mounted) return refs.overlay;
      if (!host || typeof host.appendChild !== "function") {
        throw new TypeError("A DOM host is required to mount the manual-financial UI.");
      }
      const documentObject = host.ownerDocument || (root && root.document);
      if (!documentObject || typeof documentObject.createElement !== "function") {
        throw new TypeError("A DOM document is required to mount the manual-financial UI.");
      }

      const overlay = makeElement(documentObject, "div", "mfui-overlay");
      overlay.hidden = true;
      const dialog = makeElement(documentObject, "section", "mfui-dialog");
      dialog.setAttribute("role", "dialog");
      dialog.setAttribute("aria-modal", "true");
      dialog.setAttribute("aria-labelledby", `${instanceId}-title`);

      const header = makeElement(documentObject, "header", "mfui-header");
      const heading = makeElement(documentObject, "div", "mfui-heading");
      const title = makeElement(documentObject, "h2", "mfui-title", "수동 재무 입력");
      title.id = `${instanceId}-title`;
      const subtitle = makeElement(documentObject, "p", "mfui-subtitle", "GraphE DB 행을 안전하게 조회합니다.");
      heading.append(title, subtitle);
      const close = makeButton(documentObject, "닫기", "mfui-close", () => {
        state.visible = false;
        render();
      });
      header.append(heading, close);

      const body = makeElement(documentObject, "div", "mfui-body");
      const listPanel = makeElement(documentObject, "aside", "mfui-list-panel");
      const searchForm = makeElement(documentObject, "form", "mfui-search");
      const query = makeElement(documentObject, "input", "mfui-search-input");
      query.type = "search";
      query.maxLength = contract.maximumQueryLength;
      query.placeholder = "저장 종목명 검색";
      query.setAttribute("aria-label", "수동 재무 저장 종목 검색");
      query.addEventListener("input", () => {
        state.findQuery = query.value;
      });
      query.addEventListener("keydown", event => {
        if (event.key !== "Enter" || event.isComposing || !state.hasLoadedList) return;
        event.preventDefault();
        requestNextMatchingLoad(query.value);
      });
      const searchButton = makeElement(documentObject, "button", "mfui-search-button", "조회");
      searchButton.type = "submit";
      const findPrevious = makeButton(
        documentObject,
        "위로 찾기",
        "mfui-find-button mfui-find-previous",
        () => requestPreviousMatchingLoad(query.value));
      findPrevious.setAttribute("aria-label", "현재 선택 이전의 일치 종목 찾기");
      const findNext = makeButton(
        documentObject,
        "아래로 찾기",
        "mfui-find-button mfui-find-next",
        () => requestNextMatchingLoad(query.value));
      findNext.setAttribute("aria-label", "현재 선택 다음의 일치 종목 찾기");
      searchForm.append(query, searchButton, findPrevious, findNext);
      searchForm.addEventListener("submit", event => {
        event.preventDefault();
        requestList(query.value);
      });
      const listState = makeElement(documentObject, "p", "mfui-list-state", "목록을 열어 주세요.");
      const sortHeader = makeButton(
        documentObject,
        "종목명 ↕",
        "mfui-list-sort",
        toggleDisplaySort);
      sortHeader.setAttribute("aria-label", "종목명 오름차순, 내림차순으로 전환");
      const list = makeElement(documentObject, "div", "mfui-list");
      list.setAttribute("role", "listbox");
      listPanel.append(searchForm, listState, sortHeader, list);

      const editor = makeElement(documentObject, "article", "mfui-editor");
      const editorTitle = makeElement(documentObject, "h3", "mfui-editor-title", "입력값");
      const stockStatus = makeElement(documentObject, "p", "mfui-stock-state", "종목 identity 미확인");
      const form = makeElement(documentObject, "div", "mfui-form");
      const actions = makeElement(documentObject, "div", "mfui-actions");
      const reset = makeButton(documentObject, "새 입력", "mfui-button", () => {
        if (writeBlocked()) {
          notify("진행 중인 GraphE 작업이 끝난 뒤 새 입력을 시작하세요.");
          return;
        }
        state.snapshot = null;
        state.form = emptyForm(state.screen, seedStockName());
        state.verifiedStock = null;
        state.stockStatus = "idle";
        setStatus("ready", "새 수동 재무 행을 입력할 수 있습니다.");
        render();
      });
      const verify = makeButton(documentObject, "종목 재검증", "mfui-button", () => beginStockVerification("verify-only"));
      const create = makeButton(documentObject, "신규 저장", "mfui-button mfui-primary", () => beginStockVerification("create"));
      const update = makeButton(documentObject, "수정 저장", "mfui-button", () => beginStockVerification("update"));
      const remove = makeButton(documentObject, "선택 삭제", "mfui-button mfui-danger", () => beginStockVerification("delete"));
      const removeAll = makeButton(documentObject, "전체 삭제", "mfui-button mfui-danger", deleteAll);
      const playlist = makeButton(documentObject, "플레이리스트 추가", "mfui-button mfui-accent", () => beginStockVerification("playlist"));
      actions.append(reset, verify, create, update, remove, removeAll, playlist);
      editor.append(editorTitle, stockStatus, form, actions);
      body.append(listPanel, editor);

      const footer = makeElement(documentObject, "footer", "mfui-footer");
      const status = makeElement(documentObject, "p", "mfui-status", "준비됨");
      const quarantine = makeElement(documentObject, "p", "mfui-quarantine");
      footer.append(status, quarantine);
      dialog.append(header, body, footer);
      overlay.append(dialog);
      host.appendChild(overlay);

      Object.assign(refs, {
        overlay,
        dialog,
        title,
        subtitle,
        close,
        query,
        searchButton,
        findPrevious,
        findNext,
        listState,
        sortHeader,
        list,
        editorTitle,
        stockStatus,
        form,
        actions,
        reset,
        verify,
        create,
        update,
        remove,
        removeAll,
        playlist,
        status,
        quarantine,
        formInputs: new Map()
      });
      state.mounted = true;
      render();
      return overlay;
    }

    function appendField(container, labelText, key, options = {}) {
      const documentObject = container.ownerDocument;
      const field = makeElement(documentObject, "label", "mfui-field");
      const label = makeElement(documentObject, "span", "mfui-field-label", labelText);
      const input = makeElement(documentObject, "input", "mfui-input");
      input.type = options.type || "text";
      input.inputMode = options.inputMode || "text";
      input.maxLength = options.maxLength || 200;
      input.value = state.form?.[key] || "";
      input.readOnly = options.readOnly === true;
      input.addEventListener("input", () => {
        state.form = freezeObject({ ...state.form, [key]: input.value });
        if (key === "stockName") {
          state.verifiedStock = null;
          state.stockStatus = "idle";
        }
      });
      field.append(label, input);
      container.append(field);
      refs.formInputs.set(key, input);
      return input;
    }

    function appendGroup(titleText) {
      const documentObject = refs.form.ownerDocument;
      const group = makeElement(documentObject, "fieldset", "mfui-group");
      const legend = makeElement(documentObject, "legend", "mfui-group-title", titleText);
      group.append(legend);
      refs.form.append(group);
      return group;
    }

    function renderForm() {
      if (!state.mounted) return;
      refs.form.replaceChildren();
      refs.formInputs = new Map();
      if (!state.screen || !state.form) return;
      appendField(refs.form, "종목명", "stockName", { readOnly: !!state.snapshot });
      if (state.screen === "revenue-composition") {
        appendField(refs.form, "기준일", "baseDate");
        for (let index = 1; index <= 5; index += 1) {
          const group = appendGroup(`구성 ${index}`);
          appendField(group, "구성명", `slice${index}Label`);
          appendField(group, "비율", `slice${index}Percentage`, { inputMode: "decimal", maxLength: 64 });
        }
      } else if (state.screen === "growth-metrics") {
        const periods = appendGroup("분기");
        for (let index = 1; index <= 4; index += 1) {
          appendField(periods, `분기 ${index}`, `period${index}`);
        }
        for (const [series, label] of GROWTH_SERIES) {
          const group = appendGroup(label);
          for (let index = 1; index <= 4; index += 1) {
            appendField(group, `값 ${index}`, `${series}${index}`, { inputMode: "decimal", maxLength: 64 });
          }
        }
      } else {
        for (let index = 1; index <= 6; index += 1) {
          const group = appendGroup(`분기·금액 ${index}`);
          appendField(group, "분기", `quarter${index}`);
          appendField(group, "금액", `value${index}`, { inputMode: "numeric", maxLength: 16 });
        }
      }
    }

    function renderList() {
      if (!state.mounted) return;
      refs.list.replaceChildren();
      const documentObject = refs.list.ownerDocument;
      for (const snapshot of state.items) {
        const row = makeButton(documentObject, snapshot.stockName, "mfui-list-row", () => {
          requestLoad(snapshot.stockName);
        });
        row.setAttribute("role", "option");
        row.setAttribute("aria-selected", state.snapshot?.stockName === snapshot.stockName ? "true" : "false");
        refs.list.append(row);
      }
      if (!state.items.length) {
        refs.list.append(makeElement(documentObject, "p", "mfui-empty", "조회된 행이 없습니다."));
      }
    }

    function render() {
      const view = stateView();
      if (!state.mounted) return view;
      refs.overlay.hidden = !state.visible;
      refs.title.textContent = state.screen ? SCREEN_LABELS[state.screen] : "수동 재무 입력";
      refs.subtitle.textContent = state.profile
        ? `${state.profile.table} · ${state.profile.builderKey}/${state.profile.code}`
        : "GraphE DB 행을 안전하게 조회합니다.";
      refs.query.value = state.findQuery;
      refs.listState.textContent = state.status === "loading" && pending.get("list")
        ? "목록 조회 중"
        : `${state.items.length}개 행`;
      refs.sortHeader.dataset.direction = state.sortDirection;
      refs.sortHeader.setAttribute(
        "aria-pressed",
        state.sortDirection === DISPLAY_SORT_DESCENDING ? "true" : "false");
      refs.sortHeader.setAttribute(
        "aria-label",
        state.sortDirection === DISPLAY_SORT_ASCENDING
          ? "종목명 오름차순, 내림차순으로 전환"
          : "종목명 내림차순, 오름차순으로 전환");
      refs.editorTitle.textContent = state.snapshot
        ? `${state.snapshot.stockName} 수정`
        : "신규 입력";
      refs.stockStatus.textContent = state.stockStatus === "verified" && state.verifiedStock
        ? `${state.verifiedStock.market} · ${state.verifiedStock.code} 확인됨`
        : state.stockStatus === "loading"
          ? "종목 identity 확인 중"
          : state.stockStatus === "error"
            ? "종목 identity 확인 실패"
            : "종목 identity 미확인";
      refs.status.textContent = state.message || state.status;
      refs.status.dataset.status = state.status;
      refs.quarantine.hidden = !view.writeQuarantined;
      refs.quarantine.textContent = view.quarantineMessage;
      const blocked = writeBlocked();
      refs.searchButton.disabled = !!pending.get("list");
      refs.findPrevious.disabled = findBlocked();
      refs.findNext.disabled = findBlocked();
      refs.verify.disabled = externallyLocked() || !!pending.get("stock") ||
        !!pending.get("write") || !!pending.get("load") || !!pending.get("selection");
      refs.reset.disabled = writeBlocked();
      refs.create.disabled = blocked || !!state.snapshot;
      refs.update.disabled = blocked || !state.snapshot;
      refs.remove.disabled = blocked || !state.snapshot;
      refs.removeAll.disabled = blocked;
      refs.playlist.disabled = writeBlocked() || !state.snapshot;
      renderList();
      renderForm();
      return view;
    }

    function open(value) {
      if (pending.get("write")) {
        notify("DB 쓰기 결과를 기다리는 동안 다른 GraphE 화면으로 전환할 수 없습니다.");
        return false;
      }
      const screen = resolveScreen(value);
      if (!screen) {
        notify("지원하지 않는 수동 재무 화면입니다.");
        return false;
      }
      pending.clearReads();
      state.screen = screen;
      state.profile = workflow.getScreenDefinition(screen);
      state.visible = true;
      state.query = "";
      state.findQuery = "";
      state.hasLoadedList = false;
      state.sortDirection = DISPLAY_SORT_ASCENDING;
      state.sourceItems = Object.freeze([]);
      state.items = Object.freeze([]);
      state.snapshot = null;
      state.form = emptyForm(screen, seedStockName());
      state.stockStatus = "idle";
      state.verifiedStock = null;
      setStatus("idle", "수동 재무 목록을 조회합니다.");
      render();
      return requestList("");
    }

    function setLocked(value) {
      if (typeof value !== "boolean") throw new TypeError("setLocked requires a boolean value.");
      state.locked = value;
      return render();
    }

    return Object.freeze({ mount, open, handleMessage, render, setLocked });
  }

  /**
   * App integration hook (intentionally not wired here):
   * 1. Load manual-financial-workflow.js, this file and manual-financial-ui.css.
   * 2. Construct one controller with createController(...), then call mount(document.body).
   * 3. Route the four legacy manual action clicks to controller.open(action).
   * 4. Offer every WebView message to controller.handleMessage(type, payload); it returns false
   *    for unrelated/shared stock messages that do not match its active request.
   * 5. Mirror playlist PREPARED/PROGRAM/command locks through controller.setLocked(boolean).
   * The host must keep this controller alive while a native request is pending so correlation and
   * the process-lifetime OutcomeUnknown quarantine cannot be lost.
   */

  return {
    createController,
    resolveScreen,
    emptyForm,
    recordToForm,
    formToRecord,
    sortDisplaySnapshots,
    createPendingCoordinator,
    getProcessWriteQuarantine
  };
});
