<script setup lang="ts">
import cytoscape from 'cytoscape'
import dagre from 'cytoscape-dagre'
import { storeToRefs } from 'pinia'
import { ref, shallowRef, watch, onUnmounted, computed, nextTick } from 'vue'

import { useUiStore } from '@/stores/uiStore'
import type { TraceNode } from '@/types/trace'
import {
  GROUPS_CONFIG,
  getGroupCytoscapeImage,
  getGroupShape,
  classifyNodeGroup,
} from '@/utils/nodeGroups'
import { useColorMode, useResizeObserver } from '@vueuse/core'

cytoscape.use(dagre)

const mode = useColorMode()
const isDark = computed(() => mode.value === 'dark')

const themeColors = computed(() =>
  isDark.value
    ? {
        nodeText: '#e5e5e5',
        textBackground: '#1e1e1e',
        textOutline: '#1e1e1e',
        edgeLine: '#aaa',
        selectedBorder: '#fff',
      }
    : {
        nodeText: '#333333',
        textBackground: '#f0f4ff',
        textOutline: '#ffffff',
        edgeLine: '#333333',
        selectedBorder: '#000',
      },
)

const containerRef = ref<HTMLElement | null>(null)
const cy = shallowRef<cytoscape.Core | null>(null)

const uiStore = useUiStore()
const { selectedTrace, selectedNodeId } = storeToRefs(uiStore)

const MIN_NODE_SIZE = 25
const MAX_NODE_SIZE = 80

function collectDurations(node: TraceNode): number[] {
  const durations: number[] = []

  if (node.duration !== undefined && node.duration > 0) {
    durations.push(node.duration)
  } else {
    // "Instant" node
    durations.push(1)
  }

  if (node.children?.length) {
    for (const child of node.children) {
      durations.push(...collectDurations(child as TraceNode))
    }
  }

  return durations
}

const durationRange = computed(() => {
  const parentNode = selectedTrace.value?.parentNode
  if (!parentNode) return { min: 1, max: 1 }
  const durations = collectDurations(parentNode)
  return {
    min: durations.length > 0 ? Math.min(...durations) : 1,
    max: durations.length > 0 ? Math.max(...durations) : 1,
  }
})

function calculateNodeSize(duration: number | undefined): number {
  const { min: minDuration, max: maxDuration } = durationRange.value

  if (duration === undefined || duration <= 0) {
    return MIN_NODE_SIZE
  }

  if (minDuration >= maxDuration) {
    return (MIN_NODE_SIZE + MAX_NODE_SIZE) / 2
  }

  const logMin = Math.log(Math.max(minDuration, 1))
  const logMax = Math.log(Math.max(maxDuration, 1))
  const logDuration = Math.log(Math.max(duration, 1))

  const normalized = (logDuration - logMin) / (logMax - logMin)

  return MIN_NODE_SIZE + normalized * (MAX_NODE_SIZE - MIN_NODE_SIZE)
}

/**
 * Builds Cytoscape elements from a pre-filtered node tree.
 * Children are already filtered to only include visible nodes.
 */
function buildElements(node: TraceNode, parentId?: string): cytoscape.ElementDefinition[] {
  const elements: cytoscape.ElementDefinition[] = []
  const nodeId = node.id || node.name
  const group = classifyNodeGroup(node)

  elements.push({
    data: {
      id: nodeId,
      label: node.name,
      group,
      duration: node.duration,
      type: node.type,
    },
  })

  if (parentId) {
    elements.push({
      data: {
        id: `${parentId}->${nodeId}`,
        source: parentId,
        target: nodeId,
      },
    })
  }

  // children are already filtered
  for (const child of node.children) {
    elements.push(...buildElements(child as TraceNode, nodeId))
  }

  return elements
}

const elements = computed(() => {
  const filteredRoot = uiStore.filteredTree
  if (!filteredRoot) return []

  return buildElements(filteredRoot)
})

function getCytoscapeStyles() {
  const colors = themeColors.value
  const dark = isDark.value

  return [
    {
      selector: 'node',
      style: {
        label: 'data(label)',
        color: colors.nodeText,
        'font-size': '11px',
        'font-family': '"SourceSansPro", sans-serif',
        'text-wrap': 'ellipsis',
        'text-margin-y': -12,
        'text-max-width': '150px',
        'text-valign': 'top',
        'text-halign': 'center',
        'text-background-color': colors.textBackground,
        'text-background-opacity': 0.85,
        'text-background-padding': '5px',
        'text-outline-color': colors.textOutline,
        'text-outline-width': 1,
        padding: '10px',
        width: (ele: cytoscape.NodeSingular) => calculateNodeSize(ele.data('duration')),
        height: (ele: cytoscape.NodeSingular) => calculateNodeSize(ele.data('duration')),
      },
    },
    ...Object.keys(GROUPS_CONFIG).map((groupName) => ({
      selector: `node[group="${groupName}"]`,
      style: {
        'background-image': getGroupCytoscapeImage(groupName, dark),
        shape: getGroupShape(groupName),
      } as cytoscape.Css.Node,
    })),
    {
      selector: 'node:selected',
      style: {
        'border-width': 3,
        'border-color': colors.selectedBorder,
      },
    },
    {
      selector: 'edge',
      style: {
        width: 1,
        'line-color': colors.edgeLine,
        'target-arrow-color': colors.edgeLine,
        'target-arrow-shape': 'chevron',
        'arrow-scale': 1,
        'curve-style': 'round-taxi',
        'taxi-direction': 'downward',
        'taxi-turn': 30,
        'taxi-turn-min-distance': 20,
      },
    },
  ]
}

function initCytoscape() {
  if (!containerRef.value || cy.value) return

  cy.value = cytoscape({
    container: containerRef.value,
    elements: elements.value,
    style: getCytoscapeStyles() as cytoscape.StylesheetStyle[],
    zoom: 1,
    minZoom: 0.15,
    layout: {
      name: 'dagre',
      acyclicer: undefined,
      rankDir: 'TB',
      align: 'UL',
      nodeSep: 120,
      rankSep: 70,
      padding: 30,
      fit: true,
      ready: () => cy.value?.fit(),
    } as cytoscape.LayoutOptions,
  })

  cy.value.on('tap', 'node', (event) => {
    const nodeId = event.target.id()
    uiStore.toggleSelectNode(nodeId)
  })
}

watch(selectedNodeId, (nodeId) => {
  if (!cy.value) return
  cy.value.nodes().unselect()
  if (nodeId) {
    cy.value.getElementById(nodeId).select()
  }
})

// watch for theme changes and update styles
watch(isDark, () => {
  if (!cy.value) return
  cy.value.style(getCytoscapeStyles() as cytoscape.StylesheetStyle[])
})

// watch for elements changes (triggered by activeGroupFilters via computed) and rebuild the graph
watch(
  elements,
  (newElements) => {
    if (!cy.value) return
    // remove all elements and add new filtered ones
    cy.value.elements().remove()
    cy.value.add(newElements)
    cy.value
      .layout({
        name: 'dagre',
        rankDir: 'TB',
        align: 'UL',
        nodeSep: 120,
        rankSep: 70,
        padding: 30,
        fit: true,
      } as cytoscape.LayoutOptions)
      .run()
  },
  { deep: true },
)

// initialize cytoscape when container has non-zero dimensions
useResizeObserver(containerRef, (entries) => {
  const entry = entries[0]
  if (!entry) return

  const { width, height } = entry.contentRect
  if (width > 0 && height > 0 && !cy.value) {
    nextTick(() => initCytoscape())
  }
})

onUnmounted(() => {
  cy.value?.destroy()
  cy.value = null
})
</script>

<template>
  <div ref="containerRef" class="w-full h-full" />
</template>
