import { canonicalizeMindmapData, type MindMapData } from "./mindmap-projection";

export type MindmapNodeInput = {
  uid?: string;
  text: string;
  hyperlink?: string;
  note?: string;
  refs?: unknown[];
};

export type MindmapKernelCommand =
  | { type: "updateText"; mindmapId: string; nodeId: string; text: string }
  | { type: "insertChild"; mindmapId: string; parentNodeId: string; node: MindmapNodeInput }
  | { type: "insertSiblingAfter"; mindmapId: string; targetNodeId: string; node: MindmapNodeInput }
  | { type: "deleteNode"; mindmapId: string; nodeId: string }
  | { type: "moveNode"; mindmapId: string; nodeId: string; newParentNodeId: string; order?: number }
  | { type: "patchView"; mindmapId: string; patch: Record<string, unknown> }
  | { type: "setLayout"; mindmapId: string; layout: unknown }
  | { type: "setTheme"; mindmapId: string; theme: unknown; themeConfig?: unknown };

export type MindmapCompatPayloadPatch = {
  type: "compatPayloadPatch";
  mindmapId: string;
  path: string;
  value: unknown;
  source: "toolbar" | "sidebar" | "runtime-event" | "adapter-diff";
  actionId?: string;
  runtimeRevision?: number | null;
  kernelRevision?: number | null;
};

export type MindmapCommandDiffResult = {
  commands: MindmapKernelCommand[];
  compatPatches: MindmapCompatPayloadPatch[];
};

export type ToolbarCommandInput = {
  mindmapId: string;
  runtimeCommand: string;
  activeNodeId?: string | null;
  newNode?: MindmapNodeInput;
  layout?: unknown;
  theme?: unknown;
  themeConfig?: unknown;
};

const defaultNode = (node?: MindmapNodeInput): MindmapNodeInput => ({
  text: node?.text?.trim() || "新节点",
  ...(node?.uid ? { uid: node.uid } : {}),
  ...(node?.hyperlink ? { hyperlink: node.hyperlink } : {}),
  ...(node?.note ? { note: node.note } : {}),
  ...(node?.refs ? { refs: node.refs } : {}),
});

export const createToolbarKernelCommand = (
  input: ToolbarCommandInput,
): MindmapKernelCommand | MindmapCompatPayloadPatch | null => {
  const activeNodeId = input.activeNodeId?.trim();
  switch (input.runtimeCommand) {
    case "INSERT_CHILD_NODE":
      if (!activeNodeId) return null;
      return {
        type: "insertChild",
        mindmapId: input.mindmapId,
        parentNodeId: activeNodeId,
        node: defaultNode(input.newNode),
      };
    case "INSERT_NODE":
      if (!activeNodeId) return null;
      return {
        type: "insertSiblingAfter",
        mindmapId: input.mindmapId,
        targetNodeId: activeNodeId,
        node: defaultNode(input.newNode),
      };
    case "REMOVE_NODE":
    case "DELETE_NODE":
      if (!activeNodeId) return null;
      return { type: "deleteNode", mindmapId: input.mindmapId, nodeId: activeNodeId };
    case "ADD_GENERALIZATION":
      return {
        type: "compatPayloadPatch",
        mindmapId: input.mindmapId,
        path: "root.data.generalization",
        value: { text: "概要" },
        source: "toolbar",
      };
    case "ADD_OUTER_FRAME":
      return {
        type: "compatPayloadPatch",
        mindmapId: input.mindmapId,
        path: "outerFrame",
        value: { enabled: true, activeNodeId },
        source: "toolbar",
      };
    default:
      return null;
  }
};

export const createViewPatchCommand = (
  mindmapId: string,
  patch: Record<string, unknown>,
): MindmapKernelCommand => ({
  type: "patchView",
  mindmapId,
  patch,
});

export const createThemeCommand = (
  mindmapId: string,
  theme: unknown,
  themeConfig?: unknown,
): MindmapKernelCommand => ({
  type: "setTheme",
  mindmapId,
  theme,
  themeConfig,
});

export const createLayoutCommand = (mindmapId: string, layout: unknown): MindmapKernelCommand => ({
  type: "setLayout",
  mindmapId,
  layout,
});

export const createCompatPayloadPatch = (input: {
  mindmapId: string;
  path: string;
  value: unknown;
  source: MindmapCompatPayloadPatch["source"];
  actionId?: string;
  runtimeRevision?: number | null;
  kernelRevision?: number | null;
}): MindmapCompatPayloadPatch => ({
  type: "compatPayloadPatch",
  mindmapId: input.mindmapId,
  path: input.path,
  value: input.value,
  source: input.source,
  ...(input.actionId ? { actionId: input.actionId } : {}),
  ...(typeof input.runtimeRevision === "number" ? { runtimeRevision: input.runtimeRevision } : {}),
  ...(typeof input.kernelRevision === "number" ? { kernelRevision: input.kernelRevision } : {}),
});

type IndexedRuntimeNode = {
  uid: string;
  parentUid: string | null;
  order: number;
  text: string;
  data: Record<string, unknown>;
  node: MindMapData;
};

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

const readNodeUid = (node: MindMapData, fallback: string): string => {
  const uid = node.data?.uid;
  return typeof uid === "string" && uid.trim() ? uid.trim() : fallback;
};

const readNodeText = (node: MindMapData): string => {
  const text = node.data?.text;
  return typeof text === "string" ? text : String(text ?? "");
};

const indexMindmapRuntimeTree = (input: unknown): Map<string, IndexedRuntimeNode> => {
  const root = canonicalizeMindmapData(input);
  const index = new Map<string, IndexedRuntimeNode>();
  const visit = (node: MindMapData, parentUid: string | null, order: number, path: string) => {
    const uid = readNodeUid(node, path);
    const data = isRecord(node.data) ? node.data : {};
    index.set(uid, {
      uid,
      parentUid,
      order,
      text: readNodeText(node),
      data,
      node,
    });
    const children = Array.isArray(node.children) ? node.children : [];
    children.forEach((child, childIndex) => {
      visit(canonicalizeMindmapData(child), uid, childIndex, `${path}.${childIndex}`);
    });
  };
  visit(root, null, 0, "root");
  return index;
};

const serializeComparable = (value: unknown): string => JSON.stringify(value ?? null);

const buildNodeInputFromRuntimeNode = (node: IndexedRuntimeNode): MindmapNodeInput => ({
  uid: node.uid,
  text: node.text || "新节点",
  ...(typeof node.data.hyperlink === "string" ? { hyperlink: node.data.hyperlink } : {}),
  ...(typeof node.data.note === "string" ? { note: node.data.note } : {}),
  ...(Array.isArray(node.data.refs) ? { refs: node.data.refs } : {}),
});

const pushCompatDataPatches = (
  mindmapId: string,
  prev: IndexedRuntimeNode,
  next: IndexedRuntimeNode,
  compatPatches: MindmapCompatPayloadPatch[],
) => {
  const semanticKeys = new Set(["uid", "text", "refs"]);
  const keys = new Set([...Object.keys(prev.data), ...Object.keys(next.data)]);
  keys.forEach((key) => {
    if (semanticKeys.has(key)) return;
    if (serializeComparable(prev.data[key]) === serializeComparable(next.data[key])) return;
    compatPatches.push(
      createCompatPayloadPatch({
        mindmapId,
        path: `nodes.${next.uid}.data.${key}`,
        value: next.data[key],
        source: "adapter-diff",
      }),
    );
  });
};

export const diffMindmapRuntimeDataToKernelCommands = (input: {
  mindmapId: string;
  previous: unknown;
  next: unknown;
}): MindmapCommandDiffResult => {
  const previousIndex = indexMindmapRuntimeTree(input.previous);
  const nextIndex = indexMindmapRuntimeTree(input.next);
  const commands: MindmapKernelCommand[] = [];
  const compatPatches: MindmapCompatPayloadPatch[] = [];

  nextIndex.forEach((nextNode, uid) => {
    const previousNode = previousIndex.get(uid);
    if (!previousNode) {
      const previousSibling = Array.from(nextIndex.values()).find(
        (candidate) =>
          candidate.parentUid === nextNode.parentUid &&
          candidate.order === nextNode.order - 1 &&
          previousIndex.has(candidate.uid),
      );
      if (previousSibling) {
        commands.push({
          type: "insertSiblingAfter",
          mindmapId: input.mindmapId,
          targetNodeId: previousSibling.uid,
          node: buildNodeInputFromRuntimeNode(nextNode),
        });
      } else if (nextNode.parentUid) {
        commands.push({
          type: "insertChild",
          mindmapId: input.mindmapId,
          parentNodeId: nextNode.parentUid,
          node: buildNodeInputFromRuntimeNode(nextNode),
        });
      }
      return;
    }

    if (previousNode.text !== nextNode.text) {
      commands.push({
        type: "updateText",
        mindmapId: input.mindmapId,
        nodeId: uid,
        text: nextNode.text,
      });
    }

    if (
      previousNode.parentUid !== nextNode.parentUid ||
      previousNode.order !== nextNode.order
    ) {
      commands.push({
        type: "moveNode",
        mindmapId: input.mindmapId,
        nodeId: uid,
        newParentNodeId: nextNode.parentUid ?? "",
        order: nextNode.order,
      });
    }

    pushCompatDataPatches(input.mindmapId, previousNode, nextNode, compatPatches);
  });

  previousIndex.forEach((previousNode, uid) => {
    if (!nextIndex.has(uid) && previousNode.parentUid) {
      commands.push({ type: "deleteNode", mindmapId: input.mindmapId, nodeId: uid });
    }
  });

  return { commands, compatPatches };
};
