<script setup lang="ts">
import { computed, nextTick, onMounted, ref, watch } from "vue";
import dagre from "@dagrejs/dagre";
import { Handle, MarkerType, Position, VueFlow, useVueFlow, type Edge, type Node } from "@vue-flow/core";
import { Focus, Minus, Plus } from "lucide-vue-next";
import StatusBadge from "@/components/common/StatusBadge.vue";
import {
  agentObserverStatusToken,
  type AgentObserverRelationRow,
  type AgentObserverRun,
} from "@/stores/agent-observer";
import "@vue-flow/core/dist/style.css";
import "@vue-flow/core/dist/theme-default.css";

interface MindMapNodeData {
  row: AgentObserverRelationRow;
  title: string;
  identity: string;
}

const props = defineProps<{
  rows: AgentObserverRelationRow[];
  runs: AgentObserverRun[];
  selectedRunId: string | null;
}>();

const emit = defineEmits<{
  select: [runId: string];
  toggle: [relationId: string, expanded: boolean];
}>();

const flowId = "agentrun-mind-map";
const { fitView, zoomIn, zoomOut } = useVueFlow({ id: flowId });
const runById = computed(() => new Map(props.runs.map((run) => [run.id, run])));
const layoutCache = new Map<string, Map<string, { x: number; y: number }>>();
const initialFitDone = ref(false);

const graph = computed(() => {
  // 状态和消息更新只刷新节点内容，不重新计算位置。
  const layoutKey = props.rows.map((row) => `${row.id}|${row.parentId ?? ""}|${row.kind}|${row.childCount}|${row.expanded}`).join("\n");
  let positions = layoutCache.get(layoutKey);
  if (!positions) {
    const layout = new dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
    layout.setGraph({ rankdir: "LR", ranksep: 74, nodesep: 24, edgesep: 12, marginx: 32, marginy: 32 });
    for (const row of props.rows) layout.setNode(row.id, { width: nodeWidth(row), height: 70 });
    for (const row of props.rows) if (row.parentId && layout.hasNode(row.parentId)) layout.setEdge(row.parentId, row.id);
    dagre.layout(layout);
    positions = new Map(props.rows.map((row) => {
      const position = layout.node(row.id);
      return [row.id, { x: position.x - position.width / 2, y: position.y - position.height / 2 }];
    }));
    layoutCache.set(layoutKey, positions);
    if (layoutCache.size > 8) layoutCache.delete(layoutCache.keys().next().value!);
  }

  const nodes: Node<MindMapNodeData>[] = props.rows.map((row) => {
    const position = positions.get(row.id) ?? { x: 0, y: 0 };
    const run = row.runId ? runById.value.get(row.runId) : undefined;
    const title = row.kind === "run" && run?.title ? run.title : row.label;
    return {
      id: row.id,
      type: "observer",
      position,
      sourcePosition: Position.Right,
      targetPosition: Position.Left,
      selectable: false,
      draggable: false,
      data: { row, title, identity: row.kind === "run" && run ? shortId(run.id) : shortId(row.label) },
    };
  });
  const edges: Edge[] = props.rows
    .filter((row) => row.parentId && positions.has(row.parentId))
    .map((row) => ({
      id: `${row.parentId}->${row.id}`,
      source: row.parentId!,
      target: row.id,
      type: "smoothstep",
      markerEnd: MarkerType.ArrowClosed,
    }));
  return { nodes, edges };
});

function nodeWidth(row: AgentObserverRelationRow): number {
  return row.kind === "scope" ? 260 : row.kind === "run" ? 244 : 220;
}

function shortId(value: string): string {
  return value.length > 28 ? `${value.slice(0, 13)}…${value.slice(-10)}` : value;
}

function nodeLabel(kind: AgentObserverRelationRow["kind"]): string {
  return ({ scope: "范围", session: "会话", task: "任务", run: "运行", attempt: "尝试", command: "命令" }[kind]);
}

function activate(row: AgentObserverRelationRow): void {
  if (row.runId) emit("select", row.runId);
  else if (row.childCount > 0) emit("toggle", row.id, !row.expanded);
}

async function fit(): Promise<void> {
  await nextTick();
  await fitView({ padding: 0.14, duration: 220, maxZoom: 1 });
}

async function fitInitial(): Promise<void> {
  if (initialFitDone.value || !props.rows.length) return;
  await fit();
  initialFitDone.value = true;
}

watch(() => graph.value.nodes.length, () => { void fitInitial(); }, { flush: "post" });
onMounted(() => { void fitInitial(); });
</script>

<template>
  <div class="agent-mind-map">
    <VueFlow
      :id="flowId"
      :nodes="graph.nodes"
      :edges="graph.edges"
      :min-zoom="0.18"
      :max-zoom="1.8"
      :zoom-on-double-click="false"
      :nodes-draggable="false"
      :nodes-connectable="false"
      :elements-selectable="false"
      :fit-view-on-init="false"
      class="agent-mind-map-flow"
      aria-label="AgentRun 思维导图"
    >
      <template #node-observer="{ data }">
        <article
          class="agent-mind-map-node"
          :data-kind="data.row.kind"
          :data-status="data.row.status ? agentObserverStatusToken(data.row.status).tone : 'unknown'"
          :data-selected="selectedRunId === data.row.runId"
          :title="data.row.detail || data.title"
          tabindex="0"
          @click="activate(data.row)"
          @keydown.enter.prevent="activate(data.row)"
          @keydown.space.prevent="activate(data.row)"
        >
          <Handle v-if="data.row.parentId" type="target" :position="Position.Left" />
          <Handle v-if="data.row.childCount > 0" type="source" :position="Position.Right" />
          <header>
            <span>{{ nodeLabel(data.row.kind) }}</span>
            <StatusBadge v-if="data.row.status" :status="agentObserverStatusToken(data.row.status).tone" :label="agentObserverStatusToken(data.row.status).label" />
            <button
              v-if="data.row.childCount > 0"
              class="agent-mind-map-toggle nodrag"
              type="button"
              :title="data.row.expanded ? '折叠分支' : '展开分支'"
              :aria-label="data.row.expanded ? '折叠分支' : '展开分支'"
              @click.stop="emit('toggle', data.row.id, !data.row.expanded)"
            >{{ data.row.expanded ? '−' : '+' }}</button>
          </header>
          <strong>{{ data.title }}</strong>
          <small class="mono">{{ data.row.detail || data.identity }}</small>
        </article>
      </template>
    </VueFlow>

    <div class="agent-mind-map-controls" aria-label="关系图缩放控制">
      <button type="button" title="放大" aria-label="放大" @click="zoomIn({ duration: 120 })"><Plus :size="16" aria-hidden="true" /></button>
      <button type="button" title="缩小" aria-label="缩小" @click="zoomOut({ duration: 120 })"><Minus :size="16" aria-hidden="true" /></button>
      <button type="button" title="适应视图" aria-label="适应视图" @click="fit"><Focus :size="16" aria-hidden="true" /></button>
    </div>
  </div>
</template>
