<template>
  <div class="px-4 flex flex-col h-full">
    <!-- Insights + Reset button -->
    <div class="flex justify-between pb-2">
      <div class="flex items-center">
        <Tooltip :delay-duration="500">
          <TooltipTrigger as-child>
            <Badge variant="secondary" class="cursor-help">
              <CalendarClock :size="16" />
              <span class="font-mono">{{ formattedEarliest }}</span>
            </Badge>
          </TooltipTrigger>
          <TooltipContent>Earliest start</TooltipContent>
        </Tooltip>

        <Tooltip :delay-duration="500">
          <TooltipTrigger as-child>
            <Badge variant="secondary" class="cursor-help ml-2">
              <Timer :size="16" />
              <span class="font-mono">{{ formattedDuration }}</span>
            </Badge>
          </TooltipTrigger>
          <TooltipContent>Trace duration</TooltipContent>
        </Tooltip>
      </div>
      <div class="">
        <Button
          variant="outline"
          @click="
            (redrawAndFit(), WT1iser.resetView(ViewMode.Timeline), tracesStore.resetNodeFilters())
          "
        >
          Reset view
        </Button>
      </div>
    </div>

    <!-- Timeline element -->
    <div ref="timelineEl" class="flex-1" />

    <!-- Tooltip -->
    <div v-if="tooltip.visible" :style="tooltipAnchorStyle as any">
      <div v-html="tooltip.html"></div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { CalendarClock, Timer } from 'lucide-vue-next'
import { Timeline, DataSet, type TimelineOptions } from 'vis-timeline/standalone'
import { ref, onMounted, watch, computed, nextTick } from 'vue'

import { type TimelineItem } from '@/common/interface/timeline'
import { type TraceNode } from '@/common/interface/trace'
import { ViewMode } from '@/common/views'
import { WT1iser } from '@/common/wt1'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { dateToDateSort, dateToTimeShort } from '@/utils/formatDate'
import { localizeNumber } from '@/utils/formatNumber'
import { classifyNodeGroup } from '@/utils/nodeGroups'
import { gatherTimedNodes } from '@/utils/traceToGraph'
import { useTracesStore } from '@/stores/traces'
import { isEmptyNode } from '@/utils/emptyNode'

const tracesStore = useTracesStore()
let timeline: Timeline | null = null

const timelineEl = ref<HTMLElement | null>(null)
const itemsDS = new DataSet<TimelineItem, 'id'>([])
const groupsDS = new DataSet([])
const loadingTimeline = ref(true)

const tooltip = ref({
  visible: false,
  html: '',
  x: 0,
  y: 0
})

const tooltipAnchorStyle = computed(() => ({
  position: 'fixed',
  left: `${tooltip.value.x}px`,
  top: `${tooltip.value.y}px`,
  width: '0px',
  height: '0px',
  display: 'flex'
}))

const allTimedNodes = computed(() => {
  return tracesStore.selectedTrace && tracesStore.selectedTrace.parentNode
    ? (gatherTimedNodes(tracesStore.selectedTrace.parentNode, []) as TraceNode[])
    : []
})

const itemsData = computed(() => {
  const sortedNodes = [...allTimedNodes.value].sort(
    (a, b) => new Date(a.begin || 0).getTime() - new Date(b.begin || 0).getTime()
  )

  return sortedNodes.map((node, index) => {
    const start = new Date(node.begin || 0)
    const end = node.end ? new Date(node.end) : start
    const duration = node.duration || 0
    const group = classifyNodeGroup(node)
    const emptyNode = isEmptyNode(node)
    const longDuration = duration > 50
    const className = longDuration
      ? emptyNode
        ? `node-range-item group-${group}-empty`
        : `node-range-item group-${group}`
      : emptyNode
        ? `node-point-item group-${group}-empty`
        : `node-point-item group-${group}`

    return {
      id: index,
      group: index,
      content: node.name || '(no name)',
      start,
      end: duration > 0 ? end : start,
      type: longDuration ? 'range' : 'point',
      className: className,
      data: {
        actualNodeId: node.id,
        name: node.name,
        startTime: start,
        endTime: end,
        duration,
        group
      },
      emptyNode: emptyNode
    }
  })
})

const earliestBegin = computed(() => {
  const times = allTimedNodes.value.map((n) => new Date(n.begin || 0).getTime())
  const valid = times.filter((t) => !isNaN(t))
  return valid.length ? Math.min(...valid) : 0
})

const totalDuration = computed(() => {
  const ends = allTimedNodes.value.map((n) => new Date(n.end || 0).getTime())
  const valid = ends.filter((t) => !isNaN(t))
  return valid.length ? Math.max(...valid) - earliestBegin.value : 0
})

const formattedEarliest = computed(() =>
  earliestBegin.value ? dateToDateSort(new Date(earliestBegin.value)) : 'N/A'
)

const formattedDuration = computed(() => {
  const ms = totalDuration.value
  const s = Number((ms / 1000).toFixed(2))

  return `${localizeNumber(ms)} ms (${localizeNumber(s)} s)`
})

onMounted(() => {
  if (!timelineEl.value) {
    loadingTimeline.value = false
    return
  }

  const earliest = earliestBegin.value
  const latest = earliest + totalDuration.value
  const buffer = 0.1 * totalDuration.value
  const options: TimelineOptions = {
    orientation: 'top',
    stack: false,
    autoResize: false,
    verticalScroll: true,
    horizontalScroll: true,
    height: '100%',
    min: isNaN(earliest) ? undefined : new Date(earliest - buffer),
    max: isNaN(latest) ? undefined : new Date(latest + buffer),
    groupOrder: (g1: any, g2: any) => g1.id - g2.id,
    order: (a: any, b: any) => new Date(a.start).getTime() - new Date(b.start).getTime(),
    zoomKey: 'shiftKey',
    showCurrentTime: true,
    moveable: true
  }

  timeline = new Timeline(timelineEl.value, itemsDS, groupsDS, options)

  timeline.on('changed', () => {
    loadingTimeline.value = false
  })

  timeline.on('itemover', (eventProps: { item: number | string; event: any }) => {
    const it = itemsDS.get(eventProps.item)
    if (it) showTooltip(eventProps.event, it)
  })

  timeline.on('itemout', hideTooltip)
  timeline.on('rangechange', hideTooltip)

  timeline.on('select', (props) => {
    if (!props.items || !props.items.length) return
    const selectedItem: any = itemsDS.get(props.items[0])
    if (selectedItem && !selectedItem.emptyNode) {
      tracesStore.selectedNodeId = selectedItem.data.actualNodeId
    }
  })

  refreshTimelineItems()
})

watch(
  () => tracesStore.selectedTrace,
  () => {
    refreshTimelineItems()
    redrawAndFit()
  },
  { deep: true }
)

watch(
  () => tracesStore.selectedNodeId,
  (selectedNodeId) => {
    if (!timeline) return
    const found: any = itemsDS.get().find((it: any) => it.data.actualNodeId === selectedNodeId)
    if (found) {
      timeline.setSelection([found.id])
    } else {
      timeline.setSelection([])
    }
  },
  { immediate: true }
)

function refreshTimelineItems() {
  if (!timeline) return
  loadingTimeline.value = true

  itemsDS.clear()
  groupsDS.clear()

  const newItems = itemsData.value
  itemsDS.add(newItems)
  const newGroups = newItems.map((it: any) => ({ id: it.id, content: '' }))
  groupsDS.add(newGroups as any)

  nextTick(() => {
    timeline?.redraw()

    const visContentEl = timelineEl.value?.querySelector('.vis-content') as HTMLElement
    const currentScrollTop = visContentEl?.scrollTop ?? 0

    timeline?.fit({ animation: false })

    if (visContentEl) {
      visContentEl.scrollTop = currentScrollTop
    }

    if (tracesStore.selectedNodeId) {
      const found: any = itemsDS
        .get()
        .find((it: any) => it.data.actualNodeId === tracesStore.selectedNodeId)
      if (found) {
        timeline?.setSelection([found.id])
      } else {
        timeline?.setSelection([])
      }
    }

    loadingTimeline.value = false
  })
}

function redrawAndFit() {
  if (!timeline) return

  nextTick(() => {
    const earliest = earliestBegin.value
    const latest = earliest + totalDuration.value
    const buffer = 0.1 * totalDuration.value
    const options: TimelineOptions = {
      min: isNaN(earliest) ? undefined : new Date(earliest - buffer),
      max: isNaN(latest) ? undefined : new Date(latest + buffer)
    }

    timeline?.redraw()
    timeline?.setOptions(options)

    const visContentEl = timelineEl.value?.querySelector('.vis-content') as HTMLElement
    const currentScrollTop = visContentEl?.scrollTop ?? 0

    timeline?.fit({ animation: false })

    if (visContentEl) {
      visContentEl.scrollTop = currentScrollTop
    }
  })
}

function showTooltip(domEvent: any, item: TimelineItem) {
  tooltip.value = {
    visible: true,
    x: domEvent.clientX + 10,
    y: domEvent.clientY + 10,
    html: buildTooltipHTML(item)
  }
}

function hideTooltip() {
  tooltip.value.visible = false
}

function buildTooltipHTML(item: TimelineItem) {
  const d = item.data

  if (!d) return '(no data)'

  const startStr = dateToTimeShort(d.startTime)
  const endStr = d.duration > 0 ? dateToTimeShort(d.endTime) : startStr

  let tooltipHtml = `
  <div class="p-2 px-3 bg-secondary text-xs rounded-sm">
    <span class="font-semibold">${d.name || '(no name)'}</span>
    <div class="flex justify-between text-sm mt-2">
      Start: <p class="font-mono text-xs relative top-[3.5px] ml-5">${startStr}</p>
    </div>
    <div class="flex justify-between text-sm">
      End: <p class="font-mono text-xs relative top-[3.5px] ml-5">${endStr}</p>
    </div>
    <div class="flex justify-between text-sm">
      Duration: <p class="font-mono text-xs relative top-[3.5px] ml-5">${d.duration} ms</p>
    </div>`
  if (item.emptyNode) {
    tooltipHtml += `<div class="flex justify-between text-sm mt-2">
      <span class="font-mono text-xs">
        <strong>No additional details available</strong>
      </span>
    </div>`
  }

  tooltipHtml += `</div>`

  return tooltipHtml
}
</script>

<style lang="scss">
.vis-timeline {
  border-radius: 4px;
  border-color: hsl(var(--border)) !important;
}

.vis-item {
  border-width: 0px !important;
}

.vis-item.vis-range {
  background-color: transparent;
  border-color: transparent;
}

.vis-item-visible-frame {
  position: relative;
  top: -5px;
}

.vis-item-overflow {
  background-color: transparent;
}

.node-range-item.group-agent .vis-item-visible-frame {
  background-color: var(--color-agent);
}

.node-range-item.group-guard .vis-item-visible-frame {
  background-color: var(--color-guard);
}

.node-range-item.group-tool .vis-item-visible-frame {
  background-color: var(--color-tool);
}

.node-range-item.group-llm .vis-item-visible-frame {
  background-color: var(--color-llm);
}

.node-range-item.group-event .vis-item-visible-frame {
  background-color: var(--color-event);
}

.node-range-item.group-span .vis-item-visible-frame {
  background-color: var(--color-span);
}

.node-point-item.vis-dot {
  border: 4px solid var(--color-stone-300) !important;
}

.node-point-item.vis-dot.group-agent {
  border: 4px solid var(--color-agent) !important;
}

.node-point-item.vis-dot.group-guard {
  border: 4px solid var(--color-guard) !important;
}

.node-point-item.vis-dot.group-tool {
  border: 4px solid var(--color-tool) !important;
}

.node-point-item.vis-dot.group-llm {
  border: 4px solid var(--color-llm) !important;
}

.node-point-item.vis-dot.group-event {
  border: 4px solid var(--color-event) !important;
}

.node-point-item.vis-dot.group-span {
  border: 4px solid var(--color-span) !important;
}

// empty
.node-range-item.group-agent-empty .vis-item-visible-frame {
  background-color: var(--color-agent-empty);
}

.node-range-item.group-guard-empty .vis-item-visible-frame {
  background-color: var(--color-guard-empty);
}

.node-range-item.group-tool-empty .vis-item-visible-frame {
  background-color: var(--color-tool-empty);
}

.node-range-item.group-llm-empty .vis-item-visible-frame {
  background-color: var(--color-llm-empty);
}

.node-range-item.group-event-empty .vis-item-visible-frame {
  background-color: var(--color-event-empty);
}

.node-range-item.group-span-empty .vis-item-visible-frame {
  background-color: var(--color-span-empty);
}

.node-point-item.vis-dot.group-agent-empty {
  border: 4px solid var(--color-agent-empty) !important;
}

.node-point-item.vis-dot.group-guard-empty {
  border: 4px solid var(--color-guard-empty) !important;
}

.node-point-item.vis-dot.group-tool-empty {
  border: 4px solid var(--color-tool-empty) !important;
}

.node-point-item.vis-dot.group-llm-empty {
  border: 4px solid var(--color-llm-empty) !important;
}

.node-point-item.vis-dot.group-event-empty {
  border: 4px solid var(--color-event-empty) !important;
}

.node-point-item.vis-dot.group-span-empty {
  border: 4px solid var(--color-span-empty) !important;
}

.vis-item.node-range-item {
  .vis-item-visible-frame {
    height: 4px;
    border-radius: 2px;
  }

  .vis-item-content {
    position: relative;
    top: -5px;
    left: 0;
    background: transparent !important;
    color: #333e48;
    padding: 2px 4px;
  }
}

.vis-item-content {
  font-size: 13px;
  font-weight: 500;
}

.vis-item.vis-range.vis-selected {
  background-color: #d6e1fe;
  border-color: transparent !important;
  border-radius: 2px;
}

.vis-item.vis-point.vis-selected {
  background-color: #d6e1fe !important;
  border-color: transparent !important;
}

.vis-panel {
  font-size: 12px;
  font-family: var(--font-mono);
}

.vis-panel.vis-center {
  border: 1px solid hsl(var(--border)) !important;
}

.vis-foreground .vis-group {
  border-bottom: 1px solid hsl(var(--border)) !important;
}

.vis-time-axis .vis-grid.vis-major {
  border-left: 2px solid hsl(var(--border)) !important;
}
</style>
