<script setup lang="ts">
import type {AgentDraftState, AgentOption} from '../../types';
import type {
  AgentStudioCanvasSize,
  AgentStudioConnectionView,
  AgentStudioNodeData,
  AgentStudioNodeView,
  AgentStudioViewport,
} from './types';

import {
  computed,
  createApp,
  h,
  onBeforeUnmount,
  onMounted,
  reactive,
  ref,
  shallowRef,
  watch,
} from 'vue';

import {usePreferences} from '@easyflow/preferences';

import {Tinyflow} from '@tinyflow-ai/vue';

import {useAgentStudioLayout} from '../../composables/agent-studio/useAgentStudioLayout';
import {useAgentStudioModel} from '../../composables/agent-studio/useAgentStudioModel';
import AgentStudioBaseNode from './AgentStudioBaseNode.vue';
import AgentStudioCapabilityNode from './AgentStudioCapabilityNode.vue';
import AgentStudioEdgeLayer from './AgentStudioEdgeLayer.vue';

import '@tinyflow-ai/vue/dist/index.css';

const props = defineProps<{
  knowledgeOptions?: AgentOption[];
  mcpOptions?: AgentOption[];
  pluginOptions?: AgentOption[];
  selectedNodeId: string;
  state: AgentDraftState;
  workflowOptions?: AgentOption[];
}>();

const emit = defineEmits<{
  select: [nodeId: string];
}>();

const { isDark } = usePreferences();
const { capture, layout } = useAgentStudioLayout(props.state);
const canvasRef = ref<HTMLElement>();
const canvasSize = ref<AgentStudioCanvasSize>();
const canvasModel = useAgentStudioModel(
  props.state,
  () => props.selectedNodeId,
  layout,
  () => canvasSize.value,
  () => props.knowledgeOptions || [],
  () => ({
    mcp: props.mcpOptions || [],
    plugin: props.pluginOptions || [],
    workflow: props.workflowOptions || [],
  }),
);
const liveNodes = ref<AgentStudioNodeView[]>([]);
const liveViewport = ref<AgentStudioViewport>({ x: 250, y: 100, zoom: 1 });
const flowData = computed(() => {
  const liveNodeMap = new Map(liveNodes.value.map((node) => [node.id, node]));
  return {
    edges: canvasModel.value.edges.map((edge) => ({ ...edge })),
    nodes: canvasModel.value.nodes.map((node) => {
      const liveNode = liveNodeMap.get(node.id);
      return {
        ...node,
        data: { ...node.data },
        position: { ...(liveNode?.position || node.position) },
      };
    }),
    viewport: layout.viewport ? { ...layout.viewport } : undefined,
  };
});
const lastCaptureSignature = ref('');
const lastFlowDataSignature = ref('');

const customNodes = computed(() => ({
  agentStudioBase: {
    presentation: 'plain',
    render: (parent: HTMLElement, node: any) => {
      renderVueNode(parent, AgentStudioBaseNode, node.data);
    },
    onUpdate: (parent: HTMLElement, node: any) => {
      renderVueNode(parent, AgentStudioBaseNode, node.data);
    },
  },
  agentStudioCapability: {
    presentation: 'plain',
    render: (parent: HTMLElement, node: any) => {
      renderVueNode(parent, AgentStudioCapabilityNode, node.data);
    },
    onUpdate: (parent: HTMLElement, node: any) => {
      renderVueNode(parent, AgentStudioCapabilityNode, node.data);
    },
  },
}));

const connections = computed<AgentStudioConnectionView[]>(() => {
  const nodes =
    liveNodes.value.length > 0
      ? liveNodes.value
      : (canvasModel.value.nodes as AgentStudioNodeView[]);
  const nodeMap = new Map(nodes.map((node) => [node.id, node]));
  return canvasModel.value.edges
    .map((edge) => {
      const source = nodeMap.get(edge.source);
      const target = nodeMap.get(edge.target);
      if (!source || !target) return undefined;
      return {
        active: false,
        id: edge.id,
        path: buildConnectionPath(source, target, liveViewport.value),
        sourceId: edge.source,
        targetId: edge.target,
      };
    })
    .filter(Boolean) as AgentStudioConnectionView[];
});

function renderVueNode(
  parent: HTMLElement,
  component: any,
  data: AgentStudioNodeData,
) {
  const current = (parent as any).__agentStudioNodeView as
    | undefined
    | {
        app: ReturnType<typeof createApp>;
        component: any;
        data: AgentStudioNodeData;
        signature: string;
      };
  const signature = JSON.stringify(data);
  if (current && current.component === component) {
    if (current.signature === signature) {
      return;
    }
    Object.assign(current.data, data);
    current.signature = signature;
    return;
  }

  current?.app.unmount();
  parent.textContent = '';

  const reactiveData = reactive({ ...data }) as AgentStudioNodeData;
  const app = createApp({
    render: () => h(component, { data: reactiveData }),
  });
  app.mount(parent);
  (parent as any).__agentStudioNodeView = {
    app,
    component,
    data: reactiveData,
    signature,
  };
}

function getNodeSize(node: AgentStudioNodeView) {
  return {
    height: node.height || (node.data.kind === 'base' ? 104 : 78),
    width: node.width || (node.data.kind === 'base' ? 268 : 212),
  };
}

function toScreenPoint(
  point: { x: number; y: number },
  viewport: AgentStudioViewport,
) {
  return {
    x: point.x * viewport.zoom + viewport.x,
    y: point.y * viewport.zoom + viewport.y,
  };
}

function buildConnectionPath(
  source: AgentStudioNodeView,
  target: AgentStudioNodeView,
  viewport: AgentStudioViewport,
) {
  const sourceSize = getNodeSize(source);
  const targetSize = getNodeSize(target);
  const sourceCenter = {
    x: source.position.x + sourceSize.width / 2,
    y: source.position.y + sourceSize.height / 2,
  };
  const targetCenter = {
    x: target.position.x + targetSize.width / 2,
    y: target.position.y + targetSize.height / 2,
  };
  const sourceOnLeft = targetCenter.x < sourceCenter.x;
  const start = {
    x: source.position.x + (sourceOnLeft ? 0 : sourceSize.width),
    y: sourceCenter.y,
  };
  const end = {
    x: target.position.x + (sourceOnLeft ? targetSize.width : 0),
    y: targetCenter.y,
  };
  const screenStart = toScreenPoint(start, viewport);
  const screenEnd = toScreenPoint(end, viewport);
  const midX = (screenStart.x + screenEnd.x) / 2;
  return [
    `M ${screenStart.x} ${screenStart.y}`,
    `L ${midX} ${screenStart.y}`,
    `L ${midX} ${screenEnd.y}`,
    `L ${screenEnd.x} ${screenEnd.y}`,
  ].join(' ');
}

function handleDataChange(data: any) {
  const signature = buildFlowDataSignature(data);
  if (signature === lastCaptureSignature.value) {
    return;
  }
  lastCaptureSignature.value = signature;

  if (Array.isArray(data?.nodes)) {
    liveNodes.value = data.nodes as AgentStudioNodeView[];
  }
  if (data?.viewport) {
    liveViewport.value = data.viewport as AgentStudioViewport;
  }
  capture({
    nodes: data?.nodes as AgentStudioNodeView[] | undefined,
    viewport: data?.viewport as AgentStudioViewport | undefined,
  });
  const selected = data?.nodes?.find((node: any) => node.selected);
  if (selected?.id && selected.id !== props.selectedNodeId) {
    emit('select', selected.id);
  }
}

function buildFlowDataSignature(data: any) {
  const nodes = Array.isArray(data?.nodes)
    ? data.nodes.map((node: any) => ({
        data: node.data,
        id: node.id,
        position: node.position,
        selected: node.selected,
        type: node.type,
      }))
    : [];
  const edges = Array.isArray(data?.edges)
    ? data.edges.map((edge: any) => ({
        id: edge.id,
        source: edge.source,
        target: edge.target,
      }))
    : [];
  return JSON.stringify({
    edges,
    nodes,
    viewport: data?.viewport,
  });
}

const stableFlowData = shallowRef(flowData.value);
let resizeObserver: ResizeObserver | undefined;

function syncFlowData() {
  const next = flowData.value;
  const signature = buildFlowDataSignature(next);
  if (signature === lastFlowDataSignature.value) {
    return;
  }
  lastFlowDataSignature.value = signature;
  stableFlowData.value = next;
}

syncFlowData();

watch(flowData, syncFlowData);

function updateCanvasSize() {
  const rect = canvasRef.value?.getBoundingClientRect();
  if (!rect) return;
  const next = {
    height: rect.height,
    width: rect.width,
  };
  if (
    canvasSize.value?.height === next.height &&
    canvasSize.value.width === next.width
  ) {
    return;
  }
  canvasSize.value = next;
}

onMounted(() => {
  updateCanvasSize();
  if (typeof ResizeObserver === 'undefined' || !canvasRef.value) return;
  resizeObserver = new ResizeObserver(updateCanvasSize);
  resizeObserver.observe(canvasRef.value);
});

onBeforeUnmount(() => {
  resizeObserver?.disconnect();
});
</script>

<template>
  <div ref="canvasRef" class="agent-studio-canvas">
    <Tinyflow
      class="agent-studio-canvas__flow"
      :data="stableFlowData"
      :theme="isDark ? 'dark' : 'light'"
      :custom-nodes="customNodes"
      :hide-bottom-dock="true"
      :hide-edge-panel="true"
      :hide-mini-map="true"
      :hide-node-handles="true"
      :hide-node-picker="true"
      :hide-node-setting="true"
      :hide-node-toolbar="true"
      :hide-edge-markers="true"
      :edge-animated="false"
      :edge-interaction-width="0"
      :nodes-draggable="true"
      :nodes-connectable="false"
      :elements-selectable="true"
      :drop-enabled="false"
      :on-data-change="handleDataChange"
    />
    <AgentStudioEdgeLayer :connections="connections" />
  </div>
</template>

<style scoped>
.agent-studio-canvas {
  position: absolute;
  inset: 0;
  overflow: hidden;
}

.agent-studio-canvas__flow {
  width: 100%;
  height: 100%;
}

.agent-studio-canvas :deep(.tinyflow),
.agent-studio-canvas :deep(.agentsflow) {
  width: 100%;
  height: 100% !important;
}

.agent-studio-canvas :deep(.svelte-flow__node) {
  z-index: 2 !important;
  border: 0;
  border-radius: 18px;
}

.agent-studio-canvas :deep(.svelte-flow__node::after) {
  display: none;
}

.agent-studio-canvas :deep(.svelte-flow__node:hover),
.agent-studio-canvas :deep(.svelte-flow__node.selectable.selected) {
  border: 0;
  box-shadow: none;
}

.agent-studio-canvas :deep(.svelte-flow__handle),
.agent-studio-canvas :deep(.svelte-flow__minimap),
.agent-studio-canvas :deep(.tf-left-dock),
.agent-studio-canvas :deep(.tf-top-dock),
.agent-studio-canvas :deep(.tf-bottom-dock),
.agent-studio-canvas :deep(.tf-node-toolbar) {
  display: none !important;
}

.agent-studio-canvas :deep(.tf-flow-line-path) {
  stroke: var(--el-border-color);
  stroke: color-mix(
    in srgb,
    var(--el-color-primary) 30%,
    var(--el-border-color)
  );
  stroke-width: 1.5;
}
</style>
