import { ensureMindmapUids, type MindmapOp, type MindmapTreeNode, type NodeRef } from "@/lib/mindmap/mindmapOps";
import { readMindmapLocal, writeMindmapLocal } from "@/lib/mindmap/mindmapLocalStore";
import { buildDocumentBridgeContextWithActor } from "@/lib/documents/bridge";
import { executeRustBridgeTool } from "@/lib/documents/rust-runtime";

type SupabaseRouteClient = {
  from: (table: string) => any;
};

export type OnlyOfficeSupabaseClient = SupabaseRouteClient;

export type OnlyOfficeToolContext = {
  userId: string;
  documentId?: string;
  // 来自前端（@ 选择/上传）的附件列表，优先使用（避免额外查询）
  attachments?: Array<{ id: string; title: string; fileUrl: string; mimeType?: string | null }>;
};

const isRecord = (v: unknown): v is Record<string, unknown> =>
  typeof v === "object" && v !== null && !Array.isArray(v);

type ResolvedAttachment = { id: string; title: string; fileUrl: string; mimeType?: string | null };

const resolveAttachment = (ctx: OnlyOfficeToolContext, ref: string): ResolvedAttachment | null => {
  const s = String(ref || "").trim();
  if (!s) return null;
  const list = Array.isArray(ctx.attachments) ? ctx.attachments : [];
  const byId = list.find((a) => String(a.id) === s);
  if (byId) return { id: String(byId.id), title: String(byId.title || byId.id), fileUrl: String(byId.fileUrl || ""), mimeType: byId.mimeType ?? null };
  const exactTitle = list.find((a) => String(a.title) === s);
  if (exactTitle)
    return { id: String(exactTitle.id), title: String(exactTitle.title || exactTitle.id), fileUrl: String(exactTitle.fileUrl || ""), mimeType: exactTitle.mimeType ?? null };
  const fuzzy = list.find((a) => String(a.title || "").includes(s) || String(a.fileUrl || "").includes(s));
  if (fuzzy) return { id: String(fuzzy.id), title: String(fuzzy.title || fuzzy.id), fileUrl: String(fuzzy.fileUrl || ""), mimeType: fuzzy.mimeType ?? null };
  return null;
};

const parseStoragePath = (fileUrl: string) => {
  try {
    const url = new URL(fileUrl);
    const segments = url.pathname.split("/").filter(Boolean);
    const objectIdx = segments.findIndex((seg) => seg === "object");
    if (objectIdx === -1 || objectIdx + 2 >= segments.length) return null;
    if (segments[objectIdx + 1] === "public") {
      const bucket = segments[objectIdx + 2];
      const path = segments.slice(objectIdx + 3).join("/");
      return { bucket, path };
    }
    if (segments[objectIdx + 1] === "sign") {
      const bucket = segments[objectIdx + 2];
      const path = segments.slice(objectIdx + 3).join("/");
      return { bucket, path };
    }
    return null;
  } catch {
    return null;
  }
};

const guessExt = (fileNameOrUrl: string) => {
  const s = String(fileNameOrUrl || "").trim();
  if (!s) return "";
  const cleaned = s.split("?")[0].split("#")[0];
  const parts = cleaned.split(".");
  if (parts.length < 2) return "";
  return String(parts[parts.length - 1] || "").toLowerCase();
};

const isPdf = (mimeType: string, fileName: string) => {
  const m = String(mimeType || "").toLowerCase();
  if (m.includes("pdf")) return true;
  return guessExt(fileName) === "pdf";
};

const signForDownload = async (row: Record<string, unknown>) => {
  const bucket = typeof row.bucket === "string" ? row.bucket : "";
  const storagePath = typeof row.storage_path === "string" ? row.storage_path : "";
  const fileUrl = typeof row.file_url === "string" ? row.file_url : "";
  const fileName = typeof row.file_name === "string" ? row.file_name : undefined;

  if (bucket && storagePath) {
    const data = null as any;
    const error = null as any;
    return fileUrl;
    if (error || !data?.signedUrl) throw new Error(error?.message ?? "生成签名 URL 失败");
  }

  const parsed = fileUrl ? parseStoragePath(fileUrl) : null;
  if (!parsed) return fileUrl;

  const data = null as any;
  const error = null as any;
  return fileUrl;
  if (error || !data?.signedUrl) throw new Error(error?.message ?? "生成签名 URL 失败");
  return data.signedUrl;
};

const downloadBytes = async (url: string) => {
  const resp = await fetch(url);
  if (!resp.ok) throw new Error(`下载失败：${resp.status} ${resp.statusText}`);
  return await resp.arrayBuffer();
};

const mineruParseContentList = async (args: { bytes: ArrayBuffer; filename: string; mimeType: string }) => {
  const endpoint = (process.env.MINERU_ENDPOINT || "").trim().replace(/\/$/, "");
  if (!endpoint) throw new Error("缺少 MINERU_ENDPOINT，无法解析 PDF 结构");

  const form = new FormData();
  form.append("files", new Blob([args.bytes], { type: args.mimeType || "application/octet-stream" }), args.filename || "file.pdf");
  form.append("output_dir", "./output");
  form.append("lang_list", "ch");
  form.append("backend", "pipeline");
  form.append("parse_method", "auto");
  form.append("return_md", "false");
  form.append("return_middle_json", "false");
  form.append("return_model_output", "false");
  form.append("return_content_list", "true");
  form.append("return_images", "false");
  form.append("response_format_zip", "false");

  const resp = await fetch(`${endpoint}/file_parse`, { method: "POST", body: form });
  if (!resp.ok) {
    const text = await resp.text().catch(() => "");
    throw new Error(`MinerU 解析失败：${resp.status} ${resp.statusText}${text ? `，${text.slice(0, 300)}` : ""}`);
  }
  const payload = (await resp.json().catch(() => null)) as unknown;
  if (!isRecord(payload) || !isRecord(payload.results)) throw new Error("MinerU 返回格式不正确");
  const keys = Object.keys(payload.results);
  if (!keys.length) throw new Error("MinerU 返回为空");
  const first = payload.results[keys[0]];
  if (!isRecord(first)) throw new Error("MinerU 返回格式不正确（results.*）");
  const raw = first.content_list;
  if (typeof raw !== "string" || !raw.trim()) return [];
  const parsed = JSON.parse(raw) as unknown;
  if (!Array.isArray(parsed)) return [];
  return parsed as Array<Record<string, unknown>>;
};

type ExtractedOutlineItem = { level: number; title: string; page?: number };

const outlineFromContentList = (list: Array<Record<string, unknown>>) => {
  const out: ExtractedOutlineItem[] = [];
  const seen = new Set<string>();
  for (const item of list) {
    const levelRaw = item.text_level;
    const textRaw = item.text;
    const pageIdxRaw = item.page_idx;
    if (typeof levelRaw !== "number" || !Number.isFinite(levelRaw)) continue;
    const level = Math.max(1, Math.min(6, Math.floor(levelRaw)));
    const title = String(textRaw ?? "").replace(/\s+/g, " ").trim();
    if (!title) continue;
    const pageIdx = typeof pageIdxRaw === "number" && Number.isFinite(pageIdxRaw) ? Math.max(0, Math.floor(pageIdxRaw)) : null;
    const page = pageIdx === null ? undefined : pageIdx + 1;
    const key = `${level}|${page ?? ""}|${title}`;
    if (seen.has(key)) continue;
    seen.add(key);
    out.push({ level, title, page });
  }
  return out;
};

const findNodeByUid = (root: MindmapTreeNode, uid: string): MindmapTreeNode | null => {
  const target = String(uid || "");
  if (!target) return null;
  const walk = (n: MindmapTreeNode): MindmapTreeNode | null => {
    if (String(n?.data?.uid || "") === target) return n;
    const children = Array.isArray(n.children) ? n.children : [];
    for (const c of children) {
      const hit = walk(c);
      if (hit) return hit;
    }
    return null;
  };
  return walk(root);
};

const createUid = () => {
  const anyCrypto = globalThis.crypto as unknown as { randomUUID?: () => string } | undefined;
  if (anyCrypto?.randomUUID) return anyCrypto.randomUUID();
  return Math.random().toString(36).slice(2);
};

const buildOutlineOps = (args: { parentUid: string; items: ExtractedOutlineItem[]; attachment: { assetId: string; fileUrl: string; title: string; mimeType?: string | null } }) => {
  const ops: MindmapOp[] = [];
  const parents: Record<number, string> = { 0: args.parentUid };

  for (const item of args.items) {
    const level = Math.max(1, Math.min(6, Math.floor(item.level)));
    const parent = parents[level - 1] || args.parentUid;
    const uid = createUid();
    const page = item.page;

    const refs: NodeRef[] = [
      {
        kind: "pdf",
        assetId: args.attachment.assetId,
        fileUrl: args.attachment.fileUrl,
        page,
        title: args.attachment.title,
        snippet: page ? `第 ${page} 页` : undefined,
      },
    ];

    const hyperlink = (() => {
      const base = String(args.attachment.fileUrl || "").trim();
      if (!base) return undefined;
      if (page && isPdf(String(args.attachment.mimeType ?? ""), base)) return `${base}#page=${page}`;
      return base;
    })();

    ops.push({
      op: "addChild",
      parentUid: parent,
      node: {
        uid,
        text: item.title,
        hyperlink,
        refs,
      },
    });

    parents[level] = uid;
    for (let d = level + 1; d <= 10; d += 1) delete parents[d];
  }

  return ops;
};

export const createOnlyOfficeServerTools = (args: {
  supabase: OnlyOfficeSupabaseClient;
  ctx: OnlyOfficeToolContext;
  allowedToolIds: Set<string>;
}) => {
  const buildRustContext = () =>
    buildDocumentBridgeContextWithActor({
      request: new Request("http://localhost"),
      actor: {
        actorType: "service",
        actorId: "onlyoffice-asset-to-mindmap",
        sessionId: null,
      },
      workspaceId: null,
      source: {
        channel: "onlyoffice-asset-to-mindmap",
        client: "wolai-frontend",
      },
    });

  const asset_extract_outline = async (toolArgs: Record<string, unknown>) => {
    const assetId = String(toolArgs.assetId ?? "").trim();
    const attachmentRef = String(toolArgs.attachmentRef ?? "").trim();
    const resolved = attachmentRef ? resolveAttachment(args.ctx, attachmentRef) : null;
    const targetAssetId = assetId || resolved?.id || "";
    if (!targetAssetId) throw new Error("缺少 assetId / attachmentRef");

    const { data, error } = await args.supabase
      .from("media_assets")
      .select("id,file_name,file_url,mime_type,ocr_text,ocr_status,storage_path,bucket,updated_at,deleted_at,purged_at")
      .eq("id", targetAssetId)
      .is("deleted_at", null)
      .limit(1)
      .maybeSingle();
    if (error) throw new Error(isRecord(error) && typeof error.message === "string" ? error.message : "读取附件失败");
    const row = (data && typeof data === "object" ? (data as Record<string, unknown>) : null) ?? null;
    if (!row) return { ok: true, found: false };

    const fileName = String(row.file_name ?? "");
    const fileUrl = String(row.file_url ?? "");
    const mimeType = String(row.mime_type ?? "");
    const ocrText = String(row.ocr_text ?? "");
    const ocrStatus = String(row.ocr_status ?? "");
    const textPreview = ocrText.trim().slice(0, 3000);

    if (!isPdf(mimeType, fileName || fileUrl)) {
      return {
        ok: true,
        found: true,
        supported: false,
        note: "当前仅优先支持 PDF（M3：PDF→大纲→导图）。Word/PPT 将在后续阶段通过 ONLYOFFICE /converter 或其它解析策略补齐。",
        asset: { id: String(row.id ?? ""), fileName, fileUrl, mimeType, ocrStatus, updatedAt: row.updated_at ?? null },
        hasOcrText: Boolean(ocrText.trim()),
        textPreview,
      };
    }

    // M3：PDF 优先使用 MinerU 的 content_list（含 page_idx + text_level），用于做可跳转大纲
    let outline: ExtractedOutlineItem[] = [];
    let strategy: "mineru_content_list" | "fallback_ocr_text" = "mineru_content_list";

    try {
      const signed = await signForDownload(row);
      const bytes = await downloadBytes(signed);
      const list = await mineruParseContentList({ bytes, filename: fileName || "file.pdf", mimeType: mimeType || "application/pdf" });
      outline = outlineFromContentList(list);
    } catch (e) {
      // 兜底：返回 OCR 文本的一部分，至少让模型能“基于文本生成大纲”（但没有页码保障）
      strategy = "fallback_ocr_text";
      outline = [];
      if (textPreview) outline.push({ level: 1, title: "（未解析到结构化标题：请结合 textPreview 自行生成提纲）" });
    }

    return {
      ok: true,
      found: true,
      supported: true,
      strategy,
      asset: {
        id: String(row.id ?? ""),
        fileName,
        fileUrl,
        mimeType,
        ocrStatus,
        updatedAt: row.updated_at ?? null,
      },
      hasOcrText: Boolean(ocrText.trim()),
      textPreview,
      outline,
      note:
        outline.length > 0
          ? `已提取大纲条目：${outline.length}。`
          : strategy === "fallback_ocr_text"
            ? "未能从 MinerU 获取结构化标题；请确认 MINERU_ENDPOINT 正常，或稍后重试。"
            : "未提取到标题（该 PDF 可能没有明显标题结构）。",
    };
  };

  const asset_to_mindmap = async (toolArgs: Record<string, unknown>) => {
    // 说明：附件大纲提取仍在 TS/MinerU 侧，真正的导图写入由 Rust mindmap_apply_ops 负责。
    const mindmapId = String(toolArgs.mindmapId ?? "").trim();
    if (!mindmapId) throw new Error("缺少 mindmapId");
    const parentUidArg = String(toolArgs.parentUid ?? "").trim();
    const maxItemsRaw = Number(toolArgs.maxItems ?? 120);
    const maxItems = Number.isFinite(maxItemsRaw) ? Math.max(10, Math.min(600, Math.floor(maxItemsRaw))) : 120;
    const reason = String(toolArgs.reason ?? "").trim() || null;

    const documentId = String(args.ctx.documentId ?? "").trim();
    if (!documentId) throw new Error("缺少 documentId 上下文（OnlyOffice 工具需要落盘到指定文档）");

    const outlineResult = (await asset_extract_outline(toolArgs)) as unknown;
    if (!isRecord(outlineResult) || outlineResult.ok !== true) throw new Error("提取大纲失败");
    if (outlineResult.found !== true) throw new Error("未找到附件");
    if (outlineResult.supported !== true) throw new Error(String(outlineResult.note ?? "附件类型暂不支持"));

    const outline = Array.isArray(outlineResult.outline) ? (outlineResult.outline as unknown[]) : [];
    const items = outline
      .map((x) =>
        isRecord(x)
          ? {
              level: Number(x.level ?? 1),
              title: String(x.title ?? ""),
              ...(typeof x.page === "number" ? { page: x.page } : {}),
            }
          : null,
      )
      .filter((x): x is ExtractedOutlineItem => x !== null && x.title.trim().length > 0)
      .slice(0, maxItems);
    if (!items.length) throw new Error("未提取到可用大纲条目");

    const asset = isRecord(outlineResult.asset) ? outlineResult.asset : null;
    const attachment = {
      assetId: String(asset?.id ?? ""),
      title: String(asset?.fileName ?? "附件"),
      fileUrl: String(asset?.fileUrl ?? ""),
      mimeType: isRecord(asset) ? (asset.mimeType as string | null | undefined) : null,
    };
    if (!attachment.assetId) throw new Error("缺少附件 id");

    const { data: baseRaw } = await readMindmapLocal(documentId, mindmapId);
    const base = (baseRaw && typeof baseRaw === "object" ? (baseRaw as MindmapTreeNode) : null) ?? { data: { text: "中心主题" }, children: [] };
    ensureMindmapUids(base);

    const rootUid = String(base?.data?.uid || "");
    const parentUid = parentUidArg || rootUid;
    if (!parentUid) throw new Error("无法确定 parentUid（mindmap 根节点缺少 uid）");
    if (!findNodeByUid(base, parentUid)) throw new Error("未找到 parentUid 对应节点");

    const ops = buildOutlineOps({ parentUid, items, attachment });

    const rustContext = buildRustContext();
    const rustResult = await executeRustBridgeTool<{
      ok: boolean;
      applied?: number;
      errors?: string[];
      data?: unknown;
      meta?: { reason?: string | null } | null;
    }>({
      context: rustContext,
      toolName: "mindmap_apply_ops",
      invocationKind: "command",
      args: {
        ops,
        reason,
      },
      data: base,
      target: {
        pageId: documentId,
        workspaceId: null,
        blockId: mindmapId,
      },
      reason,
    });

    const resultData = rustResult.result && typeof rustResult.result === "object" ? (rustResult.result as Record<string, unknown>).data : null;
    const nextData = resultData && typeof resultData === "object" && !Array.isArray(resultData) ? (resultData as MindmapTreeNode) : base;
    await writeMindmapLocal(documentId, mindmapId, nextData, "OnlyOffice 生成导图（Rust）");

    const applied = Number((rustResult.result as Record<string, unknown> | undefined)?.applied ?? ops.length) || 0;
    const errors = Array.isArray((rustResult.result as Record<string, unknown> | undefined)?.errors)
      ? ((rustResult.result as Record<string, unknown>).errors as string[])
      : [];

    return {
      ok: true,
      mindmapId,
      parentUid,
      applied,
      errors,
      ops,
      meta: {
        assetId: attachment.assetId,
        fileName: attachment.title,
        items: items.length,
        strategy: String(outlineResult.strategy ?? ""),
        rustOwner: "mindmap_apply_ops",
        rustReason: reason,
      },
    };
  };

  const run = async (toolId: string, toolArgs: Record<string, unknown>) => {
    if (!args.allowedToolIds.has(toolId)) throw new Error(`工具未被允许：${toolId}`);
    if (toolId === "asset_extract_outline") return await asset_extract_outline(toolArgs);
    if (toolId === "asset_to_mindmap") return await asset_to_mindmap(toolArgs);
    throw new Error(`未知工具：${toolId}`);
  };

  return { run };
};
