import type { TraceNode } from '@/common/interface/trace'
import { isEmptyNode } from '@/utils/emptyNode'

import { classifyNodeGroup } from './nodeGroups'
import type {
  MinMaxDuration,
  NodeDetails,
  TimelineEdge,
  TimelineNode,
  TimelineNodeData
} from '@/common/interface/timeline'

export function traceToFullHierarchy(parentNode: TraceNode) {
  const minMax = { minDuration: Infinity, maxDuration: 0 }
  const { nodes, edges } = buildGraphFromTrace(parentNode, undefined, minMax)

  const childCountMap = computeChildCounts(nodes, edges)
  for (let n of nodes) {
    const cCount = childCountMap[n.data.id] || 0
    n.data.label = truncateLabel(n.data.label)
    if (cCount > 0) {
      n.data.expandable = true
      n.data.collapsed = false
    } else {
      n.data.expandable = false
    }
  }

  return {
    nodes,
    edges,
    minDuration: minMax.minDuration === Infinity ? 0 : minMax.minDuration,
    maxDuration: minMax.maxDuration
  }
}

function buildGraphFromTrace(
  node: TraceNode,
  parentId: string | undefined,
  minMax: MinMaxDuration
) {
  const nodes: TimelineNode[] = []
  const edges: TimelineEdge[] = []

  const nodeId = node.id
  const nodeData = createNodeData(node, nodeId, minMax)
  nodes.push({ data: nodeData })

  if (parentId) {
    edges.push({ data: { source: parentId, target: nodeId } })
  }

  let childNodes: TimelineNode[] = []
  let childEdges: TimelineEdge[] = []

  if (node.children && Array.isArray(node.children) && node.children.length > 0) {
    for (const c of node.children) {
      const { nodes: cnodes, edges: cedges } = buildGraphFromTrace(c, nodeId, minMax)
      childNodes.push(...cnodes)
      childEdges.push(...cedges)
    }
  }

  nodes.push(...childNodes)
  edges.push(...childEdges)

  if (childNodes.length > 0 || childEdges.length > 0) {
    nodeData._childElements = { nodes: childNodes, edges: childEdges }
  }

  return { nodes, edges }
}

function createNodeData(
  rawNode: TraceNode,
  id: string | undefined,
  minMax: MinMaxDuration
): TimelineNodeData {
  const label = rawNode.name || rawNode.type || 'Node'
  const group = classifyNodeGroup(rawNode)

  const rawDuration = rawNode.duration || 0
  if (rawDuration > minMax.maxDuration) {
    minMax.maxDuration = rawDuration
  }
  if (rawDuration > 0 && rawDuration < minMax.minDuration) {
    minMax.minDuration = rawDuration
  }

  const details = parseNodeDetails(rawNode)
  const emptyNode = isEmptyNode(rawNode)
  return {
    id,
    label,
    group,
    collapsed: true,
    expandable: true,
    details,
    fullLabel: label,
    rawDuration,
    begin: rawNode.begin,
    end: rawNode.end,
    _childElements: {
      nodes: [],
      edges: []
    },
    emptyNode: emptyNode
  }
}

function parseNodeDetails(node: TraceNode) {
  const details: NodeDetails = {
    inputs: [],
    outputs: [],
    attributes: []
  }

  if (node.inputs && Object.keys(node.inputs).length > 0) {
    details.inputs = objectToKeyValueArray(node.inputs)
  }
  if (node.outputs && Object.keys(node.outputs).length > 0) {
    details.outputs = objectToKeyValueArray(node.outputs)
  }
  if (node.attributes && Object.keys(node.attributes).length > 0) {
    details.attributes = objectToKeyValueArray(node.attributes)
  }

  if (node.duration !== undefined) {
    details.attributes = details.attributes
    details.attributes.push({ key: 'Duration', value: `${node.duration} ms` })
  }
  if (node.begin) {
    details.attributes = details.attributes
    details.attributes.push({ key: 'Begin', value: node.begin })
  }
  if (node.end) {
    details.attributes = details.attributes
    details.attributes.push({ key: 'End', value: node.end })
  }

  return details
}

function objectToKeyValueArray(obj: any): { key: string; value: string }[] {
  const arr = []
  for (const [k, v] of Object.entries(obj)) {
    arr.push({ key: k, value: JSON.stringify(v, null, 2) })
  }
  return arr
}

function computeChildCounts(nodes: TimelineNode[], edges: TimelineEdge[]) {
  const childCount: { [key: string]: number } = {}

  for (let n of nodes) {
    childCount[n.data.id] = 0
  }
  for (let e of edges) {
    childCount[e.data.source] = (childCount[e.data.source] || 0) + 1
  }
  return childCount
}

function truncateLabel(label: string, maxLength = 15) {
  if (label.length > maxLength) {
    return label.slice(0, maxLength) + '…'
  }
  return label
}

export function gatherTimedNodes(node: TraceNode, nodes: TraceNode[]) {
  if (node && node.begin) {
    nodes.push(node)
  } else if (node && !node.begin) {
    nodes.push({
      ...node,
      begin: node.timestamp
    })
  }

  if (node && node.children) {
    for (const c of node.children) {
      gatherTimedNodes(c, nodes)
    }
  }

  return nodes
}
