"use client";

import { useEffect, useMemo, useRef, useState } from "react";
import type { TreeShellHostMode } from "@/components/sidebar/tree-shell-host";
import type { FileTreeRow } from "@/lib/file-tree/types";

type TreeShellDocRow = Extract<FileTreeRow, { kind: "doc" }>;
type TreeShellIndexRow = Extract<FileTreeRow, { kind: "index" }>;
type TreeShellAssetFolderRow = Extract<FileTreeRow, { kind: "asset-folder" }>;
type TreeShellAssetRow = Extract<FileTreeRow, { kind: "asset" }>;

type TreeShellBridgeContextMenuPayload = {
  documentId: string | null;
  assetId: string | null;
  rowId: string | null;
  rowKind: string | null;
  x: number;
  y: number;
};

type TreeShellBridgeMutationPayload = {
  type: string;
  documentId: string | null;
};

type TreeShellBridgeSelectionPayload = {
  selectedRowIds: string[];
  anchorRowId: string | null;
  focusedRowId: string | null;
};

export type TreeShellPickerItem =
  | { kind: "root"; id: null; title: string; subtitle?: string; depth?: number }
  | { kind: "doc"; id: string; title: string; subtitle?: string; depth?: number };

type TreeShellInlineProjectionItem = {
  nodeId: string;
  parentNodeId: string | null;
  title: string;
  depth: number;
  childCount: number;
  position: number;
  expandedByDefault: boolean;
};

export type TreeShellIframeHostProps = {
  mode: TreeShellHostMode;
  surfaceTestId: string;
  workspaceId: string;
  rootNodeId?: string | null;
  activeDocumentId?: string | null;
  allowRootPick?: boolean;
  excludeIds?: string[];
  pickerItems?: TreeShellPickerItem[];
  fileTreeRows?: FileTreeRow[];
  channel?: string;
  host?: string;
  onNavigate?: (documentId: string) => void;
  onPick?: (targetId: string | null) => void;
  onPageContextMenu?: (payload: { documentId: string; x: number; y: number }) => void;
  onFileTreeContextMenu?: (payload: TreeShellBridgeContextMenuPayload) => void;
  onFileTreeSelectionChange?: (payload: TreeShellBridgeSelectionPayload) => void;
  onInternalDrop?: (args: { targetRow: FileTreeRow; rowIds: string[]; copy: boolean }) => void;
  onDropFiles?: (docId: string, files: FileList | File[], targetRow?: FileTreeRow) => void;
  onAssetOpen?: (payload: { assetId: string; documentId: string | null }) => void;
  onTreeMutation?: (payload: TreeShellBridgeMutationPayload) => void;
};

type TreeShellBridgeMessage = {
  channel: string;
  type: string;
  documentId: string | null;
  assetId: string | null;
  rowId: string | null;
  rowKind: string | null;
  x: number;
  y: number;
  selectedRowIds: string[];
  anchorRowId: string | null;
  focusedRowId: string | null;
  rowIds: string[];
  copy: boolean;
  targetRow: FileTreeRow | null;
  files: File[];
};

const INLINE_TREE_SHELL_LOADING_HTML = [
  "<!doctype html>",
  '<html lang="zh-CN">',
  "<head>",
  '  <meta charset="utf-8" />',
  '  <meta name="viewport" content="width=device-width, initial-scale=1" />',
  "  <title>Tree Shell Loading</title>",
  "  <style>",
  "    body { margin: 0; font-family: \"Noto Sans CJK SC\", \"Source Han Sans SC\", sans-serif; background: #f8fafc; color: #475569; }",
  "    main { min-height: 100vh; display: grid; place-items: center; }",
  "    p { margin: 0; font-size: 13px; }",
  "  </style>",
  "</head>",
  "<body>",
  "  <main><p>正在加载树结果…</p></main>",
  "</body>",
  "</html>",
].join("");

const normalizeString = (value: unknown, fallback = "") => {
  if (typeof value !== "string") return fallback;
  const normalized = value.trim();
  return normalized || fallback;
};

const readNullableString = (value: unknown) => {
  const normalized = normalizeString(value);
  return normalized || null;
};

const readNumber = (value: unknown) => {
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
};

const readBoolean = (value: unknown) => value === true;

const isRecord = (value: unknown): value is Record<string, unknown> => {
  return Boolean(value) && typeof value === "object";
};

const readStringArray = (value: unknown) => {
  if (!Array.isArray(value)) {
    return [];
  }
  return value
    .map((item) => normalizeString(item))
    .filter(Boolean);
};

const readFiles = (value: unknown) => {
  if (typeof FileList !== "undefined" && value instanceof FileList) {
    return Array.from(value);
  }
  if (!Array.isArray(value)) {
    return [];
  }
  return value.filter((item): item is File => typeof File !== "undefined" && item instanceof File);
};

const readFileTreeRow = (value: unknown): FileTreeRow | null => {
  if (!isRecord(value)) {
    return null;
  }

  const kind = normalizeString(value.kind);
  const rowId = normalizeString(value.rowId);
  const docId = normalizeString(value.docId);
  const depth = Math.max(0, readNumber(value.depth));

  if (!kind || !rowId || !docId) {
    return null;
  }

  switch (kind) {
    case "doc":
      if (!isRecord(value.node)) {
        return null;
      }
      return {
        kind: "doc",
        rowId: rowId as FileTreeRow["rowId"],
        depth,
        docId,
        parentDocId: readNullableString(value.parentDocId),
        node: value.node as TreeShellDocRow["node"],
        hasChildren: readBoolean(value.hasChildren),
        isExpanded: readBoolean(value.isExpanded),
      };
    case "index":
      if (!isRecord(value.node)) {
        return null;
      }
      return {
        kind: "index",
        rowId: rowId as FileTreeRow["rowId"],
        depth,
        docId,
        parentDocId: readNullableString(value.parentDocId) ?? docId,
        node: value.node as TreeShellIndexRow["node"],
      };
    case "asset-folder":
      if (!isRecord(value.asset)) {
        return null;
      }
      return {
        kind: "asset-folder",
        rowId: rowId as FileTreeRow["rowId"],
        depth,
        docId,
        parentDocId: readNullableString(value.parentDocId) ?? docId,
        asset: value.asset as TreeShellAssetFolderRow["asset"],
        hasChildren: readBoolean(value.hasChildren),
        isExpanded: readBoolean(value.isExpanded),
      };
    case "asset":
      if (!isRecord(value.asset)) {
        return null;
      }
      return {
        kind: "asset",
        rowId: rowId as FileTreeRow["rowId"],
        depth,
        docId,
        parentDocId: readNullableString(value.parentDocId) ?? docId,
        asset: value.asset as TreeShellAssetRow["asset"],
      };
    default:
      return null;
  }
};

function parseTreeShellBridgeMessage(
  value: unknown,
  expectedChannel: string,
): TreeShellBridgeMessage | null {
  if (!value || typeof value !== "object") {
    return null;
  }
  const payload = value as Record<string, unknown>;
  const channel = normalizeString(payload.channel);
  const type = normalizeString(payload.type);
  if (!channel || !type || channel !== expectedChannel) {
    return null;
  }
  return {
    channel,
    type,
    documentId: readNullableString(payload.documentId ?? payload.docId),
    assetId: readNullableString(payload.assetId),
    rowId: readNullableString(payload.rowId ?? payload.targetRowId),
    rowKind: readNullableString(payload.rowKind ?? payload.targetRowKind),
    x: readNumber(payload.x),
    y: readNumber(payload.y),
    selectedRowIds: readStringArray(payload.selectedRowIds),
    anchorRowId: readNullableString(payload.anchorRowId),
    focusedRowId: readNullableString(payload.focusedRowId),
    rowIds: readStringArray(payload.rowIds),
    copy: readBoolean(payload.copy),
    targetRow: readFileTreeRow(payload.targetRow),
    files: readFiles(payload.files),
  };
}

const escapeInlineScriptJson = (input: string) => {
  return input
    .replace(/&/g, "\\u0026")
    .replace(/</g, "\\u003c")
    .replace(/>/g, "\\u003e");
};

export function buildTreeShellInlinePickerItems(
  items: TreeShellPickerItem[],
): TreeShellInlineProjectionItem[] {
  return items
    .filter(
      (item): item is Extract<TreeShellPickerItem, { kind: "doc"; id: string }> =>
        item.kind === "doc" && Boolean(normalizeString(item.id)),
    )
    .map((item, index) => ({
      nodeId: normalizeString(item.id),
      parentNodeId: null,
      title: normalizeString(item.title, "无标题"),
      depth: typeof item.depth === "number" && Number.isFinite(item.depth) ? Math.max(0, item.depth) : 0,
      childCount: 0,
      position: index,
      expandedByDefault: false,
    }));
}

export function injectTreeShellInlineOverrides(
  html: string,
  overrides: { items?: TreeShellInlineProjectionItem[] },
) {
  const payloadJson = escapeInlineScriptJson(JSON.stringify(overrides));
  const scriptTag = `<script>window.__MNOTE_TREE_SHELL_OVERRIDE__ = ${payloadJson};</script>`;
  const anchor = '<script id="tree-shell-state"';
  if (html.includes(anchor)) {
    return html.replace(anchor, `${scriptTag}${anchor}`);
  }
  return `${scriptTag}${html}`;
}

export function buildTreeShellIframeSrc(input: {
  mode: TreeShellHostMode;
  workspaceId: string;
  rootNodeId?: string | null;
  activeDocumentId?: string | null;
  allowRootPick?: boolean;
  excludeIds?: string[];
  channel: string;
  host: string;
}) {
  const params = new URLSearchParams();
  params.set("workspaceId", input.workspaceId);
  params.set("mode", input.mode);
  params.set("channel", input.channel);
  params.set("host", input.host);

  const rootNodeId = normalizeString(input.rootNodeId);
  if (rootNodeId) {
    params.set("rootNodeId", rootNodeId);
  }

  const activeDocumentId = normalizeString(input.activeDocumentId);
  if (activeDocumentId) {
    params.set("activeDocumentId", activeDocumentId);
  }

  if (input.allowRootPick) {
    params.set("allowRootPick", "1");
  }

  const excludeIds = (input.excludeIds ?? [])
    .map((item) => normalizeString(item))
    .filter(Boolean);
  if (excludeIds.length > 0) {
    params.set("excludeIds", excludeIds.join(","));
  }

  return `/api/tree/shell?${params.toString()}`;
}

export function TreeShellIframeHost({
  mode,
  surfaceTestId,
  workspaceId,
  rootNodeId = null,
  activeDocumentId = null,
  allowRootPick = false,
  excludeIds = [],
  pickerItems = [],
  fileTreeRows = [],
  channel,
  host,
  onNavigate,
  onPick,
  onPageContextMenu,
  onFileTreeContextMenu,
  onFileTreeSelectionChange,
  onInternalDrop,
  onDropFiles,
  onAssetOpen,
  onTreeMutation,
}: TreeShellIframeHostProps) {
  const iframeRef = useRef<HTMLIFrameElement | null>(null);
  const [inlineSrcDoc, setInlineSrcDoc] = useState<string | null>(null);
  const [inlineLoadFailed, setInlineLoadFailed] = useState(false);
  const resolvedChannel = channel?.trim() || surfaceTestId;
  const resolvedHost = host?.trim() || surfaceTestId;
  const fileTreeRowById = useMemo(
    () => new Map(fileTreeRows.map((row) => [row.rowId, row])),
    [fileTreeRows],
  );
  const inlinePickerItems = useMemo(
    () => (mode === "picker" ? buildTreeShellInlinePickerItems(pickerItems) : []),
    [mode, pickerItems],
  );
  const useInlinePickerOverride = inlinePickerItems.length > 0;
  const src = useMemo(
    () =>
      buildTreeShellIframeSrc({
        mode,
        workspaceId,
        rootNodeId,
        activeDocumentId,
        allowRootPick,
        excludeIds,
        channel: resolvedChannel,
        host: resolvedHost,
      }),
    [
      activeDocumentId,
      allowRootPick,
      excludeIds,
      mode,
      resolvedChannel,
      resolvedHost,
      rootNodeId,
      workspaceId,
    ],
  );

  useEffect(() => {
    if (!useInlinePickerOverride) {
      setInlineSrcDoc(null);
      setInlineLoadFailed(false);
      return;
    }

    let disposed = false;
    setInlineLoadFailed(false);
    setInlineSrcDoc(null);

    void (async () => {
      try {
        const response = await fetch(src, {
          method: "GET",
          credentials: "include",
          cache: "no-store",
        });
        if (!response.ok) {
          throw new Error(`tree shell inline override fetch failed: ${response.status}`);
        }
        const html = await response.text();
        if (disposed) {
          return;
        }
        setInlineSrcDoc(
          injectTreeShellInlineOverrides(html, {
            items: inlinePickerItems,
          }),
        );
      } catch {
        if (disposed) {
          return;
        }
        setInlineLoadFailed(true);
        setInlineSrcDoc(null);
      }
    })();

    return () => {
      disposed = true;
    };
  }, [inlinePickerItems, src, useInlinePickerOverride]);

  useEffect(() => {
    const onMessage = (event: MessageEvent) => {
      if (event.source !== iframeRef.current?.contentWindow) {
        return;
      }
      const message = parseTreeShellBridgeMessage(event.data, resolvedChannel);
      if (!message) {
        return;
      }

      switch (message.type) {
        case "tree.navigate":
          if (message.documentId) {
            onNavigate?.(message.documentId);
          }
          return;
        case "tree.pick":
          onPick?.(message.documentId);
          return;
        case "tree.pick.root":
          onPick?.(null);
          return;
        case "tree.page.context-menu":
          if (message.documentId) {
            onPageContextMenu?.({
              documentId: message.documentId,
              x: message.x,
              y: message.y,
            });
          }
          return;
        case "tree.filetree.context-menu":
          onFileTreeContextMenu?.({
            documentId: message.documentId,
            assetId: message.assetId,
            rowId: message.rowId,
            rowKind: message.rowKind,
            x: message.x,
            y: message.y,
          });
          return;
        case "tree.filetree.selection.changed":
          onFileTreeSelectionChange?.({
            selectedRowIds: message.selectedRowIds,
            anchorRowId: message.anchorRowId,
            focusedRowId: message.focusedRowId,
          });
          return;
        case "tree.filetree.internal-drop":
          if (message.rowIds.length > 0) {
            const targetRow =
              message.targetRow ??
              (message.rowId ? (fileTreeRowById.get(message.rowId) ?? null) : null);
            if (!targetRow) {
              return;
            }
            onInternalDrop?.({
              targetRow,
              rowIds: message.rowIds,
              copy: message.copy,
            });
          }
          return;
        case "tree.filetree.external-drop":
        case "tree.filetree.drop-files":
          if (message.files.length > 0) {
            const targetRow =
              message.targetRow ??
              (message.rowId ? (fileTreeRowById.get(message.rowId) ?? null) : null);
            const targetDocId = message.documentId ?? targetRow?.docId ?? null;
            if (!targetDocId) {
              return;
            }
            onDropFiles?.(targetDocId, message.files, targetRow ?? undefined);
          }
          return;
        case "tree.asset.open":
          if (message.assetId) {
            onAssetOpen?.({
              assetId: message.assetId,
              documentId: message.documentId,
            });
          }
          return;
        case "tree.node.created":
        case "tree.node.renamed":
        case "tree.subtree.moved":
          onTreeMutation?.({
            type: message.type,
            documentId: message.documentId,
          });
          return;
      }
    };

    window.addEventListener("message", onMessage);
    return () => {
      window.removeEventListener("message", onMessage);
    };
  }, [
    onAssetOpen,
    fileTreeRowById,
    onFileTreeContextMenu,
    onFileTreeSelectionChange,
    onInternalDrop,
    onDropFiles,
    onNavigate,
    onPageContextMenu,
    onPick,
    onTreeMutation,
    resolvedChannel,
  ]);

  return (
    <iframe
      ref={iframeRef}
      src={useInlinePickerOverride && !inlineLoadFailed ? undefined : src}
      srcDoc={useInlinePickerOverride && !inlineLoadFailed ? inlineSrcDoc ?? INLINE_TREE_SHELL_LOADING_HTML : undefined}
      title={`tree-shell-${mode}`}
      data-testid={`${surfaceTestId}-rust-iframe`}
      data-tree-shell-mode={mode}
      data-tree-shell-channel={resolvedChannel}
      data-tree-shell-inline={useInlinePickerOverride && !inlineLoadFailed ? "1" : "0"}
      className="h-full w-full border-0 bg-white"
    />
  );
}
