import {
  ACCEPT_ATTR,
  CREATE_DOC_TYPES,
  classifyError,
  configureOfficeLicense,
  createOfficeController,
  getCreateDocType,
  isOpenCancelledError,
  isSupportedOfficeFile,
} from "../../shared/officeWidget.js";
import { SUPPORTED_LANGS } from "../../shared/ui-defaults.js";

const SDK_UI_ENTRY = "../../lib/UI.js";
const SDK_PRELOAD_ENTRY = "../../lib/preload.js";
const MAX_LOG_ITEMS = 160;
const DEMO_LAYOUT_VERSION = "create-file";
const LOCALE_STORAGE_KEY = "office-sdk-full-evaluation-locale";
const SUPPORTED_DEMO_LOCALES = ["zh-CN", "en-US"];

const I18N = {
  "zh-CN": {
    preloadSdk: "预加载 SDK",
    refreshVisibleArea: "刷新可视区",
    hideLeft: "隐藏左侧",
    showLeft: "显示左侧",
    apiPanel: "API 面板",
    hideApi: "隐藏 API",
    closeDocument: "关闭文档",
    openDocumentTitle: "打开文档",
    chooseFile: "选择或拖拽文件",
    fileNotSelected: "尚未选择文件",
    openSelectedFile: "打开所选文件",
    createDocumentTitle: "新建文档",
    documentType: "文档类型",
    createDocumentType: "新建文档类型",
    createFile: "新建文件",
    openOptions: "打开选项",
    documentLanguage: "语言",
    readOnlyMode: "只读模式",
    showTopbar: "显示顶部工具栏",
    showBottombar: "显示底部状态栏",
    optionsHint: "以上选项会在下一次打开文档时生效。",
    readyTitle: "准备就绪",
    readyDetail: "请通过本地 HTTP 服务打开本页面，然后选择本地文档或新建文件。",
    openStateClosed: "未打开",
    openStateOpen: "已打开",
    apiTestPanel: "API 测试面板",
    apiCalls: "API 调用",
    eventSubscriptions: "事件订阅",
    refreshDocs: "刷新文档",
    apiDocHint: 'API 方法下拉会从 <code>{path}</code> 实时读取当前文档类型的 Markdown；SDK 文档更新后点击“刷新文档”即可重新加载。',
    component: "组件",
    methodName: "方法名",
    selectApiAfterOpen: "打开文档后选择 API。",
    jsonArgs: "参数 JSON 数组",
    jsonArgsPlaceholder: '例如 [120] 或 [{"r":68,"g":114,"b":196,"a":1}]',
    runSelectedApi: "执行所选 API",
    eventsHint: "组件和事件名下拉会从当前文档类型的 Markdown 事件表读取；文档新增事件后点击“刷新文档”即可同步。",
    eventName: "事件名",
    subscribe: "订阅",
    unsubscribe: "取消订阅",
    logTitle: "调用结果 / 事件日志",
    copy: "复制",
    clear: "清空",
    logHint: "API 调用结果、错误、订阅事件都会统一记录在这里。",
    none: "无",
    unknown: "未知",
    readLoading: "读取中",
    noMethod: "暂无方法",
    noEvent: "暂无事件",
    noComponent: "暂无组件",
    noComponentEvent: "暂无组件事件",
    openDocumentFirst: "请先打开文档",
    readFailed: "读取失败",
    loadingSdkTitle: "正在加载 SDK",
    loadingSdkDetail: "请确认页面不是以 file:// 打开，并且 lib 目录完整存在。",
    sdkLoadedTitle: "SDK 已加载",
    sdkLoadedDetail: "可以选择文档打开，或直接新建文件。",
    opening: "打开中...",
    openingDocument: "正在打开文档",
    creatingDocument: "正在新建文档",
    documentLoaded: "文档已加载",
    documentCreated: "文档已创建",
    documentClosed: "文档已关闭",
    chooseAnotherFile: "可以重新选择文件。",
    fileSelected: "文件已选择",
    unsupportedFile: "文件类型不支持",
    preloadFailed: "预加载失败",
    apiLoadPrompt: "打开文档后，将从 {path} 中读取当前文档类型的 API 方法表。",
    readingApiDocs: "正在从 {path} 读取 API 文档...",
    apiDocsLoadFailed: "读取 API 文档失败。请确认 {path} 目录已随 SDK 一起部署，并通过 HTTP 服务访问本页面。",
    noMethodsParsed: "未在 {path} 中解析到方法表。",
    noCallableMethod: "当前组件在文档中暂无可调用方法。",
    chooseComponentAndMethod: "请选择组件和方法。",
    docNoDescription: "文档暂无说明",
    returnLabel: "返回",
    paramsLabel: "参数",
    sourceLabel: "来源",
    noArgs: "无参数",
    chooseApiMethod: "请选择要调用的 API 方法",
    refreshDocsBeforeOpen: "请先打开文档，Demo 会根据当前文档类型读取对应 Markdown。",
    readEventsFrom: "正在从 {path} 读取事件表...",
    eventList: "事件列表",
    eventDocsLoadFailed: "读取事件文档失败",
    noSuchComponent: "当前文档不存在组件 app.{componentName}",
    appNotReady: "请先打开文档，再调用 API。",
    notCallable: "app.{componentName}.{methodName} 不是可调用方法",
    invalidJsonArgs: "参数必须是 JSON 数组，例如 [120]",
    domMissing: "api-test DOM 初始化失败，缺少节点: {nodes}",
    openPanelFailed: "文档已打开，但 Demo 面板渲染失败",
    createFileFailed: "新建文件失败",
    apiCallFailed: "API 调用失败",
    subscribeFailed: "订阅失败",
    unsubscribeFailed: "取消订阅失败",
    copyLogFailed: "复制日志失败",
    languageChanged: "界面语言已切换",
    nextOpenLanguage: "下一次打开文档将使用 {locale}。",
    apiDocTypeClosed: "未打开",
    langLabels: {
      "zh-CN": "简体中文 (zh-CN)",
      "en-US": "English (en-US)",
    },
  },
  "en-US": {
    preloadSdk: "Preload SDK",
    refreshVisibleArea: "Refresh View",
    hideLeft: "Hide Left",
    showLeft: "Show Left",
    apiPanel: "API Panel",
    hideApi: "Hide API",
    closeDocument: "Close Document",
    openDocumentTitle: "Open Document",
    chooseFile: "Choose or Drop File",
    fileNotSelected: "No file selected",
    openSelectedFile: "Open Selected File",
    createDocumentTitle: "Create Document",
    documentType: "Document Type",
    createDocumentType: "Create document type",
    createFile: "Create File",
    openOptions: "Open Options",
    documentLanguage: "Language",
    readOnlyMode: "Read-only Mode",
    showTopbar: "Show Top Toolbar",
    showBottombar: "Show Bottom Bar",
    optionsHint: "These options take effect the next time a document is opened.",
    readyTitle: "Ready",
    readyDetail: "Open this page through a local HTTP server, then choose a local document or create a file.",
    openStateClosed: "Not Opened",
    openStateOpen: "Opened",
    apiTestPanel: "API Test Panel",
    apiCalls: "API Calls",
    eventSubscriptions: "Event Subscriptions",
    refreshDocs: "Refresh Docs",
    apiDocHint: 'API methods are read live from <code>{path}</code> for the current document type. Click "Refresh Docs" after SDK docs change.',
    component: "Component",
    methodName: "Method",
    selectApiAfterOpen: "Open a document, then select an API.",
    jsonArgs: "JSON Args Array",
    jsonArgsPlaceholder: 'For example [120] or [{"r":68,"g":114,"b":196,"a":1}]',
    runSelectedApi: "Run Selected API",
    eventsHint: "Component and event dropdowns are read from the current document type's Markdown event table. Click Refresh Docs after new events are added.",
    eventName: "Event Name",
    subscribe: "Subscribe",
    unsubscribe: "Unsubscribe",
    logTitle: "Call Results / Event Log",
    copy: "Copy",
    clear: "Clear",
    logHint: "API results, errors, and subscribed events are recorded here.",
    none: "None",
    unknown: "unknown",
    readLoading: "Loading",
    noMethod: "No methods",
    noEvent: "No events",
    noComponent: "No components",
    noComponentEvent: "No component events",
    openDocumentFirst: "Open a document first",
    readFailed: "Load failed",
    loadingSdkTitle: "Loading SDK",
    loadingSdkDetail: "Make sure this page is not opened with file:// and the lib directory is complete.",
    sdkLoadedTitle: "SDK Loaded",
    sdkLoadedDetail: "Choose a document to open, or create a new file.",
    opening: "Opening...",
    openingDocument: "Opening document",
    creatingDocument: "Creating document",
    documentLoaded: "Document loaded",
    documentCreated: "Document created",
    documentClosed: "Document closed",
    chooseAnotherFile: "You can choose another file.",
    fileSelected: "File selected",
    unsupportedFile: "Unsupported file type",
    preloadFailed: "Preload failed",
    apiLoadPrompt: "After opening a document, the API method table for the current type will be read from {path}.",
    readingApiDocs: "Reading API docs from {path}...",
    apiDocsLoadFailed: "Failed to read API docs. Make sure {path} is deployed with the SDK and access this page through HTTP.",
    noMethodsParsed: "No method table was parsed from {path}.",
    noCallableMethod: "No callable methods are documented for the current component.",
    chooseComponentAndMethod: "Choose a component and method.",
    docNoDescription: "No description in docs",
    returnLabel: "Returns",
    paramsLabel: "Parameters",
    sourceLabel: "Source",
    noArgs: "No parameters",
    chooseApiMethod: "Choose an API method to call",
    refreshDocsBeforeOpen: "Open a document first. The demo reads Markdown by the current document type.",
    readEventsFrom: "Reading event table from {path}...",
    eventList: "Event list",
    eventDocsLoadFailed: "Failed to read event docs",
    noSuchComponent: "The current document does not have component app.{componentName}",
    appNotReady: "Open a document before calling APIs.",
    notCallable: "app.{componentName}.{methodName} is not callable",
    invalidJsonArgs: "Arguments must be a JSON array, for example [120]",
    domMissing: "api-test DOM initialization failed; missing nodes: {nodes}",
    openPanelFailed: "Document opened, but the demo panel failed to render",
    createFileFailed: "Failed to create file",
    apiCallFailed: "API call failed",
    subscribeFailed: "Subscribe failed",
    unsubscribeFailed: "Unsubscribe failed",
    copyLogFailed: "Failed to copy log",
    languageChanged: "Interface language changed",
    nextOpenLanguage: "The next document open will use {locale}.",
    apiDocTypeClosed: "Not Opened",
    langLabels: {
      "zh-CN": "Simplified Chinese (zh-CN)",
      "en-US": "English (en-US)",
    },
  },
};

const DOC_TYPE = {
  1: { key: "docx", label: "DOCX / Word" },
  2: { key: "xlsx", label: "XLSX / Excel" },
  3: { key: "pptx", label: "PPTX / PowerPoint" },
};

const COMPONENTS_BY_TYPE = {
  common: ["Document", "Selection", "Finder", "UndoRedo"],
  docx: ["Document", "Paragraph", "Table", "Selection", "Finder", "UndoRedo"],
  xlsx: ["Document", "Workbook", "Worksheet", "Selection", "Finder", "Cursor", "UndoRedo"],
  pptx: ["Document", "Paragraph", "TextBox", "Player", "Selection", "Cursor", "UndoRedo"],
};

const DEFAULT_EVENTS = {
  docx: [
    ["Document", "DOCX_PAGE_POSITION_CHANGE"],
    ["Document", "DOCX_END_LOADING"],
    ["Document", "DOCUMENT_EXPORT_READY"],
    ["Document", "DOCX_ZOOM_CHANGE"],
    ["Document", "DOCUMENT_EDITING_ENABLED"],
    ["Document", "DOCUMENT_EDITING_DISABLED"],
    ["Selection", "SELECTION_CHANGE"],
    ["Finder", "DOCX_OPEN_FIND_UI"],
    ["UndoRedo", "UNDO_REDO_STATE_CHANGE"],
  ],
  xlsx: [
    ["Document", "XLSX_WORKSHEET_LOADED"],
    ["Document", "XLSX_END_LOADING"],
    ["Document", "DOCUMENT_EXPORT_READY"],
    ["Document", "XLSX_EDITING_STATUS_CHANGE"],
    ["Document", "XLSX_ZOOM_CHANGE"],
    ["Document", "XLSX_WORKSHEET_CHANGE"],
    ["Document", "DOCUMENT_EDITING_ENABLED"],
    ["Document", "DOCUMENT_EDITING_DISABLED"],
    ["Selection", "SELECTION_CHANGE"],
    ["Finder", "XLSX_OPEN_FIND_UI"],
    ["UndoRedo", "UNDO_REDO_STATE_CHANGE"],
  ],
  pptx: [
    ["Document", "DOCUMENT_READY"],
    ["Document", "DOCUMENT_EXPORT_READY"],
    ["Document", "PPTX_SLIDES_CHANGED"],
    ["Document", "PPTX_ZOOM_CHANGE"],
    ["Document", "PPTX_FIRST_PAGE_LOADED"],
    ["Document", "PPTX_START_LOADING"],
    ["Document", "DOCUMENT_EDITING_ENABLED"],
    ["Document", "DOCUMENT_EDITING_DISABLED"],
    ["Selection", "SELECTION_CHANGE"],
    ["UndoRedo", "UNDO_REDO_STATE_CHANGE"],
  ],
};

const els = {};
const quickActions = new Map();
const actionParamControls = new Map();
const activeListeners = new Map();
let selectedFile = null;
let OfficeUI = null;
let controller = null;
let apiPanelVisible = false;
let leftPanelCollapsed = false;
let currentRightTab = "api";
let apiGroupsCache = [];
let current = {
  app: null,
  widget: null,
  docType: null,
  docTypeName: null,
  fileName: "",
};
const licenseConfig = {
  licenseSN: "m+YjqEP/ztlHAVipmLZIRPmdJVN2V+mmPSrd3dyKgODAntbbyJm0ug==",
  licenseKey: "6W5ScrFpOj3YW9TfEwKVXUPiq40mOIxqO7RHOVEWLFaeGioeFJHySGnpwYqkUyxcPt1y3zf2STGgbPpoPTAzMZfGSjrhG2EZLSgSZCD9ngN2ucz96J5Zu87kB3JmdBHeI3gNFyKye8sSyEHtGj1Dert6d8w42/OsZUalTIqFxOVXN+dyyvkbGejExSp9BJGORDA5SPtrltN4zAqL74CR5T8w3QDHuzaZYIHzQce80CchtOXXjYRwvtZb/hyMRWId3FTNnQfKBQjIourM/0YaF4PD90Dzt09qOvx3HGMQV9rQ0sBEeHfgS/ay0g0RJXhghhvAHdgRbGZA6DagZ7zrSJnsnYmZe7Jz2Z1eHJTHnlMPAksj7dsjo+uosBz9uNtqcZqfDp78p7wBxecez/8rsz5h2BLnA3QgJY6FuSatRnv+rgbxvYFhHy7FLIlALttul9nolZk11vqXOoOT7BbfEuZloOf3LuWp+AxY1kaBRDYg6RMjTdVL4mNEqFCrw25gugtSVih1Je21OjSdYN4lTdr7hWuFkbT1/hECdSQgzEMZZXSMWSnKOkx+BgQH6Hbo84dXK2EhkrPLtPFGv0ZoSy04fNUgMR6JGadIVfyOckdPHAY2sk7Umz6gyiI2B7aUkWLQs+BGYr0mXwccn5kiJbk/RQwTJsl5CjR5BwBx3JMOuX0139WBGYxwqfrhgDZV6AQEi2cn0jG97yw/XVyv8R+p7mGkNZpy7KDA28xh2uzA0nq1TaGtqxeQ0MU9679Om68SUK+WabeiPE7DdxayHrgSOR3AOLCBi5ut1hJ+u/XdFzffx/fSu5lTjwkkxp3d2IJBjc6tBrNQKqiuT3k4hSjUckqmUL64sYlRu5EA+N9f7fnLAKvu2P0fdJXeiPc8HMU4nEBGvvzpzlq8XYW96qPnIAoV9XvCN9+kFMMWZ6s/T03tvUSxG0ZqIaX4o8Mh/QxKOeAepplt3hTJcXXbpf8A2zC9+JV1Kyl2ifheJ3i+AF9N2F6mQI8XzutMdvyduAfyFpFJc0DMOXGAm8BlLS8tAzleiDAixD1MhK5g3BmxCRhwZKEn9mar+a0XA5cz1L9Zs9Q6hxbovcazXS1D7G6M+EhAPoax8z9EBgAbEuYfCrhqJgfSpSXR8iEU5gZPJQplNNqUgRaREQwxmadkjARTXPtdyvFdxVI0tB5P6ByXzmqCY2p6w559SfXtQ6yXmXMLGhoiTixEhfL6A/jgZaGCR3buKjSqtQNThq5if9Akd+3xwX4NW6OS5iJKwYEw8N8TlQ==",
};
let currentLocale = normalizeDemoLocale(localStorage.getItem(LOCALE_STORAGE_KEY) || document.documentElement.lang || "zh-CN");

function $(selector) {
  return document.querySelector(selector);
}

function normalizeDemoLocale(locale) {
  return SUPPORTED_DEMO_LOCALES.includes(locale) ? locale : "zh-CN";
}

function t(key, values = {}) {
  const template = I18N[currentLocale]?.[key] ?? I18N["zh-CN"][key] ?? key;
  if (typeof template !== "string") {
    return template;
  }
  return template.replace(/\{([A-Za-z0-9_]+)\}/g, (_, name) => String(values[name] ?? ""));
}

function apiDocsRoot() {
  return `docs/Markdown/${currentLocale}/api-reference`;
}

function nowTime() {
  return new Date().toLocaleTimeString(currentLocale, { hour12: false });
}

function safeStringify(value) {
  if (value === undefined) {
    return "undefined";
  }
  if (typeof value === "string") {
    return value;
  }
  if (value instanceof Error) {
    return `${value.name}: ${value.message}\n${value.stack ?? ""}`.trim();
  }

  const seen = new WeakSet();
  try {
    return JSON.stringify(
      value,
      (key, item) => {
        if (typeof item === "function") {
          return `[Function ${item.name || "anonymous"}]`;
        }
        if (typeof item === "object" && item !== null) {
          if (seen.has(item)) {
            return "[Circular]";
          }
          seen.add(item);
        }
        return item;
      },
      2,
    );
  } catch (error) {
    return String(value);
  }
}

function log(level, title, detail) {
  if (!els.eventLog) {
    return;
  }

  const item = document.createElement("div");
  item.className = `fed-log-item fed-${level}`;
  const body = detail === undefined ? "" : safeStringify(detail);
  item.innerHTML = `
    <div class="fed-log-head">
      <span>${escapeHtml(level)}: ${escapeHtml(title)}</span>
      <span class="fed-log-time">${escapeHtml(nowTime())}</span>
    </div>
    <div>${escapeHtml(body)}</div>
  `;
  els.eventLog.prepend(item);

  while (els.eventLog.children.length > MAX_LOG_ITEMS) {
    els.eventLog.lastElementChild?.remove();
  }
}

function escapeHtml(value) {
  return String(value ?? "")
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&#039;");
}

function setStatus(title, detail = "") {
  els.statusTitle.textContent = title;
  els.statusDetail.textContent = detail;
}

let lastResultSnapshot = null;

function setLastResult(value) {
  lastResultSnapshot = value;
}

function recordCallResult(title, value, level = "result") {
  setLastResult(value);
  log(level, title, value);
}

function docInfoFromType(docType) {
  return DOC_TYPE[Number(docType)] ?? { key: "unknown", label: `unknown(${docType ?? "none"})` };
}

function updateDocMeta() {
  const info = docInfoFromType(current.docType);
  const typeText = current.docType ? `${current.docType} / ${current.docTypeName || info.key}` : `none / ${t("none").toLowerCase()}`;
  els.docMeta.textContent = `docType: ${typeText}`;
  els.apiDocType.textContent = current.docType ? info.label : t("apiDocTypeClosed");
  els.apiDocType.className = current.docType ? "fed-badge fed-ok" : "fed-badge fed-soft";
  els.openState.textContent = current.docType ? t("openStateOpen") : t("openStateClosed");
  els.openState.className = current.docType ? "fed-badge fed-ok" : "fed-badge";
}

function setApiPanelVisible(visible) {
  apiPanelVisible = Boolean(visible);
  els.rightPanel.classList.toggle("fed-right-hidden", !apiPanelVisible);
  els.rightPanel.style.display = apiPanelVisible ? "" : "none";
  els.rightPanel.setAttribute("aria-hidden", apiPanelVisible ? "false" : "true");
  els.workspace.classList.toggle("fed-api-open", apiPanelVisible);
  els.toggleApi.setAttribute("aria-expanded", apiPanelVisible ? "true" : "false");
  els.toggleApi.textContent = apiPanelVisible ? t("hideApi") : t("apiPanel");
  els.toggleApi.classList.toggle("fed-active-api", apiPanelVisible);

  if (apiPanelVisible) {
    setRightTab(currentRightTab || "api");
  }
  scheduleViewerRefresh();
}

function setLeftPanelCollapsed(collapsed) {
  leftPanelCollapsed = Boolean(collapsed);
  els.workspace.classList.toggle("fed-left-collapsed", leftPanelCollapsed);
  els.leftPanel.setAttribute("aria-hidden", leftPanelCollapsed ? "true" : "false");
  els.toggleLeft.setAttribute("aria-expanded", leftPanelCollapsed ? "false" : "true");
  els.toggleLeft.textContent = leftPanelCollapsed ? t("showLeft") : t("hideLeft");
  els.toggleLeft.classList.toggle("fed-active-side", leftPanelCollapsed);
  scheduleViewerRefresh();
}

function setRightTab(tabName) {
  currentRightTab = tabName === "events" ? "events" : "api";
  const isApi = currentRightTab === "api";

  els.tabApi.classList.toggle("fed-active", isApi);
  els.tabEvents.classList.toggle("fed-active", !isApi);
  els.tabApi.setAttribute("aria-selected", isApi ? "true" : "false");
  els.tabEvents.setAttribute("aria-selected", !isApi ? "true" : "false");

  els.panelApi.hidden = !isApi;
  els.panelEvents.hidden = isApi;
  els.panelApi.classList.toggle("fed-active", isApi);
  els.panelEvents.classList.toggle("fed-active", !isApi);
}

function setupLanguageOptions() {
  const selected = els.lang.value || currentLocale;
  els.lang.innerHTML = "";
  for (const item of SUPPORTED_LANGS) {
    const option = document.createElement("option");
    option.value = item.value;
    option.textContent = t("langLabels")?.[item.value] ?? item.label;
    els.lang.appendChild(option);
  }
  els.lang.value = selected;
}


async function loadSdk({ preload = false } = {}) {
  if (OfficeUI) {
    return OfficeUI;
  }

  setStatus(t("loadingSdkTitle"), t("loadingSdkDetail"));

  if (preload) {
    await import(SDK_PRELOAD_ENTRY);
    log("system", "preload.js imported");
  }

  const module = await import(SDK_UI_ENTRY);
  OfficeUI = module.default;
  controller = createOfficeController({ OfficeUI });
  log("success", "UI.js imported", {
    hasOpenFile: typeof OfficeUI?.openfile === "function",
    hasCreateFile: typeof OfficeUI?.createfile === "function",
  });
  setStatus(t("sdkLoadedTitle"), t("sdkLoadedDetail"));
  return OfficeUI;
}

function getOpenOptions() {
  return {
    userData: {
      userId: els.userId.value.trim() || "fulleval-user",
      clientId: els.clientId.value.trim() || "fulleval-browser",
      nickName: els.nickName.value.trim() || "Full Eval User",
      opts: {
        color: els.userColor.value || "#4472c4",
      },
    },
    uiOptions: {
      showTopBar: els.topbar.checked,
      showBottomBar: els.bottombar.checked,
    },
    mode: {
      readOnly: els.readonly.checked,
      lang: els.lang.value || "zh-CN",
    },
  };
}


function scheduleViewerRefresh() {
  const run = () => {
    try {
      controller?.refresh?.();
      current.app?.Document?.updateVisibleArea?.();
    } catch (error) {
      // 可视区刷新是布局辅助动作，避免因 SDK 内部短暂未就绪打断主流程。
    }
  };
  requestAnimationFrame(run);
  requestAnimationFrame(() => requestAnimationFrame(run));
  window.setTimeout(run, 120);
  window.setTimeout(run, 360);
  window.setTimeout(run, 900);
  window.setTimeout(run, 1500);
}

function buildFileDataFromInput() {
  if (!selectedFile) {
    throw new Error("MISSING_FILE: please choose an Office document first");
  }
  if (!isSupportedOfficeFile(selectedFile.name)) {
    throw new Error(`UNSUPPORTED_FILE: ${selectedFile.name}`);
  }
  return {
    docId: `eval-file-${Date.now()}-${selectedFile.name.replace(/[^a-z0-9._-]+/gi, "-")}`,
    fileName: selectedFile.name,
    file: selectedFile,
  };
}

async function openDocument(fileData) {
  try {
    await loadSdk();
    await detachAllListeners();
    setLastResult(t("opening"));
    els.viewer.classList.remove("fed-has-doc");
    setStatus(t("openingDocument"), fileData.fileName);
    log("system", "openfile called", {
      fileName: fileData.fileName,
      docId: fileData.docId,
      option: getOpenOptions(),
    });
    configureOfficeLicense(OfficeUI, licenseConfig);
    const result = await controller.open(els.viewer, fileData, getOpenOptions());
    finishDocumentOpen(result, fileData.fileName, t("documentLoaded"));
  } catch (error) {
    if (isOpenCancelledError(error)) {
      return;
    }
    console.error("[api-test] open failed", error);
    const info = classifyError(error);
    log("error", info.title, info.detail);
    els.viewer.classList.remove("fed-has-doc");
    setStatus(info.title, info.detail);
    setLastResult(error);
  }
}

function finishDocumentOpen(result, fileName, statusTitle) {
  current = {
    app: result.rawApp,
    widget: result.widget,
    docType: result.docType ?? result.widget?.docType ?? null,
    docTypeName: result.widget?.docTypeName ?? null,
    fileName,
  };

  els.viewer.classList.add("fed-has-doc");
  setStatus(statusTitle, fileName);
  setLastResult({ docType: current.docType, docTypeName: current.docTypeName, fileName: current.fileName });

  try {
    updateDocMeta();
    renderApiPanel({ force: true });
    void renderEventPanel({ force: true, autoSubscribe: true });
  } catch (uiError) {
    log("error", t("openPanelFailed"), uiError);
    setLastResult(uiError);
  }

  scheduleViewerRefresh();
}

async function createDocument(docType) {
  const item = getCreateDocType(docType);
  if (!item) {
    const error = new Error(`INVALID_CREATE_DOC_TYPE: ${docType}`);
    const info = classifyError(error);
    setStatus(info.title, info.detail);
    setLastResult(error);
    return;
  }

  try {
    await loadSdk();
    await detachAllListeners();
    setLastResult(t("opening"));
    els.viewer.classList.remove("fed-has-doc");
    setStatus(t("creatingDocument"), item.label);
    log("system", "createfile called", {
      docType: item.value,
      option: getOpenOptions(),
    });

    configureOfficeLicense(OfficeUI, licenseConfig);
    const result = await controller.create(els.viewer, item.value, getOpenOptions());
    selectedFile = null;
    els.fileInput.value = "";
    els.fileName.textContent = item.fileName;
    finishDocumentOpen(result, item.fileName, t("documentCreated"));
  } catch (error) {
    if (isOpenCancelledError(error)) {
      return;
    }
    console.error("[api-test] create failed", error);
    const info = classifyError(error);
    log("error", t("createFileFailed"), info.detail);
    els.viewer.classList.remove("fed-has-doc");
    setStatus(info.title, info.detail);
    setLastResult(error);
  }
}

async function closeDocument() {
  await detachAllListeners();
  await controller?.close();
  current = { app: null, widget: null, docType: null, docTypeName: null, fileName: "" };
  if (!selectedFile) {
    els.fileName.textContent = t("fileNotSelected");
  }
  els.viewer.classList.remove("fed-has-doc");
  updateDocMeta();
  renderApiPanel();
  populateComponentSelectors();
  setStatus(t("documentClosed"), t("chooseAnotherFile"));
  setLastResult(t("none"));
  log("system", "document closed");
}

function getDocKey() {
  return docInfoFromType(current.docType).key;
}

function getCurrentComponents() {
  const key = getDocKey();
  return COMPONENTS_BY_TYPE[key] ?? COMPONENTS_BY_TYPE.common;
}


const API_DOC_PATH_BY_LOCALE = {
  "zh-CN": {
    docx: "../../docs/Markdown/zh-CN/api-reference/docx.md",
    xlsx: "../../docs/Markdown/zh-CN/api-reference/xlsx.md",
    pptx: "../../docs/Markdown/zh-CN/api-reference/pptx.md",
  },
  "en-US": {
    docx: "../../docs/Markdown/en-US/api-reference/docx.md",
    xlsx: "../../docs/Markdown/en-US/api-reference/xlsx.md",
    pptx: "../../docs/Markdown/en-US/api-reference/pptx.md",
  },
};

function getApiDocPath(docKey) {
  return API_DOC_PATH_BY_LOCALE[currentLocale]?.[docKey] ?? API_DOC_PATH_BY_LOCALE["zh-CN"][docKey];
}

const docReferenceCache = new Map();
let apiRenderToken = 0;
let eventRenderToken = 0;
let currentDocApiItems = [];
let currentDocApiByKey = new Map();
let currentDocEventItems = [];
let currentDocEventsByComponent = new Map();

function stripInlineCode(value) {
  const text = String(value ?? "").trim();
  if (text.startsWith("`") && text.endsWith("`")) {
    return text.slice(1, -1).replaceAll("\\|", "|");
  }
  return text.replaceAll("\\|", "|").replace(/`([^`]+)`/g, "$1").trim();
}

function stripMarkdown(value) {
  return String(value ?? "")
    .replaceAll("\\|", "|")
    .replace(/`([^`]+)`/g, "$1")
    .replace(/\[(.+?)\]\(.+?\)/g, "$1")
    .trim();
}

function splitMarkdownRow(line) {
  const raw = String(line ?? "").trim();
  if (!raw.startsWith("|")) {
    return [];
  }

  const body = raw.replace(/^\|/, "").replace(/\|\s*$/, "");
  const cells = [];
  let cell = "";
  let inCode = false;
  let escaped = false;

  for (const char of body) {
    if (char === "`" && !escaped) {
      inCode = !inCode;
      cell += char;
    } else if (char === "|" && !inCode && !escaped) {
      cells.push(cell.trim());
      cell = "";
    } else {
      cell += char;
    }
    escaped = char === "\\" && !escaped;
    if (char !== "\\") {
      escaped = false;
    }
  }
  cells.push(cell.trim());
  return cells;
}

function isMethodTableHeader(line) {
  const cells = splitMarkdownRow(line).map(stripMarkdown);
  return (cells[0] === "方法" || cells[0] === "Method") && (cells[1] === "签名" || cells[1] === "Signature");
}

function isMarkdownSeparator(line) {
  return /^\|\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?$/.test(String(line ?? "").trim());
}

function isApiComponentName(value) {
  return /^[A-Z][A-Za-z0-9_]*$/.test(String(value ?? "").trim());
}

function normalizeJsArgsToJsonArray(rawArgs) {
  const raw = String(rawArgs ?? "").trim();
  if (!raw) {
    return [];
  }
  const jsonish = `[${raw}]`
    .replace(/([{,]\s*)([A-Za-z_$][\w$]*)\s*:/g, '$1"$2":')
    .replace(/'/g, '"')
    .replace(/,\s*([}\]])/g, "$1");
  return JSON.parse(jsonish);
}

function extractExampleArgs(markdown) {
  const samples = new Map();
  const callPattern = /app\.([A-Z][A-Za-z0-9_]*)\.([A-Za-z_$][\w$]*)\(([^;]*)\);/g;
  let match;
  while ((match = callPattern.exec(markdown)) !== null) {
    const [, component, method, rawArgs] = match;
    const key = `${component}.${method}`;
    if (samples.has(key)) {
      continue;
    }
    try {
      samples.set(key, normalizeJsArgsToJsonArray(rawArgs));
    } catch {
      // 文档示例里如包含变量或表达式，参数仍可由签名推断并允许手动编辑。
    }
  }
  return samples;
}

function parseProductApiMarkdown(markdown, docKey, docPath) {
  const lines = String(markdown ?? "").split(/\r?\n/);
  const sampleArgs = extractExampleArgs(markdown);
  const items = [];
  let componentName = null;
  let inMethodTable = false;

  for (const line of lines) {
    const heading = line.match(/^##\s+(.+?)\s*$/);
    if (heading) {
      const headingText = heading[1].trim();
      componentName = isApiComponentName(headingText) ? headingText : null;
      inMethodTable = false;
      continue;
    }

    if (!componentName) {
      continue;
    }

    if (isMethodTableHeader(line)) {
      inMethodTable = true;
      continue;
    }

    if (!inMethodTable) {
      continue;
    }

    if (isMarkdownSeparator(line)) {
      continue;
    }

    if (!line.trim().startsWith("|")) {
      inMethodTable = false;
      continue;
    }

    const cells = splitMarkdownRow(line);
    if (cells.length < 4) {
      continue;
    }

    const method = stripInlineCode(cells[0]);
    const signature = stripInlineCode(cells[1]);
    const returns = stripInlineCode(cells[2]);
    const description = stripMarkdown(cells.slice(3).join(" | "));
    if (!/^[A-Za-z_$][\w$]*$/.test(method)) {
      continue;
    }

    const key = `${componentName}.${method}`;
    items.push({
      key,
      docKey,
      component: componentName,
      method,
      signature,
      returns,
      description,
      docPath,
      sampleArgs: sampleArgs.has(key) ? sampleArgs.get(key) : undefined,
    });
  }

  return items;
}


function isEventTableHeader(line) {
  const cells = splitMarkdownRow(line).map(stripMarkdown);
  return (cells[0] === "事件名" || cells[0] === "Event Name") && cells[1]?.toLowerCase() === "payload";
}

function parseProductEventMarkdown(markdown, docKey, docPath) {
  const lines = String(markdown ?? "").split(/\r?\n/);
  const items = [];
  const seen = new Set();
  let headingComponentName = null;
  let eventComponentName = null;
  let waitingForEventHeader = false;
  let inEventTable = false;

  for (const line of lines) {
    const heading = line.match(/^##\s+(.+?)\s*$/);
    if (heading) {
      const headingText = heading[1].trim();
      headingComponentName = isApiComponentName(headingText) ? headingText : null;
      eventComponentName = null;
      waitingForEventHeader = false;
      inEventTable = false;
      continue;
    }

    const supportMatch = line.match(/^\s*([A-Z][A-Za-z0-9_]*)\s+(?:支持的组件事件|component events)[:：]\s*$/i);
    if (supportMatch) {
      eventComponentName = supportMatch[1];
      waitingForEventHeader = true;
      inEventTable = false;
      continue;
    }

    if (/(?:支持的组件事件|component events)[:：]\s*$/i.test(line) && headingComponentName) {
      eventComponentName = headingComponentName;
      waitingForEventHeader = true;
      inEventTable = false;
      continue;
    }

    if (!eventComponentName) {
      continue;
    }

    if (waitingForEventHeader && isEventTableHeader(line)) {
      waitingForEventHeader = false;
      inEventTable = true;
      continue;
    }

    if (!inEventTable) {
      continue;
    }

    if (isMarkdownSeparator(line)) {
      continue;
    }

    if (!line.trim().startsWith("|")) {
      eventComponentName = null;
      waitingForEventHeader = false;
      inEventTable = false;
      continue;
    }

    const cells = splitMarkdownRow(line);
    if (cells.length < 2) {
      continue;
    }

    const eventName = stripInlineCode(cells[0]);
    const payload = stripInlineCode(cells[1]);
    const description = stripMarkdown(cells.slice(2).join(" | "));
    if (!/^[A-Z0-9_]+$/.test(eventName)) {
      continue;
    }

    const key = `${eventComponentName}.${eventName}`;
    if (seen.has(key)) {
      continue;
    }
    seen.add(key);
    items.push({ key, docKey, component: eventComponentName, eventName, payload, description, docPath });
  }

  return items;
}

function parseProductReferenceMarkdown(markdown, docKey, docPath) {
  return {
    apiItems: parseProductApiMarkdown(markdown, docKey, docPath),
    eventItems: parseProductEventMarkdown(markdown, docKey, docPath),
    loadedAt: new Date().toISOString(),
  };
}

async function loadDocReferenceFromDocs(docKey, { force = false } = {}) {
  const path = getApiDocPath(docKey);
  if (!path) {
    return { apiItems: [], eventItems: [], loadedAt: new Date().toISOString() };
  }

  const cacheKey = `${currentLocale}:${docKey}`;
  if (force) {
    docReferenceCache.delete(cacheKey);
  }

  if (docReferenceCache.has(cacheKey)) {
    return docReferenceCache.get(cacheKey);
  }

  const response = await fetch(`${path}?_fed_docs=${Date.now()}`, { cache: "no-store" });
  if (!response.ok) {
    throw new Error(`Failed to read API/event docs: ${path} (${response.status})`);
  }
  const markdown = await response.text();
  const reference = parseProductReferenceMarkdown(markdown, docKey, path);
  docReferenceCache.set(cacheKey, reference);
  return reference;
}

async function loadApiItemsFromDocs(docKey, options) {
  const reference = await loadDocReferenceFromDocs(docKey, options);
  return reference.apiItems;
}

async function loadEventItemsFromDocs(docKey, options) {
  const reference = await loadDocReferenceFromDocs(docKey, options);
  return reference.eventItems;
}


function getComponentOrder(apiItems) {
  const seen = new Set();
  const components = [];
  for (const item of apiItems) {
    if (!seen.has(item.component)) {
      seen.add(item.component);
      components.push(item.component);
    }
  }
  return components;
}

function extractParameterText(signature) {
  const text = String(signature ?? "").trim();
  const start = text.indexOf("(");
  if (start < 0) {
    return "";
  }

  let depth = 0;
  let inString = null;
  let escaped = false;
  for (let i = start; i < text.length; i += 1) {
    const char = text[i];
    if (inString) {
      if (char === inString && !escaped) {
        inString = null;
      }
      escaped = char === "\\" && !escaped;
      if (char !== "\\") {
        escaped = false;
      }
      continue;
    }
    if (char === '"' || char === "'") {
      inString = char;
      continue;
    }
    if (char === "(") {
      depth += 1;
    } else if (char === ")") {
      depth -= 1;
      if (depth === 0) {
        return text.slice(start + 1, i).trim();
      }
    }
  }
  return "";
}

function splitTopLevel(value, delimiter = ",") {
  const parts = [];
  let part = "";
  let depth = 0;
  let inString = null;
  let escaped = false;

  for (const char of String(value ?? "")) {
    if (inString) {
      part += char;
      if (char === inString && !escaped) {
        inString = null;
      }
      escaped = char === "\\" && !escaped;
      if (char !== "\\") {
        escaped = false;
      }
      continue;
    }

    if (char === '"' || char === "'") {
      inString = char;
      part += char;
      continue;
    }

    if ("([{<".includes(char)) {
      depth += 1;
    } else if (")]}>".includes(char)) {
      depth = Math.max(0, depth - 1);
    }

    if (char === delimiter && depth === 0) {
      parts.push(part.trim());
      part = "";
    } else {
      part += char;
    }
  }

  if (part.trim()) {
    parts.push(part.trim());
  }
  return parts;
}

function parseSignatureParameters(signature) {
  const paramText = extractParameterText(signature);
  if (!paramText) {
    return [];
  }
  return splitTopLevel(paramText).map((raw, index) => {
    const match = raw.match(/^([A-Za-z_$][\w$]*)(\?)?\s*:\s*(.+)$/);
    if (!match) {
      return { index, raw, name: `arg${index + 1}`, optional: false, type: raw };
    }
    return {
      index,
      raw,
      name: match[1],
      optional: Boolean(match[2]),
      type: match[3].trim(),
    };
  });
}

function firstStringLiteral(typeText) {
  const match = String(typeText ?? "").match(/"([^"]+)"/);
  return match ? match[1] : null;
}

function firstNumberLiteral(typeText) {
  const match = String(typeText ?? "").match(/(?:^|\s)(-?\d+(?:\.\d+)?)(?:\s*(?:\\\|\||$))/);
  return match ? Number(match[1]) : null;
}

function inferRequiredArgValue(api, param) {
  const methodKey = `${api.component}.${api.method}`;
  const lowerMethod = api.method.toLowerCase();
  const lowerName = param.name.toLowerCase();
  const type = String(param.type ?? "");

  const exactDefaults = {
    "Document.setZoom": [120],
    "Document.setPageView": ["wide"],
    "Document.goToPage": [1],
    "Document.goToSlide": [1],
    "Document.setPageOrientation": ["Portrait"],
    "Document.setPageSize": [21, 29.7],
    "Paragraph.setFontName": ["Microsoft YaHei"],
    "Paragraph.setFontSize": [16],
    "Paragraph.setFontColor": [{ r: 68, g: 114, b: 196, a: 1 }],
    "Paragraph.setAlignment": ["Centered"],
    "Paragraph.setLineSpacing": [1.5],
    "Selection.setFillColor": [{ r: 255, g: 242, b: 204, a: 1 }],
    "Selection.setFontColor": [{ r: 68, g: 114, b: 196, a: 1 }],
    "Selection.setBorderColor": [{ r: 68, g: 114, b: 196, a: 1 }],
    "Workbook.setWorksheetName": ["Demo"],
    "Finder.search": [currentLocale === "zh-CN" ? "测试" : "test"],
    "TextBox.setVerticalAlignment": [1],
    "Player.play": [2],
    "Player.enterFullScreen": [1],
  };

  if (Object.hasOwn(exactDefaults, methodKey) && param.index < exactDefaults[methodKey].length) {
    return exactDefaults[methodKey][param.index];
  }

  const stringLiteral = firstStringLiteral(type);
  if (stringLiteral !== null) {
    return stringLiteral;
  }

  const numberLiteral = firstNumberLiteral(type);
  if (numberLiteral !== null) {
    return numberLiteral;
  }

  if (/boolean/.test(type)) {
    return true;
  }

  if (/number/.test(type)) {
    if (lowerMethod.includes("zoom")) return 120;
    if (lowerMethod.includes("fontsize")) return 16;
    if (lowerMethod.includes("slide") || lowerMethod.includes("page")) return 1;
    if (lowerName.includes("count")) return 1;
    if (lowerName.includes("index")) return 0;
    if (lowerName.includes("height")) return 29.7;
    if (lowerName.includes("width")) return 21;
    return 1;
  }

  if (/string\[\]/.test(type) || /Array/.test(type)) {
    return [];
  }

  if (/string/.test(type)) {
    if (lowerMethod.includes("fontname")) return "Microsoft YaHei";
    if (lowerMethod.includes("worksheetname")) return "Demo";
    if (lowerMethod.includes("search") || lowerMethod.includes("find")) return currentLocale === "zh-CN" ? "测试" : "test";
    if (lowerName.includes("format")) return "General";
    return currentLocale === "zh-CN" ? "测试" : "test";
  }

  if (/Color/.test(type) || lowerMethod.includes("color")) {
    return { r: 68, g: 114, b: 196, a: 1 };
  }

  if (/unknown|object|Record|Options/.test(type)) {
    return {};
  }

  return null;
}

function buildDefaultArgsForApi(api) {
  if (Array.isArray(api.sampleArgs)) {
    return clonePlain(api.sampleArgs);
  }

  const params = parseSignatureParameters(api.signature);
  const args = [];
  for (const param of params) {
    if (param.optional) {
      continue;
    }
    args.push(inferRequiredArgValue(api, param));
  }
  return args;
}

function describeApiSource() {
  const docKey = getDocKey();
  return getApiDocPath(docKey) ?? t("apiDocTypeClosed");
}


function getEventComponentOrder(eventItems) {
  const seen = new Set();
  const components = [];
  for (const item of eventItems) {
    if (!seen.has(item.component)) {
      seen.add(item.component);
      components.push(item.component);
    }
  }
  return components;
}

function setEventPanelLoading(message) {
  currentDocEventItems = [];
  currentDocEventsByComponent = new Map();
  els.eventComponent.innerHTML = `<option value="">${escapeHtml(t("readLoading"))}</option>`;
  els.eventName.innerHTML = `<option value="">${escapeHtml(t("noEvent"))}</option>`;
  els.eventComponent.disabled = true;
  els.eventName.disabled = true;
  els.subscribe.disabled = true;
  els.unsubscribe.disabled = true;
  if (message) {
    log("system", t("eventList"), message);
  }
}

function renderEventPanel({ force = false, autoSubscribe = false } = {}) {
  const token = ++eventRenderToken;
  currentDocEventItems = [];
  currentDocEventsByComponent = new Map();

  if (!current.docType) {
    els.eventComponent.innerHTML = `<option value="">${escapeHtml(t("openDocumentFirst"))}</option>`;
    els.eventName.innerHTML = `<option value="">${escapeHtml(t("openDocumentFirst"))}</option>`;
    els.eventComponent.disabled = true;
    els.eventName.disabled = true;
    els.subscribe.disabled = true;
    els.unsubscribe.disabled = true;
    return Promise.resolve([]);
  }

  const docKey = getDocKey();
  setEventPanelLoading(t("readEventsFrom", { path: describeApiSource() }));

  return loadEventItemsFromDocs(docKey, { force })
    .then((items) => {
      if (token !== eventRenderToken) {
        return [];
      }
      currentDocEventItems = items;
      currentDocEventsByComponent = items.reduce((map, item) => {
        if (!map.has(item.component)) {
          map.set(item.component, []);
        }
        map.get(item.component).push(item);
        return map;
      }, new Map());
      renderEventComponentSelect();
      if (autoSubscribe) {
        attachDefaultEvents();
      }
      return items;
    })
    .catch((error) => {
      if (token !== eventRenderToken) {
        return [];
      }
      currentDocEventItems = [];
      currentDocEventsByComponent = new Map();
      els.eventComponent.innerHTML = `<option value="">${escapeHtml(t("readFailed"))}</option>`;
      els.eventName.innerHTML = `<option value="">${escapeHtml(t("noEvent"))}</option>`;
      els.eventComponent.disabled = true;
      els.eventName.disabled = true;
      els.subscribe.disabled = true;
      els.unsubscribe.disabled = true;
      log("error", t("eventDocsLoadFailed"), error);
      return [];
    });
}

function renderEventComponentSelect() {
  const components = getEventComponentOrder(currentDocEventItems);
  els.eventComponent.innerHTML = "";
  els.eventName.innerHTML = "";

  if (!components.length) {
    els.eventComponent.innerHTML = `<option value="">${escapeHtml(t("noComponentEvent"))}</option>`;
    els.eventName.innerHTML = `<option value="">${escapeHtml(t("noEvent"))}</option>`;
    els.eventComponent.disabled = true;
    els.eventName.disabled = true;
    els.subscribe.disabled = true;
    els.unsubscribe.disabled = true;
    return;
  }

  els.eventComponent.disabled = false;
  els.eventName.disabled = false;
  els.subscribe.disabled = false;
  els.unsubscribe.disabled = false;

  for (const component of components) {
    const count = currentDocEventsByComponent.get(component)?.length ?? 0;
    const option = document.createElement("option");
    option.value = component;
    option.textContent = `${component} (${count})`;
    els.eventComponent.appendChild(option);
  }

  renderEventNameSelect();
}

function renderEventNameSelect() {
  const componentName = els.eventComponent.value;
  const events = currentDocEventsByComponent.get(componentName) ?? [];
  els.eventName.innerHTML = "";

  if (!events.length) {
    els.eventName.innerHTML = `<option value="">${escapeHtml(t("noEvent"))}</option>`;
    els.eventName.disabled = true;
    els.subscribe.disabled = true;
    els.unsubscribe.disabled = true;
    return;
  }

  els.eventName.disabled = false;
  els.subscribe.disabled = false;
  els.unsubscribe.disabled = false;

  for (const item of events) {
    const option = document.createElement("option");
    option.value = item.eventName;
    option.textContent = item.description ? `${item.eventName} — ${item.description}` : item.eventName;
    option.title = `${item.component}.${item.eventName}\nPayload: ${item.payload || t("none")}\n${item.description || ""}`.trim();
    els.eventName.appendChild(option);
  }
}

function populateComponentSelectors() {
  void renderEventPanel();
}


function ensureApp() {
  if (!current.app) {
    throw new Error(t("appNotReady"));
  }
  return current.app;
}

function getComponent(componentName) {
  const app = ensureApp();
  const component = app?.[componentName];
  if (!component) {
    throw new Error(t("noSuchComponent", { componentName }));
  }
  return component;
}

async function invokeApi(componentName, methodName, args = []) {
  const component = getComponent(componentName);
  const method = component?.[methodName];
  if (typeof method !== "function") {
    throw new Error(t("notCallable", { componentName, methodName }));
  }

  const started = performance.now();
  const value = method.apply(component, args);
  const result = value && typeof value.then === "function" ? await value : value;
  const elapsedMs = Math.round(performance.now() - started);
  const output = {
    call: `app.${componentName}.${methodName}`,
    args,
    result,
    elapsedMs,
  };
  recordCallResult(output.call, { args, result, elapsedMs }, "result");
  return result;
}


function parseJsonArray(raw) {
  const parsed = JSON.parse(raw || "[]");
  if (!Array.isArray(parsed)) {
    throw new Error(t("invalidJsonArgs"));
  }
  return parsed;
}

function clonePlain(value) {
  if (value === undefined) {
    return undefined;
  }
  try {
    return JSON.parse(JSON.stringify(value));
  } catch {
    return value;
  }
}

function setApiPanelLoading(message) {
  els.apiPanel.classList.add("fed-empty");
  els.apiComponentSelect.innerHTML = `<option value="">${escapeHtml(t("readLoading"))}</option>`;
  els.apiMethodSelect.innerHTML = `<option value="">${escapeHtml(t("noMethod"))}</option>`;
  els.apiComponentSelect.disabled = true;
  els.apiMethodSelect.disabled = true;
  els.runDocApi.disabled = true;
  els.apiMethodMeta.textContent = message;
  els.apiArgs.value = "[]";
}

function renderApiPanel({ force = false } = {}) {
  const token = ++apiRenderToken;
  currentDocApiItems = [];
  currentDocApiByKey = new Map();

  if (!current.docType) {
    setApiPanelLoading(t("apiLoadPrompt", { path: apiDocsRoot() }));
    els.apiComponentSelect.innerHTML = `<option value="">${escapeHtml(t("openDocumentFirst"))}</option>`;
    return;
  }

  const docKey = getDocKey();
  const sourcePath = describeApiSource();
  setApiPanelLoading(t("readingApiDocs", { path: sourcePath }));

  loadApiItemsFromDocs(docKey, { force })
    .then((items) => {
      if (token !== apiRenderToken) {
        return;
      }
      currentDocApiItems = items;
      currentDocApiByKey = new Map(items.map((item) => [item.key, item]));
      renderApiComponentSelect();
    })
    .catch((error) => {
      if (token !== apiRenderToken) {
        return;
      }
      currentDocApiItems = [];
      currentDocApiByKey = new Map();
      els.apiPanel.classList.add("fed-empty");
      els.apiComponentSelect.innerHTML = `<option value="">${escapeHtml(t("readFailed"))}</option>`;
      els.apiMethodSelect.innerHTML = `<option value="">${escapeHtml(t("noMethod"))}</option>`;
      els.apiComponentSelect.disabled = true;
      els.apiMethodSelect.disabled = true;
      els.runDocApi.disabled = true;
      els.apiMethodMeta.textContent = t("apiDocsLoadFailed", { path: apiDocsRoot() });
      log("error", t("apiDocsLoadFailed", { path: apiDocsRoot() }), error);
    });
}

function renderApiComponentSelect() {
  const components = getComponentOrder(currentDocApiItems);
  els.apiComponentSelect.innerHTML = "";
  els.apiMethodSelect.innerHTML = "";

  if (!components.length) {
    els.apiPanel.classList.add("fed-empty");
    els.apiComponentSelect.innerHTML = `<option value="">${escapeHtml(t("noComponent"))}</option>`;
    els.apiMethodSelect.innerHTML = `<option value="">${escapeHtml(t("noMethod"))}</option>`;
    els.apiComponentSelect.disabled = true;
    els.apiMethodSelect.disabled = true;
    els.runDocApi.disabled = true;
    els.apiMethodMeta.textContent = t("noMethodsParsed", { path: describeApiSource() });
    els.apiArgs.value = "[]";
    return;
  }

  els.apiPanel.classList.remove("fed-empty");
  els.apiComponentSelect.disabled = false;
  els.apiMethodSelect.disabled = false;
  els.runDocApi.disabled = false;

  for (const component of components) {
    const count = currentDocApiItems.filter((item) => item.component === component).length;
    const option = document.createElement("option");
    option.value = component;
    option.textContent = `${component} (${count})`;
    els.apiComponentSelect.appendChild(option);
  }

  renderApiMethodSelect();
}

function renderApiMethodSelect() {
  const componentName = els.apiComponentSelect.value;
  const methods = currentDocApiItems.filter((item) => item.component === componentName);
  els.apiMethodSelect.innerHTML = "";

  if (!methods.length) {
    els.apiMethodSelect.innerHTML = `<option value="">${escapeHtml(t("noMethod"))}</option>`;
    els.apiMethodSelect.disabled = true;
    els.runDocApi.disabled = true;
    els.apiMethodMeta.textContent = t("noCallableMethod");
    els.apiArgs.value = "[]";
    return;
  }

  els.apiMethodSelect.disabled = false;
  els.runDocApi.disabled = false;

  for (const item of methods) {
    const option = document.createElement("option");
    option.value = item.key;
    option.textContent = item.method;
    els.apiMethodSelect.appendChild(option);
  }

  renderSelectedDocApi();
}

function getSelectedDocApi() {
  return currentDocApiByKey.get(els.apiMethodSelect.value) ?? null;
}

function renderSelectedDocApi() {
  const api = getSelectedDocApi();

  if (!api) {
    els.apiMethodMeta.textContent = t("chooseComponentAndMethod");
    els.apiArgs.value = "[]";
    return;
  }

  const defaultArgs = buildDefaultArgsForApi(api);
  const params = parseSignatureParameters(api.signature);
  const paramText = params.length
    ? params.map((param) => `${param.name}${param.optional ? "?" : ""}: ${param.type}`).join(currentLocale === "zh-CN" ? "；" : "; ")
    : t("noArgs");

  els.apiMethodMeta.innerHTML = `
    <div>
      <strong>${escapeHtml(api.component)}.${escapeHtml(api.method)}</strong>
      <code>${escapeHtml(api.signature)}</code>
    </div>
    <div class="fed-api-doc-desc">${escapeHtml(api.description || t("docNoDescription"))}</div>
    <div class="fed-api-doc-extra">
      <span>${escapeHtml(t("returnLabel"))}: ${escapeHtml(api.returns || t("unknown"))}</span>
      <span>${escapeHtml(t("paramsLabel"))}: ${escapeHtml(paramText)}</span>
      <span>${escapeHtml(t("sourceLabel"))}: ${escapeHtml(api.docPath)}</span>
    </div>
  `;
  els.apiArgs.value = safeStringify(defaultArgs);
}

async function runSelectedDocApi() {
  const api = getSelectedDocApi();
  if (!api) {
    throw new Error(t("chooseApiMethod"));
  }
  const args = parseJsonArray(els.apiArgs.value);
  log("api", `${api.component}.${api.method}`, {
    source: api.docPath,
    signature: api.signature,
    args,
  });
  await invokeApi(api.component, api.method, args);
}

async function refreshDocReferenceFromDocs() {
  if (!current.docType) {
    log("warn", t("refreshDocs"), t("refreshDocsBeforeOpen"));
    return;
  }

  const docKey = getDocKey();
  docReferenceCache.delete(`${currentLocale}:${docKey}`);
  log("system", t("refreshDocs"), describeApiSource());
  renderApiPanel({ force: true });
  await renderEventPanel({ force: true });
}

function listenerKey(componentName, eventName) {
  return `${componentName}:${eventName}`;
}

async function detachAllListeners() {
  if (!current.app || activeListeners.size === 0) {
    activeListeners.clear();
    return;
  }

  for (const [key, record] of activeListeners) {
    try {
      current.app?.[record.componentName]?.removeEventListener?.(record.eventName, record.handler);
    } catch (error) {
      console.warn("remove listener failed", key, error);
    }
  }
  activeListeners.clear();
}

function subscribeEvent(componentName, eventName, source = "custom") {
  const component = getComponent(componentName);
  if (typeof component.addEventListener !== "function") {
    throw new Error(`app.${componentName} 不支持 addEventListener`);
  }

  const key = listenerKey(componentName, eventName);
  if (activeListeners.has(key)) {
    log("warn", "event duplicated locally", { componentName, eventName });
    return "duplicated locally";
  }

  const handler = (...args) => {
    log("event", `${componentName}.${eventName}`, args.length <= 1 ? args[0] : args);
  };
  const status = component.addEventListener(eventName, handler);
  if (status === "ok") {
    activeListeners.set(key, { componentName, eventName, handler, source });
  }
  log(status === "ok" ? "success" : "warn", `${componentName}.${eventName} subscribe`, { status, source });
  return status;
}

function unsubscribeEvent(componentName, eventName) {
  const key = listenerKey(componentName, eventName);
  const record = activeListeners.get(key);
  if (!record) {
    log("warn", "event listener not found", { componentName, eventName });
    return;
  }
  const component = getComponent(componentName);
  const status = component.removeEventListener(eventName, record.handler);
  activeListeners.delete(key);
  log(status === "ok" ? "success" : "warn", `${componentName}.${eventName} unsubscribe`, { status });
}


function attachDefaultEvents() {
  for (const item of currentDocEventItems) {
    try {
      subscribeEvent(item.component, item.eventName, "auto-doc");
    } catch (error) {
      log("warn", `auto event skipped: ${item.component}.${item.eventName}`, error);
    }
  }
}

function applyStaticTranslations() {
  document.querySelectorAll("[data-i18n]").forEach((node) => {
    node.textContent = t(node.dataset.i18n);
  });
  document.querySelectorAll("[data-i18n-placeholder]").forEach((node) => {
    node.setAttribute("placeholder", t(node.dataset.i18nPlaceholder));
  });
  document.querySelectorAll("[data-i18n-aria-label]").forEach((node) => {
    node.setAttribute("aria-label", t(node.dataset.i18nAriaLabel));
  });
}

function renderApiDocHint() {
  if (els.apiDocHint) {
    els.apiDocHint.innerHTML = t("apiDocHint", { path: apiDocsRoot() });
  }
}

function applyLocale(locale, { syncOpenLang = true, refreshPanels = true, announce = false } = {}) {
  currentLocale = normalizeDemoLocale(locale);
  localStorage.setItem(LOCALE_STORAGE_KEY, currentLocale);
  document.documentElement.lang = currentLocale;

  applyStaticTranslations();
  renderApiDocHint();
  setupLanguageOptions();

  if (syncOpenLang) {
    els.lang.value = currentLocale;
  }

  for (const button of [els.localeZh, els.localeEn]) {
    const active = button?.dataset.locale === currentLocale;
    button?.classList.toggle("fed-active-locale", active);
    button?.setAttribute("aria-pressed", active ? "true" : "false");
  }

  if (!selectedFile) {
    els.fileName.textContent = current.fileName || t("fileNotSelected");
  }
  updateDocMeta();
  setApiPanelVisible(apiPanelVisible);
  setLeftPanelCollapsed(leftPanelCollapsed);
  if (!current.docType && !OfficeUI) {
    setStatus(t("readyTitle"), t("readyDetail"));
  }

  if (refreshPanels) {
    docReferenceCache.clear();
    renderApiPanel({ force: true });
    void renderEventPanel({ force: true });
  }

  if (announce) {
    log("system", t("languageChanged"), t("nextOpenLanguage", { locale: currentLocale }));
  }
}


function bindDom() {
  Object.assign(els, {
    localeZh: $("#btn-locale-zh"),
    localeEn: $("#btn-locale-en"),
    preload: $("#btn-preload"),
    refresh: $("#btn-refresh"),
    close: $("#btn-close"),
    toggleApi: $("#btn-toggle-api"),
    toggleLeft: $("#btn-toggle-left"),
    workspace: $("#workspace"),
    leftPanel: $("#left-panel"),
    rightPanel: $("#right-panel"),
    tabApi: $("#tab-api"),
    tabEvents: $("#tab-events"),
    panelApi: $("#panel-api"),
    panelEvents: $("#panel-events"),
    fileInput: $("#file-input"),
    fileName: $("#file-name"),
    openFile: $("#btn-open-file"),
    createFile: $("#btn-create-file"),
    createDocType: $("#create-doc-type"),
    topbar: $("#opt-topbar"),
    bottombar: $("#opt-bottombar"),
    readonly: $("#opt-readonly"),
    lang: $("#opt-lang"),
    userId: $("#user-id"),
    nickName: $("#nick-name"),
    clientId: $("#client-id"),
    userColor: $("#user-color"),
    viewer: $("#viewer"),
    statusTitle: $("#status-title"),
    statusDetail: $("#status-detail"),
    docMeta: $("#doc-meta"),
    openState: $("#open-state"),
    apiDocType: $("#api-doc-type"),
    apiDocHint: $("#api-doc-hint"),
    apiPanel: $("#api-panel"),
    apiComponentSelect: $("#api-component-select"),
    apiMethodSelect: $("#api-method-select"),
    apiMethodMeta: $("#api-method-meta"),
    apiArgs: $("#api-args"),
    runDocApi: $("#btn-run-doc-api"),
    refreshDocs: $("#btn-refresh-docs"),
    refreshEventsDocs: $("#btn-refresh-events-docs"),
    eventLog: $("#event-log"),
    copyLog: $("#btn-copy-log"),
    clearLog: $("#btn-clear-log"),
    eventComponent: $("#event-component"),
    eventName: $("#event-name"),
    subscribe: $("#btn-subscribe"),
    unsubscribe: $("#btn-unsubscribe"),
  });

  const missing = Object.entries(els)
    .filter(([, node]) => !node)
    .map(([name]) => name);
  if (missing.length) {
    throw new Error(t("domMissing", { nodes: missing.join(", ") }));
  }
}

function bindEvents() {
  els.localeZh.addEventListener("click", () => {
    applyLocale("zh-CN", { announce: true });
  });
  els.localeEn.addEventListener("click", () => {
    applyLocale("en-US", { announce: true });
  });

  els.fileInput.setAttribute("accept", ACCEPT_ATTR);
  for (const item of CREATE_DOC_TYPES) {
    const option = document.createElement("option");
    option.value = String(item.value);
    option.textContent = item.label;
    els.createDocType.appendChild(option);
  }
  els.fileInput.addEventListener("change", (event) => {
    const file = event.target.files?.[0];
    if (!file) {
      return;
    }

    selectedFile = file;
    els.fileName.textContent = file.name;
    const supported = isSupportedOfficeFile(file.name);
    log(supported ? "system" : "warn", "file selected", {
      name: file.name,
      size: file.size,
      type: file.type || "unknown",
      supported,
    });
    setStatus(supported ? t("fileSelected") : t("unsupportedFile"), file.name);
  });

  els.preload.addEventListener("click", async () => {
    try {
      await loadSdk({ preload: true });
    } catch (error) {
      log("error", t("preloadFailed"), error);
      setStatus(t("preloadFailed"), String(error?.message ?? error));
      setLastResult(error);
    }
  });

  els.refresh.addEventListener("click", () => {
    try {
      controller?.refresh();
      current.app?.Document?.updateVisibleArea?.();
      log("system", "visible area refreshed");
    } catch (error) {
      log("error", "refresh failed", error);
    }
  });

  els.close.addEventListener("click", () => {
    void closeDocument();
  });

  els.toggleApi.addEventListener("click", () => {
    setApiPanelVisible(!apiPanelVisible);
  });

  els.toggleLeft.addEventListener("click", () => {
    setLeftPanelCollapsed(!leftPanelCollapsed);
  });

  els.tabApi.addEventListener("click", () => {
    setRightTab("api");
  });

  els.tabEvents.addEventListener("click", () => {
    setRightTab("events");
  });

  els.openFile.addEventListener("click", () => {
    try {
      void openDocument(buildFileDataFromInput());
    } catch (error) {
      const info = classifyError(error);
      log("error", info.title, info.detail);
      els.viewer.classList.remove("fed-has-doc");
      setStatus(info.title, info.detail);
      setLastResult(error);
    }
  });

  els.createFile.addEventListener("click", () => {
    void createDocument(Number(els.createDocType.value));
  });


  els.apiComponentSelect.addEventListener("change", () => {
    renderApiMethodSelect();
  });

  els.apiMethodSelect.addEventListener("change", () => {
    renderSelectedDocApi();
  });

  els.runDocApi.addEventListener("click", async () => {
    if (els.runDocApi.disabled) {
      return;
    }
    try {
      await runSelectedDocApi();
    } catch (error) {
      log("error", t("apiCallFailed"), error);
      setLastResult(error);
    }
  });

  els.refreshDocs.addEventListener("click", () => {
    void refreshDocReferenceFromDocs();
  });

  els.refreshEventsDocs.addEventListener("click", () => {
    void refreshDocReferenceFromDocs();
  });

  els.eventComponent.addEventListener("change", () => {
    renderEventNameSelect();
  });

  els.subscribe.addEventListener("click", () => {
    try {
      subscribeEvent(els.eventComponent.value, els.eventName.value.trim(), "custom");
    } catch (error) {
      log("error", t("subscribeFailed"), error);
      setLastResult(error);
    }
  });

  els.unsubscribe.addEventListener("click", () => {
    try {
      unsubscribeEvent(els.eventComponent.value, els.eventName.value.trim());
    } catch (error) {
      log("error", t("unsubscribeFailed"), error);
      setLastResult(error);
    }
  });

  els.clearLog.addEventListener("click", () => {
    els.eventLog.replaceChildren();
    log("system", "log cleared");
  });

  els.copyLog.addEventListener("click", async () => {
    const text = [...els.eventLog.querySelectorAll(".fed-log-item")].map((node) => node.textContent.trim()).join("\n\n");
    try {
      await navigator.clipboard.writeText(text);
      log("success", "log copied");
    } catch (error) {
      log("error", t("copyLogFailed"), error);
    }
  });

  window.addEventListener("beforeunload", () => {
    void controller?.close();
  });
}

async function init() {
  bindDom();
  applyLocale(currentLocale, { syncOpenLang: true, refreshPanels: false });
  populateComponentSelectors();
  updateDocMeta();
  bindEvents();
  renderApiPanel();
  setRightTab("api");
  setApiPanelVisible(false);
  log("system", `demo layout ${DEMO_LAYOUT_VERSION} loaded`);
  log("system", "demo ready", t("apiDocHint", { path: apiDocsRoot() }).replace(/<[^>]+>/g, ""));
}

if (document.readyState === "loading") {
  document.addEventListener("DOMContentLoaded", () => void init(), { once: true });
} else {
  void init();
}
