"use client";

import { useEffect, useMemo, useRef, useState } from "react";
import { useSearchParams } from "next/navigation";
import { OnlyOfficeAiAgentPanel } from "@/components/onlyoffice/OnlyOfficeAiAgentPanel";
import { getMnoteRuntimeConfig } from "@/lib/runtime-config";
import { useConvex } from "convex/react";
import { api } from "@/lib/convex/api";
import {
  buildOnlyOfficeCallbackUrl,
  buildOnlyOfficeForcesaveUrl,
  buildOnlyOfficeOpenFileId,
  docTypeFromExt,
  hashOnlyOfficeKey,
  resolveOnlyOfficeDocumentUrl,
} from "@/lib/onlyoffice/client-session";

type EditorMode = "view" | "edit";

const MNOTE_AGENT_PLUGIN_GUID = "asc.{F2B9A7B4-3A22-4E0E-8B32-3B0DE7AE8A60}";

declare global {
  interface Window {
    __MNOTE_ONLYOFFICE_ERRLOG__?: Array<Record<string, unknown>>;
    __MNOTE_ONLYOFFICE_ERR_HOOKED__?: boolean;
    __MNOTE_ONLYOFFICE_DOMPATCHED__?: boolean;
    __MNOTE_ONLYOFFICE_DEBUG__?: Record<string, unknown>;
  }
}

const setupOnlyOfficeGlobalErrorCapture = () => {
  if (typeof window === "undefined") return;
  if (window.__MNOTE_ONLYOFFICE_ERR_HOOKED__) return;
  window.__MNOTE_ONLYOFFICE_ERR_HOOKED__ = true;

  const push = (payload: Record<string, unknown>) => {
    try {
      window.__MNOTE_ONLYOFFICE_ERRLOG__ = window.__MNOTE_ONLYOFFICE_ERRLOG__ || [];
      window.__MNOTE_ONLYOFFICE_ERRLOG__.push(payload);
      // 说明：写入 localStorage，便于“发生崩溃导致整页 reload”时仍能回溯最近错误。
      window.localStorage.setItem("mnote_onlyoffice_last_error", JSON.stringify(payload));
    } catch {
      // ignore
    }
  };

  window.addEventListener(
    "error",
    (e) => {
      try {
        push({
          kind: "error",
          message: String((e as ErrorEvent).message || ""),
          filename: String((e as ErrorEvent).filename || ""),
          lineno: Number((e as ErrorEvent).lineno || 0),
          colno: Number((e as ErrorEvent).colno || 0),
          name: String(((e as any)?.error as any)?.name || ""),
        });
      } catch {
        // ignore
      }
    },
    true,
  );

  window.addEventListener(
    "unhandledrejection",
    (e) => {
      try {
        const r = (e as PromiseRejectionEvent).reason as any;
        push({
          kind: "rejection",
          message: String(r?.message ?? r ?? ""),
          name: String(r?.name ?? ""),
        });
      } catch {
        // ignore
      }
    },
    true,
  );

  // 说明：ONLYOFFICE 内部偶发触发 removeChild 的 NotFoundError（不同环境下 message 可能为空）。
  // 该异常会导致 Next.js 直接显示“客户端异常”白屏，因此这里在 ONLYOFFICE 页面内对 removeChild 做兜底补丁。
  // 仅在 /onlyoffice 页面生效，不影响其它页面。
  if (!window.__MNOTE_ONLYOFFICE_DOMPATCHED__) {
    window.__MNOTE_ONLYOFFICE_DOMPATCHED__ = true;
    try {
      const orig = Node.prototype.removeChild;
       
      (Node.prototype as any).removeChild = function removeChildPatched<T extends Node>(child: T): T {
        try {
          return orig.call(this, child) as T;
        } catch (e) {
          const name = (e as any)?.name ? String((e as any).name) : "";
          if (name === "NotFoundError") {
            return child;
          }
          throw e;
        }
      };
    } catch {
      // ignore
    }
  }
};

setupOnlyOfficeGlobalErrorCapture();

const loadScript = (src: string) =>
  new Promise<void>((resolve, reject) => {
    const existing = document.querySelector(`script[src="${src}"]`) as HTMLScriptElement | null;
    if (existing) {
      if (existing.dataset.mnoteLoaded === "1") {
        resolve();
        return;
      }
      if (existing.dataset.mnoteFailed === "1") {
        reject(new Error(`加载 ONLYOFFICE 脚本失败: ${src}`));
        return;
      }
      existing.addEventListener("load", () => resolve(), { once: true });
      existing.addEventListener("error", () => reject(new Error(`加载 ONLYOFFICE 脚本失败: ${src}`)), {
        once: true,
      });
      return;
    }
    const script = document.createElement("script");
    script.src = src;
    script.async = true;
    script.onload = () => {
      script.dataset.mnoteLoaded = "1";
      delete script.dataset.mnoteFailed;
      resolve();
    };
    script.onerror = () => {
      script.dataset.mnoteFailed = "1";
      reject(new Error(`加载 ONLYOFFICE 脚本失败: ${src}`));
    };
    document.body.appendChild(script);
  });

const loadScriptCandidates = async (candidates: string[]) => {
  let lastError: Error | null = null;
  for (const src of candidates) {
    try {
      await loadScript(src);
      return src;
    } catch (error) {
      lastError = error instanceof Error ? error : new Error(String(error));
    }
  }
  throw lastError ?? new Error("加载 ONLYOFFICE 脚本失败");
};

const setupOnlyOfficeInternalRequestRewrite = (baseUrl: string, onlyofficeBaseUrlDesktop?: string | null) => {
  // 说明：当我们用 `/onlyoffice-server` 反代 ONLYOFFICE 时，编辑器运行期仍可能发起指向
  // `http://127.0.0.1:8081/cache/...` 的绝对请求（来自 ONLYOFFICE 内部），导致浏览器跨域被 CORS 拦截。
  // 这里在 ONLYOFFICE 页面内对 XHR/fetch/window.open/location.assign 等做 URL 重写：
  // 把 “内部 8081” 的请求改写回同源 `/onlyoffice-server/*`。
  // 注意：ONLYOFFICE 会创建多个 iframe（同源但不同 realm），因此这里也会周期性给新出现的 iframe 打补丁。
  if (typeof window === "undefined") return;
  if (!baseUrl) return;

  const normalizedBase = String(baseUrl || "").trim().replace(/\/+$/, "");
  const isProxyMode = normalizedBase === "/onlyoffice-server" || normalizedBase.endsWith("/onlyoffice-server");
  if (!isProxyMode) return;

  const proxyPrefix = (() => {
    if (/^https?:\/\//i.test(normalizedBase)) return normalizedBase;
    return `${window.location.origin.replace(/\/+$/, "")}${normalizedBase}`;
  })();

  const internalOrigins = new Set<string>([
    "http://127.0.0.1:8081",
    "http://localhost:8081",
    "http://127.0.0.1:8082",
    "http://localhost:8082",
    // 说明：部分环境下 ONLYOFFICE 会错误拼出 https://127.0.0.1:8081 这类 URL，
    // 浏览器会报 ERR_SSL_PROTOCOL_ERROR（因为 8081 实际是 http）。这里也一起兜底重写。
    "https://127.0.0.1:8081",
    "https://localhost:8081",
    "https://127.0.0.1:8082",
    "https://localhost:8082",
  ]);
  try {
    if (onlyofficeBaseUrlDesktop) {
      const u = new URL(onlyofficeBaseUrlDesktop);
      internalOrigins.add(`${u.protocol}//${u.host}`);
    }
  } catch {
    // ignore
  }

  const patchWindow = (win: Window) => {
    try {
      if ((win as any).__MNOTE_ONLYOFFICE_XHR_REWRITE__) return;
      const rewriteUrl = (input: string) => {
        try {
          const u = new (win as any).URL(input, (win as any).location?.origin || window.location.origin);
          // 说明：外网 https 访问时，若 ONLYOFFICE 错误生成 http://<host>/onlyoffice-server/... 会被 Mixed Content 阻止；
          // 这里提前升级为 https，避免浏览器直接拦截请求。
          try {
            const loc = (win as any).location;
            if (loc?.protocol === "https:" && u.protocol === "http:" && u.host === loc.host) {
              if (u.pathname === "/onlyoffice-server" || u.pathname.startsWith("/onlyoffice-server/")) {
                u.protocol = "https:";
                return u.toString();
              }
            }
          } catch {
            // ignore
          }
          const origin = `${u.protocol}//${u.host}`;
          if (!internalOrigins.has(origin)) return input;
          return `${proxyPrefix}${u.pathname}${u.search}${u.hash}`;
        } catch {
          return input;
        }
      };

      // 说明：修复 “下载为 xlsx/docx” 等场景：这类下载常通过 window.open/location 跳转，
      // 如果 URL 指向 127.0.0.1:8081，会导致浏览器无法下载（因为 8081 只在 Docker 内部/代理后可用）。
      try {
        const origWinOpen = (win as any).open;
        if (typeof origWinOpen === "function") {
          (win as any).open = function openPatched(
            url?: string | URL,
            target?: string,
            features?: string,
          ) {
            const nextUrl =
              typeof url === "string"
                ? rewriteUrl(url)
                : url instanceof URL
                  ? new URL(rewriteUrl(url.toString()))
                  : url;
            return origWinOpen.call(this, nextUrl as any, target as any, features as any);
          };
        }
      } catch {
        // ignore
      }

      try {
        const loc = (win as any).location as Location | undefined;
        if (loc && typeof loc.assign === "function") {
          const origAssign = loc.assign.bind(loc);
          loc.assign = ((url: string) => origAssign(rewriteUrl(url))) as any;
        }
        if (loc && typeof loc.replace === "function") {
          const origReplace = loc.replace.bind(loc);
          loc.replace = ((url: string) => origReplace(rewriteUrl(url))) as any;
        }
      } catch {
        // ignore
      }

      try {
        const origFetch = (win as any).fetch;
        if (typeof origFetch === "function") {
          (win as any).fetch = function fetchPatched(input: any, init?: any) {
            try {
              if (typeof input === "string") {
                return origFetch.call(this, rewriteUrl(input), init);
              }
              if (input instanceof URL) {
                return origFetch.call(this, new URL(rewriteUrl(input.toString())), init);
              }
              if (input && typeof input === "object" && typeof input.url === "string") {
                const next = new Request(rewriteUrl(input.url), input);
                return origFetch.call(this, next, init);
              }
            } catch {
              // ignore
            }
            return origFetch.call(this, input, init);
          };
        }
      } catch {
        // ignore
      }

      try {
        const origOpen = (win as any).XMLHttpRequest?.prototype?.open;
        if (typeof origOpen === "function") {
          (win as any).XMLHttpRequest.prototype.open = function openPatched(
            method: string,
            url: string,
            async?: boolean,
            user?: string | null,
            password?: string | null,
          ) {
            const nextUrl = typeof url === "string" ? rewriteUrl(url) : url;
            return origOpen.call(this, method, nextUrl, async, user as any, password as any);
          };
        }
      } catch {
        // ignore
      }

      (win as any).__MNOTE_ONLYOFFICE_XHR_REWRITE__ = true;
    } catch {
      // ignore
    }
  };

  patchWindow(window);

  try {
    const start = Date.now();
    const timer = window.setInterval(() => {
      try {
        const frames = Array.from(document.querySelectorAll("iframe"));
        for (const f of frames) {
          try {
            const w = (f as HTMLIFrameElement).contentWindow;
            if (!w) continue;
            // 说明：同源时才能访问 location；跨域会抛异常，直接跳过。
             
            w.location?.origin;
            patchWindow(w);
          } catch {
            // ignore
          }
        }
      } catch {
        // ignore
      }
      if (Date.now() - start > 120_000) {
        window.clearInterval(timer);
      }
    }, 1000);
  } catch {
    // ignore
  }
};

const waitForDocEditorReady = async (timeoutMs = 120_000) => {
  const start = Date.now();
   
  while (true) {
    // @ts-expect-error ONLYOFFICE 全局对象
    const ok = Boolean(window.DocsAPI && window.DocsAPI.DocEditor);
    if (ok) return;
    if (Date.now() - start > timeoutMs) {
      throw new Error("等待 ONLYOFFICE DocEditor 初始化超时");
    }
     
    await new Promise((r) => setTimeout(r, 250));
  }
};

export default function OnlyOfficePage() {
  const params = useSearchParams();
  const fileUrl = params.get("fileUrl") ?? "";
  const fileName = params.get("fileName") ?? "未命名文档";
  const fileType = (params.get("fileType") ?? "docx").toLowerCase();
  const mode = (params.get("mode") ?? "edit") as EditorMode;
  const assetId = params.get("assetId") ?? "";
  const documentId = params.get("documentId") ?? "";
  const initialUserId = params.get("userId") ?? "";
  const channel = (params.get("channel") ?? "").trim().toLowerCase();
  const convex = useConvex();
  const [error, setError] = useState<string | null>(null);
  const [authedUserId, setAuthedUserId] = useState<string>(initialUserId);
  const [resolvedMode, setResolvedMode] = useState<EditorMode | null>(mode === "view" ? "view" : null);
  const [resolvedDisableDownload, setResolvedDisableDownload] = useState(false);
  const [resolvedDisableCopy, setResolvedDisableCopy] = useState(false);
  const [permissionResolved, setPermissionResolved] = useState(false);
  const [assetSignedUrl, setAssetSignedUrl] = useState<string>("");
  const [assetStorageId, setAssetStorageId] = useState<string>("");
  const [forceSaveState, setForceSaveState] = useState<{
    busy: boolean;
    message: string | null;
    ok: boolean | null;
  }>({ busy: false, message: null, ok: null });
  const runtimeConfig = useMemo(() => getMnoteRuntimeConfig(), []);
  const baseUrlCandidates = useMemo(() => {
    const uniq: string[] = [];
    const push = (v?: string | null) => {
      const s = String(v || "").trim().replace(/\/+$/, "");
      if (!s) return;
      if (!uniq.includes(s)) uniq.push(s);
    };

    const isPageLocal = (() => {
      if (typeof window === "undefined") return true;
      const host = window.location.hostname;
      return host === "127.0.0.1" || host === "localhost";
    })();

    // 说明：网页端优先走同源 /onlyoffice-server（Next 代理到 ONLYOFFICE_INTERNAL_URL），
    // 避免配置里误写成 https 自签证书域名导致浏览器报 ERR_CERT_AUTHORITY_INVALID。
    // 同时同源路径也更利于缓存与跨环境迁移（无需改域名/端口）。
    try {
      push("/onlyoffice-server");
      push(`${window.location.origin.replace(/\/+$/, "")}/onlyoffice-server`);
    } catch {
      // ignore
    }

    // 说明：默认优先使用运行期根据 isDesktop 归一化后的 onlyofficeBaseUrl；
    // 如遇到端口转发/本机服务不可达，可自动回退到另一套配置。
    if (channel === "web") {
      push(runtimeConfig.onlyofficeBaseUrlWeb);
      push(runtimeConfig.onlyofficeBaseUrl);
      // 说明：Web 端通过公网访问时，不应回退到 localhost/127（会触发浏览器“本地网络”权限提示，且必然不可达）。
      if (isPageLocal) {
        push(runtimeConfig.onlyofficeBaseUrlDesktop);
      }
      return uniq;
    }
    if (channel === "desktop") {
      push(runtimeConfig.onlyofficeBaseUrlDesktop);
      push(runtimeConfig.onlyofficeBaseUrl);
      push(runtimeConfig.onlyofficeBaseUrlWeb);
      return uniq;
    }

    push(runtimeConfig.onlyofficeBaseUrl);
    if (runtimeConfig.isDesktop) {
      push(runtimeConfig.onlyofficeBaseUrlWeb);
    } else {
      if (isPageLocal) {
        push(runtimeConfig.onlyofficeBaseUrlDesktop);
      }
    }
    return uniq;
  }, [channel, runtimeConfig]);

  const [baseUrlIndex, setBaseUrlIndex] = useState(0);
  const baseUrl = baseUrlCandidates[baseUrlIndex] || "";
  const storageHostOverride = runtimeConfig.onlyofficeStorageHostOverride;
  const proxyOrigin = runtimeConfig.onlyofficeProxyOrigin;
  const callbackOrigin = runtimeConfig.onlyofficeCallbackOrigin;
  const docEditorRef = useRef<any>(null);

  useEffect(() => {
    // 说明：为了让 document.key 在同一附件的多次打开之间保持稳定（提升缓存命中与加载速度），
    // 这里在有 assetId 时拉取一次附件元信息（storage_id）。
    if (!assetId) return;
    let canceled = false;
    fetch(`/api/media/sign?assetId=${encodeURIComponent(assetId)}`, { method: "GET" })
      .then(async (r) => {
        if (!r.ok) return null;
        return (await r.json().catch(() => null)) as
          | { signedUrl?: string; asset?: { storage_id?: string | null } | null }
          | null;
      })
      .then((payload) => {
        if (!payload || canceled) return;
        const signed = String(payload.signedUrl || "").trim();
        const sid = String(payload.asset?.storage_id || "").trim();
        if (signed) setAssetSignedUrl(signed);
        if (sid) setAssetStorageId(sid);
      })
      .catch(() => {
        // ignore
      });
    return () => {
      canceled = true;
    };
  }, [assetId]);

  useEffect(() => {
    // 说明：ONLYOFFICE 回调由文档服务器触发，不携带用户 Cookie。
    // 为了让 /api/onlyoffice/callback 能以真实用户身份写回存储，
    // 这里尽量从当前会话获取 userId，并透传到 callbackUrl。
    if (authedUserId) return undefined;
    let canceled = false;
    fetch("/api/auth/whoami", { method: "GET" })
      .then(async (r) => {
        if (!r.ok) return null;
        return (await r.json().catch(() => null)) as { userId?: string } | null;
      })
      .then((payload) => {
        const uid = String(payload?.userId || "").trim();
        if (!uid) return;
        if (canceled) return;
        setAuthedUserId(uid);
      })
      .catch(() => {
        // ignore
      });
    return () => {
      canceled = true;
    };
  }, [authedUserId]);

  useEffect(() => {
    // 只读权限：强制以 view 模式打开（避免分享页面只读但 ONLYOFFICE 仍可编辑）。
    if (!documentId) {
      setResolvedMode(mode === "view" ? "view" : mode);
      setResolvedDisableDownload(false);
      setResolvedDisableCopy(false);
      setPermissionResolved(true);
      return;
    }
    if (!authedUserId) {
      setPermissionResolved(false);
      return;
    }

    let canceled = false;
    void (async () => {
      setPermissionResolved(false);
      try {
        const perm = await convex.query(api.documents.getPermissionForUser, {
          userId: authedUserId,
          id: documentId,
        });
        if (canceled) return;
        const disableDownload = Boolean((perm as any)?.disableDownload);
        const disableCopy = Boolean((perm as any)?.disableCopy);
        setResolvedDisableDownload(disableDownload);
        setResolvedDisableCopy(disableCopy);
        setPermissionResolved(true);

        if (mode === "view") {
          setResolvedMode("view");
          return;
        }

        if (!perm || (perm as any).permission !== "edit") {
          setResolvedMode("view");
        } else {
          setResolvedMode("edit");
        }
      } catch {
        if (canceled) return;
        setResolvedMode(mode === "view" ? "view" : mode);
        setResolvedDisableDownload(false);
        setResolvedDisableCopy(false);
        setPermissionResolved(true);
      }
    })();

    return () => {
      canceled = true;
    };
  }, [authedUserId, convex, documentId, mode]);

  useEffect(() => {
    // 说明：在部分 ONLYOFFICE 版本/环境下，编辑器内部会触发 DOMException(NotFoundError: removeChild)，
    // 该异常会被 Next.js 捕获并显示“客户端异常”大红屏，但实际文档仍可继续使用。
    // 这里仅对该特定错误做兜底拦截，避免误伤其它真实错误。
    const onError = (event: ErrorEvent) => {
      const err = event.error as unknown;
      const msgFromError = (() => {
        try {
          return typeof err === "string" ? err : String((err as any)?.message ?? err);
        } catch {
          return "";
        }
      })();
      const msg = msgFromError || event.message || "";
      const name = (err as any)?.name ? String((err as any).name) : "";

      try {
        window.__MNOTE_ONLYOFFICE_ERRLOG__ = window.__MNOTE_ONLYOFFICE_ERRLOG__ || [];
        window.__MNOTE_ONLYOFFICE_ERRLOG__.push({
          kind: "error",
          name,
          message: msg,
          filename: event.filename || "",
          lineno: event.lineno || 0,
          colno: event.colno || 0,
        });
      } catch {
        // ignore
      }

      // 说明：ONLYOFFICE 内部偶发抛出 DOMException(NotFoundError)，在部分版本下 message 可能为空，
      // 但会被 Next.js 捕获后直接显示“客户端异常”白屏；该错误通常不影响文档继续使用。
      if (name === "NotFoundError") {
        event.preventDefault();
        try {
          event.stopImmediatePropagation();
          event.stopPropagation();
        } catch {
          // ignore
        }
        return;
      }

      // 说明：部分隧道/证书环境下，ONLYOFFICE 会尝试注册 ServiceWorker（用于缓存静态资源）。
      // 但浏览器会以 SecurityError 失败，并触发全局错误，导致 Next.js 显示“客户端异常”白屏。
      // 该错误不影响文档实际编辑能力，因此这里对其进行兜底拦截。
      if (
        name === "SecurityError" ||
        msg.includes("Failed to register a ServiceWorker") ||
        msg.includes("An SSL certificate error occurred when fetching the script")
      ) {
        event.preventDefault();
        try {
          event.stopImmediatePropagation();
          event.stopPropagation();
        } catch {
          // ignore
        }
      }
    };
    const onRejection = (event: PromiseRejectionEvent) => {
      const reason = event.reason as unknown;
      const msg = (() => {
        try {
          return typeof reason === "string" ? reason : String((reason as any)?.message ?? reason);
        } catch {
          return "";
        }
      })();
      const name = (reason as any)?.name ? String((reason as any).name) : "";
      try {
        window.__MNOTE_ONLYOFFICE_ERRLOG__ = window.__MNOTE_ONLYOFFICE_ERRLOG__ || [];
        window.__MNOTE_ONLYOFFICE_ERRLOG__.push({
          kind: "rejection",
          name,
          message: msg,
        });
      } catch {
        // ignore
      }
      if (name === "NotFoundError") {
        event.preventDefault();
        try {
          event.stopImmediatePropagation();
          event.stopPropagation();
        } catch {
          // ignore
        }
        return;
      }
      if (
        name === "SecurityError" ||
        msg.includes("Failed to register a ServiceWorker") ||
        msg.includes("An SSL certificate error occurred when fetching the script")
      ) {
        event.preventDefault();
        try {
          event.stopImmediatePropagation();
          event.stopPropagation();
        } catch {
          // ignore
        }
      }
    };
    window.addEventListener("error", onError);
    window.addEventListener("unhandledrejection", onRejection);
    return () => {
      window.removeEventListener("error", onError);
      window.removeEventListener("unhandledrejection", onRejection);
    };
  }, []);

  const targetDocType = useMemo(() => docTypeFromExt(fileType), [fileType]);
  const effectiveFileUrl = assetSignedUrl || fileUrl;
  const docKey = useMemo(() => {
    // 说明：ONLYOFFICE 的 document.key 会参与其内部缓存/分片路由（部分版本会把它拼进请求参数），
    // 如果 key 每次打开都变化，会导致浏览器缓存命中率极低。
    // - 有 assetId：优先用 assetId + storage_id（保存写回后 storage_id 会变化，自然失效）
    // - 无 assetId：退回到现有 hash 逻辑
    if (assetId) {
      const sid = String(assetStorageId || "").trim();
      // 说明：ONLYOFFICE 的 document.key 允许字符集为 [0-9a-zA-Z_.=-]，不包含 ":" 等字符；
      // 否则可能报错（例如 errorCode=-23）。这里用 hash 生成安全 key，同时在 storage_id 变化时自动失效。
      return sid ? `${assetId}_${hashOnlyOfficeKey(sid)}` : assetId;
    }
    return hashOnlyOfficeKey(`${effectiveFileUrl}-${fileName}`);
  }, [assetId, assetStorageId, effectiveFileUrl, fileName]);
  const resolvedFileUrl = useMemo(() => {
    return resolveOnlyOfficeDocumentUrl({
      effectiveFileUrl,
      proxyOrigin,
      storageHostOverride,
      runtimeSupabaseUrl: runtimeConfig.supabaseUrl || "",
      useConvex: Boolean(runtimeConfig.useConvex),
    });
  }, [effectiveFileUrl, proxyOrigin, storageHostOverride, runtimeConfig.supabaseUrl, runtimeConfig.useConvex]);

  useEffect(() => {
    try {
      window.__MNOTE_ONLYOFFICE_DEBUG__ = {
        pageOrigin: window.location.origin,
        baseUrl,
        proxyOrigin,
        callbackOrigin,
        fileUrlInput: effectiveFileUrl,
        resolvedFileUrl,
        fileName,
        fileType,
        mode,
        resolvedMode,
        resolvedDisableDownload,
        resolvedDisableCopy,
        permissionResolved,
        computedPermissions: {
          edit: resolvedMode === "edit",
          download: !resolvedDisableDownload,
          print: !resolvedDisableDownload,
          copy: !resolvedDisableCopy,
        },
        assetId,
        documentId,
        docKey,
      };
    } catch {
      // ignore
    }
  }, [
    assetId,
    authedUserId,
    baseUrl,
    callbackOrigin,
    docKey,
    effectiveFileUrl,
    fileName,
    fileType,
    mode,
    resolvedMode,
    resolvedDisableCopy,
    resolvedDisableDownload,
    permissionResolved,
    documentId,
    proxyOrigin,
    resolvedFileUrl,
  ]);

  useEffect(() => {
    let canceled = false;
    const destroyEditor = () => {
      try {
        const inst = docEditorRef.current;
        docEditorRef.current = null;
        if (inst && typeof inst.destroyEditor === "function") {
          inst.destroyEditor();
        }
      } catch {
        // ignore
      }
      try {
        const el = document.getElementById("onlyoffice-frame");
        if (el) el.innerHTML = "";
      } catch {
        // ignore
      }
    };

    // 说明：如果是附件（assetId）编辑模式，则必须拿到真实 userId 才能让
    // /api/onlyoffice/callback 通过工作空间成员校验并把文件写回存储。
    // 否则会出现：编辑器里看似“已保存”，但下载/再次打开仍是旧文件。
    if (mode !== "view" && assetId && !authedUserId) {
      return;
    }
    if (!baseUrl) {
      setError("缺少 NEXT_PUBLIC_ONLYOFFICE_BASE_URL 配置，无法加载编辑器。");
      return;
    }
    if (!effectiveFileUrl) {
      setError("缺少 fileUrl 参数。");
      return;
    }

    setupOnlyOfficeInternalRequestRewrite(baseUrl, runtimeConfig.onlyofficeBaseUrlDesktop);

    // 说明：外网访问（例如 frp/隧道）时，OnlyOffice 文档服务器运行在本机 Docker 容器内，无法直接访问
    // document.url 里的 127.0.0.1/localhost。此时必须把 document.url 指向一个“容器可访问”的 Next Origin
    //（onlyofficeProxyOrigin / onlyofficeProxyOriginWeb），让 Next 服务端代为回源下载。
    try {
      const pageHost = window.location.hostname;
      const isPageLocal = pageHost === "127.0.0.1" || pageHost === "localhost";
      const isPageRemote = !isPageLocal;
      const u = new URL(effectiveFileUrl);
      const isFileLocal = u.hostname === "127.0.0.1" || u.hostname === "localhost" || u.hostname === "host.docker.internal";
      if (isPageRemote && isFileLocal && !proxyOrigin) {
        setError(
          "外网访问时检测到 fileUrl 为本机地址（127.0.0.1/localhost），但未配置 onlyofficeProxyOriginWeb。请在 public/mnote-env.json 配置 onlyofficeProxyOriginWeb/onlyofficeCallbackOriginWeb（例如 http://host.docker.internal:3000 或当前 Docker 可达的主机 IP）。",
        );
        return;
      }
    } catch {
      // ignore
    }

    if (!resolvedMode) return;
    if (documentId && !permissionResolved) return;
    if (resolvedMode !== "view" && assetId && !authedUserId) return;

    const scriptUrls = [
      `${baseUrl.replace(/\/$/, "")}/web-apps/apps/api/documents/api.js`,
      `${baseUrl.replace(/\/$/, "")}/web-apps/apps/api/documents/api-all.js`,
      `${baseUrl.replace(/\/$/, "")}/web-apps/apps/api/documents/editor.js`,
    ];
    loadScriptCandidates(scriptUrls)
      .then(async () => {
        // 说明：api.js 的 onload 并不代表 DocsAPI/DocEditor 已完全就绪（在慢网/高负载时会出现空白页）。
        // 因此这里额外等待 DocEditor 挂载，避免偶发“白屏但无错误”的体验。
        await waitForDocEditorReady(120_000);
         
        const pluginConfigUrl = `${window.location.origin}/onlyoffice/plugins/agent-tools/config.json`;

        const config: any = {
          width: "100%",
          height: "100%",
          document: {
            fileType,
            title: fileName,
            url: resolvedFileUrl,
            // 说明：key 用于 ONLYOFFICE 内部区分文档实例；应随 URL/文件名变化，避免缓存/冲突。
            // 这里改为“对同一附件尽量稳定”的 key，以提升远端场景的缓存命中与加载速度。
            key: docKey,
          },
          documentType: targetDocType,
          events: {
            // 说明：用于 E2E 判定“文档已真正打开”，避免仅靠 iframe/canvas 误判。
            // 注意：部分版本回调名是 onDocumentReady，也有文档提到 onAppReady；两者都注册。
            onDocumentReady: () => {
              (window as any).__MNOTE_ONLYOFFICE_READY__ = true;
            },
            onAppReady: () => {
              (window as any).__MNOTE_ONLYOFFICE_READY__ = true;
            },
            onError: (e: unknown) => {
              const msg = (() => {
                try {
                  if (typeof e === "string") return e;
                  return JSON.stringify(e);
                } catch {
                  return String(e);
                }
              })();
              setError(msg);
            },
          },
          editorConfig: {
            mode: mode === "view" ? "view" : "edit",
            lang: "zh-CN",
            // 说明：ONLYOFFICE 文档服务器会通过 callbackUrl 回传保存事件，
            // 我们在 /api/onlyoffice/callback 中接收并回写到 Supabase Storage。
            callbackUrl: (() => {
              return buildOnlyOfficeCallbackUrl({
                callbackOrigin,
                proxyOrigin,
                windowOrigin: window.location.origin,
                assetId,
                userId: authedUserId,
              });
            })(),
            customization: {
              feedback: { visible: false },
              // 说明：启用 forcesave 后，用户点击 ONLYOFFICE 的“保存”会触发 status=6 回调，
              // 从而立即把最新版本写回主存储（无需等待关闭文档触发 status=2）。
              forcesave: true,
            },
            plugins: {
              autostart: [MNOTE_AGENT_PLUGIN_GUID],
              pluginsData: [pluginConfigUrl],
            },
          },
        };

        // 兜底：有些版本不会触发 onDocumentReady/onAppReady，这里用轮询判断“编辑器 DOM 已出现”
        // 来设置 ready flag，保证远程 E2E 判定稳定。
        // 只读权限兜底：即使外部传了 mode=edit，也强制改为 view，且禁用编辑写回。
        try {
          config.document = config.document || {};
          config.document.permissions = {
            ...(config.document.permissions || {}),
            edit: resolvedMode !== "view",
            download: !resolvedDisableDownload,
            print: !resolvedDisableDownload,
            copy: !resolvedDisableCopy,
          };
          config.editorConfig = config.editorConfig || {};
          config.editorConfig.mode = resolvedMode === "view" ? "view" : "edit";
          config.editorConfig.customization = config.editorConfig.customization || {};
          config.editorConfig.customization.forcesave = resolvedMode !== "view";
        } catch {
          // ignore
        }

        (window as any).__MNOTE_ONLYOFFICE_READY__ = false;
        const readyDeadline = Date.now() + 120_000;
        const timer = window.setInterval(() => {
          const root = document.querySelector("#onlyoffice-frame") as HTMLElement | null;
          const body = document.body as HTMLElement | null;
          const count =
            (root ? root.querySelectorAll("iframe,canvas").length : 0) +
            (body ? body.querySelectorAll("iframe,canvas").length : 0);
          if (count > 0) {
            (window as any).__MNOTE_ONLYOFFICE_READY__ = true;
            window.clearInterval(timer);
          } else if (Date.now() > readyDeadline) {
            window.clearInterval(timer);
          }
        }, 500);

        fetch("/api/onlyoffice/sign", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ config }),
        })
          .then(async (r) => {
            if (!r.ok) {
              const payload = await r.json().catch(() => null);
              throw new Error(payload?.error ?? "OnlyOffice 签名失败");
            }
            const { token, documentToken, editorConfigToken } = (await r.json()) as {
              token: string | null;
              documentToken: string | null;
              editorConfigToken: string | null;
            };
            if (canceled) return;

            // 兼容不同 ONLYOFFICE 配置：有些版本/配置会校验 document.token/editorConfig.token。
            if (token) config.token = token;
            if (documentToken) {
              config.document = config.document || {};
              config.document.token = documentToken;
            }
            if (editorConfigToken) {
              config.editorConfig = config.editorConfig || {};
              config.editorConfig.token = editorConfigToken;
            }

            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            if (canceled) return;
            docEditorRef.current = new (window as any).DocsAPI.DocEditor("onlyoffice-frame", config);
          })
          .catch((err: Error) => {
            // 说明：如果服务端强制 JWT 且你未配置 ONLYOFFICE_JWT_SECRET，会在这里失败或在编辑器内报错。
            setError(err.message);
          });
      })
      .catch((err: Error) => {
        // 说明：优先“无感回退”到备选 baseUrl（常见于本机 8081 未启动/外网转发不可达）。
        const hasNext = baseUrlIndex + 1 < baseUrlCandidates.length;
        if (hasNext) {
          setBaseUrlIndex((i) => i + 1);
          return;
        }
        setError(err.message);
      });

    return () => {
      canceled = true;
      destroyEditor();
    };
  }, [
    assetId,
    authedUserId,
    baseUrl,
    baseUrlCandidates.length,
    baseUrlIndex,
    callbackOrigin,
    docKey,
    effectiveFileUrl,
    fileName,
    fileType,
    mode,
    proxyOrigin,
    resolvedFileUrl,
    documentId,
    permissionResolved,
    resolvedDisableCopy,
    resolvedDisableDownload,
    resolvedMode,
    targetDocType,
    runtimeConfig.onlyofficeBaseUrlDesktop,
  ]);

  const canForceSave = resolvedMode === "edit" && Boolean(assetId) && Boolean(docKey);
  const triggerForceSave = async () => {
    if (!canForceSave) return;
    if (forceSaveState.busy) return;
    setForceSaveState({ busy: true, message: "正在触发同步保存…", ok: null });
    try {
      const r = await fetch(
        buildOnlyOfficeForcesaveUrl({
          windowOrigin: window.location.origin,
          assetId,
          key: docKey,
        }),
        { method: "POST" },
      );
      if (!r.ok) {
        const payload = (await r.json().catch(() => null)) as { error?: string } | null;
        throw new Error(payload?.error ?? "触发失败");
      }
      setForceSaveState({ busy: false, message: "已触发同步保存：请稍等 1~3 秒后再下载/刷新。", ok: true });
      window.setTimeout(() => {
        setForceSaveState((s) => (s.ok ? { ...s, message: null } : s));
      }, 4000);
    } catch (e) {
      setForceSaveState({
        busy: false,
        message: `同步保存失败：${(e as Error).message}`,
        ok: false,
      });
    }
  };

  if (!error && mode !== "view" && assetId && !authedUserId) {
    return (
      <div className="flex h-screen items-center justify-center bg-slate-50 text-sm text-gray-600">
        正在获取用户身份，用于写回保存…
      </div>
    );
  }

  if (error) {
    return (
      <div className="flex h-screen flex-col items-center justify-center gap-3 bg-slate-50">
        <p className="text-base font-semibold text-red-600">ONLYOFFICE 加载失败</p>
        <p className="text-sm text-gray-600">{error}</p>
      </div>
    );
  }

  return (
    <div className="relative h-screen w-screen bg-slate-50">
      <div id="onlyoffice-frame" className="h-full w-full" />
      {canForceSave && (
        <div className="pointer-events-none absolute right-3 top-3 z-50 flex flex-col items-end gap-2">
          <button
            type="button"
            className="pointer-events-auto rounded-md bg-white/90 px-3 py-2 text-xs text-gray-700 shadow-sm ring-1 ring-gray-200 hover:bg-white disabled:cursor-not-allowed disabled:opacity-60"
            disabled={forceSaveState.busy}
            onClick={() => void triggerForceSave()}
            title="不依赖 ONLYOFFICE 内置保存按钮，直接触发 forcesave 写回主存储"
          >
            {forceSaveState.busy ? "同步保存中…" : "同步保存"}
          </button>
          {forceSaveState.message && (
            <div className="pointer-events-none max-w-[320px] rounded-md bg-black/70 px-3 py-2 text-xs text-white">
              {forceSaveState.message}
            </div>
          )}
        </div>
      )}
      <OnlyOfficeAiAgentPanel
        openFile={{
          id: buildOnlyOfficeOpenFileId({
            assetId,
            docKey,
            resolvedFileUrl,
            fileName,
          }),
          title: fileName,
          fileUrl: resolvedFileUrl,
          mimeType: null,
        }}
      />
    </div>
  );
}
