/**
 * Mermaid Flowchart → Whiteboard elements converter.
 * Parses Mermaid source directly (no SVG, no internal APIs).
 *
 * Supported:
 *   flowchart TD/LR/BT/RL
 *   Node shapes: A[id], A[label], A(label), A{label}, A((label)), A[[label]]
 *   Edges: --> |label|  ---  ==>
 *   Subgraphs: subgraph name ... end
 */

import type { ConvertedNode, ConvertedEdge, MermaidConversionResult, MermaidConvertOptions } from './mermaidToElements.types'

interface ParsedNode {
  id: string
  label: string
  shape: 'rectangle' | 'ellipse' | 'diamond' | 'circle' | 'subroutine'
}

interface ParsedEdge {
  start: string
  end: string
  label?: string
  style?: 'solid' | 'dotted' | 'dashed'
}

interface ParsedSubgraph {
  id: string
  label: string
  nodeIds: string[]
}

function detectShape(raw: string): ParsedNode['shape'] {
  if (raw.startsWith('((') && raw.endsWith('))')) return 'circle'
  if (raw.startsWith('(') && raw.endsWith(')')) return 'ellipse'
  if (raw.startsWith('{') && raw.endsWith('}')) return 'diamond'
  if (raw.startsWith('[[') && raw.endsWith(']]')) return 'subroutine'
  if (raw.startsWith('[(') && raw.endsWith(')]')) return 'subroutine'
  return 'rectangle'
}

function extractLabel(raw: string): string {
  const t = raw.trim()
  if (t.startsWith('[[') && t.endsWith(']]')) return t.slice(2, -2)
  if (t.startsWith('[(') && t.endsWith(')]')) return t.slice(2, -2)
  if (t.startsWith('((') && t.endsWith('))')) return t.slice(2, -2)
  if (t.startsWith('(') && t.endsWith(')')) return t.slice(1, -1)
  if (t.startsWith('[') && t.endsWith(']')) return t.slice(1, -1)
  if (t.startsWith('{') && t.endsWith('}')) return t.slice(1, -1)
  return t
}

function detectDirection(code: string): 'TD' | 'LR' | 'BT' | 'RL' {
  const m = code.match(/flowchart\s+(TD|LR|BT|RL)/i)
  return (m ? m[1].toUpperCase() : 'TD') as 'TD' | 'LR' | 'BT' | 'RL'
}

function parseFlowchartCode(code: string) {
  const direction = detectDirection(code)
  const nodes = new Map<string, ParsedNode>()
  const edges: ParsedEdge[] = []
  const subgraphs: ParsedSubgraph[] = []
  let inSubgraph: ParsedSubgraph | null = null

  for (const line of code.split('\n')) {
    const trimmed = line.trim()
    if (!trimmed || trimmed.toLowerCase().startsWith('flowchart ')) continue

    const sgMatch = trimmed.match(/^subgraph\s+(\S+)(?:\s+"([^"]+)")?\s*$/i)
    if (sgMatch) {
      inSubgraph = { id: sgMatch[1], label: sgMatch[2] ?? sgMatch[1], nodeIds: [] }
      continue
    }
    if (trimmed.toLowerCase() === 'end') {
      if (inSubgraph) { subgraphs.push(inSubgraph); inSubgraph = null }
      continue
    }

    // Strip shaped node content so plainIdRe cannot match words inside brackets.
    // e.g. "A[Go shopping]" → "A " so "Go" and "shopping" are not matched as nodes.
    const strippedLine = trimmed
      .replace(/[A-Za-z_][A-Za-z0-9_]*(?:\[\(.+?\)\]|\[\[.+?\]\]|\(.+?\)|\[.+?\]|\{.+?\})/g, match => {
        const id = match.match(/^([A-Za-z_][A-Za-z0-9_]*)/)?.[1] ?? match
        return id + ' '
      })

    // Parse edges: match arrows, labels, and node IDs in order, then pair consecutive nodes.
    // Supports: --> ---> == ==> -.-> etc., chains like A --> B --> C, and edge labels |text|.
    const edgeTokenRe = /(--{1,3}>?|={2,3}>?|-(?:\.-){1,3}>?)|\|([^|]+)\||([A-Za-z_][A-Za-z0-9_]*(?:\[\(.+?\)\]|\[\[.+?\]\]|\(.+?\)|\[.+?\]|\{.+?\})?)/g
    let lastNode: string | null = null
    let lastArrow = ''
    let pendingLabel = ''
    let m2: RegExpExecArray | null
    while ((m2 = edgeTokenRe.exec(trimmed)) !== null) {
      if (m2[1]) { lastArrow = m2[1]; continue }
      if (m2[2]) { pendingLabel = m2[2]; continue }
      if (m2[3]) {
        const nodeId = m2[3].trim().match(/^([A-Za-z_][A-Za-z0-9_]*)/)?.[1] ?? m2[3].trim()
        if (lastNode) {
          if (!/^(subgraph|end)$/i.test(lastNode) && !/^(subgraph|end)$/i.test(nodeId)) {
            const label = pendingLabel || undefined
            let style: ParsedEdge['style'] = 'solid'
            if (lastArrow.includes('.')) style = 'dotted'
            edges.push({ start: lastNode, end: nodeId, label, style })
          }
          lastArrow = ''
          pendingLabel = ''
        }
        lastNode = nodeId
      }
    }

    // After stripping shaped node content, remaining ids on the line are plain node refs.
    // Only match ids that appear as standalone tokens (surrounded by spaces or line start/end).
    const plainIdRe = /(^|\s)([A-Za-z_][A-Za-z0-9_]*)(\s|$)/g
    let m: RegExpExecArray | null
    while ((m = plainIdRe.exec(strippedLine)) !== null) {
      const id = m[2]
      if (/^(subgraph|end|style|class|linkStyle|click)$/i.test(id)) continue
      if (!nodes.has(id)) {
        nodes.set(id, { id, label: id, shape: 'rectangle' })
        inSubgraph?.nodeIds.push(id)
      }
    }

    // Shaped nodes override any plain entry (richer shape info)
    const shapedRe = /([A-Za-z_][A-Za-z0-9_]*)(\[\(.+?\)\]|\[\[.+?\]\]|\(.+?\)|\[.+?\]|\{.+?\})/g
    while ((m = shapedRe.exec(trimmed)) !== null) {
      const id = m[1]
      const shapeRaw = m[2]
      nodes.set(id, { id, label: extractLabel(shapeRaw), shape: detectShape(shapeRaw) })
      if (inSubgraph && !inSubgraph.nodeIds.includes(id)) inSubgraph.nodeIds.push(id)
    }
  }

  return { direction, nodes: Array.from(nodes.values()), edges, subgraphs }
}

interface Pos { x: number; y: number; width: number; height: number }
const W = 160, H = 50, GX = 60, GY = 70

function autoLayout(
  nodes: ParsedNode[],
  edges: ParsedEdge[],
  subgraphs: ParsedSubgraph[],
  dir: 'TD' | 'LR' | 'BT' | 'RL',
  offsetX: number,
  offsetY: number,
): Map<string, Pos> {
  const positions = new Map<string, Pos>()
  const succ = new Map<string, string[]>(), pred = new Map<string, string[]>()
  for (const n of nodes) { succ.set(n.id, []); pred.set(n.id, []) }
  for (const e of edges) { succ.get(e.start)?.push(e.end); pred.get(e.end)?.push(e.start) }

  const levels = new Map<string, number>(), visited = new Set<string>()
  const roots = nodes.filter(n => !(pred.get(n.id)?.length))

  function assign(ids: string[], lv: number) {
    for (const id of ids) {
      if (visited.has(id)) { levels.set(id, Math.max(levels.get(id)!, lv)); continue }
      levels.set(id, lv); visited.add(id)
      assign(succ.get(id) ?? [], lv + 1)
    }
  }
  roots.forEach(r => assign([r.id], 0))
  nodes.forEach(n => { if (!levels.has(n.id)) levels.set(n.id, 0) })

  const byLevel = new Map<number, string[]>()
  for (const [id, lv] of levels) {
    if (!byLevel.has(lv)) byLevel.set(lv, [])
    byLevel.get(lv)!.push(id)
  }

  const maxLv = Math.max(0, ...Array.from(byLevel.keys()))
  const horiz = dir === 'LR' || dir === 'RL'
  const rev = dir === 'BT' || dir === 'RL'

  for (let lv = 0; lv <= maxLv; lv++) {
    const ids = byLevel.get(lv) ?? []
    for (let i = 0; i < ids.length; i++) {
      const id = ids[i]
      const x = horiz
        ? (rev ? maxLv - lv : lv) * (W + GX) + offsetX
        : i * (W + GX) + offsetX
      const y = horiz
        ? i * (H + GY) + offsetY
        : (rev ? maxLv - lv : lv) * (H + GY) + offsetY
      positions.set(id, { x, y, width: W, height: H })
    }
  }

  for (const sg of subgraphs) {
    const poses = sg.nodeIds.map(id => positions.get(id)).filter((p): p is Pos => !!p)
    if (poses.length) {
      const minX = Math.min(...poses.map(p => p.x)) - 12
      const minY = Math.min(...poses.map(p => p.y)) - 24
      const maxX = Math.max(...poses.map(p => p.x + p.width)) + 12
      const maxY = Math.max(...poses.map(p => p.y + p.height)) + 12
      positions.set(sg.id, { x: minX, y: minY, width: maxX - minX, height: maxY - minY })
    }
  }

  return positions
}

function inferColor(node: ParsedNode): { fill?: string; stroke?: string } {
  const l = node.label.toLowerCase()
  const map: Record<string, { fill: string; stroke: string }> = {
    start: { fill: '#e6ffed', stroke: '#2ea44f' },
    end: { fill: '#ffeef0', stroke: '#cb2431' },
    begin: { fill: '#e6ffed', stroke: '#2ea44f' },
    finish: { fill: '#ffeef0', stroke: '#cb2431' },
    done: { fill: '#e6ffed', stroke: '#2ea44f' },
    yes: { fill: '#e6ffed', stroke: '#2ea44f' },
    no: { fill: '#ffeef0', stroke: '#cb2431' },
    error: { fill: '#ffeef0', stroke: '#cb2431' },
    success: { fill: '#e6ffed', stroke: '#2ea44f' },
    failure: { fill: '#ffeef0', stroke: '#cb2431' },
  }
  for (const [k, v] of Object.entries(map)) {
    if (l === k || l.includes(k)) return { fill: v.fill, stroke: v.stroke }
  }
  // Default: white fill, neutral stroke
  return { fill: '#ffffff', stroke: '#334155' }
}

export async function mermaidToElements(
  code: string,
  options: MermaidConvertOptions = {},
): Promise<MermaidConversionResult | null> {
  try {
    const { offsetX = 0, offsetY = 0 } = options
    const { direction, nodes, edges, subgraphs } = parseFlowchartCode(code)
    if (!nodes.length) return null

    const pos = autoLayout(nodes, edges, subgraphs, direction, offsetX, offsetY)

    const convertedNodes: ConvertedNode[] = nodes.map(n => {
      const p = pos.get(n.id)!
      const color = inferColor(n)
      return {
        type: n.shape,
        id: n.id,
        x: p.x,
        y: p.y,
        width: p.width,
        height: p.height,
        text: n.label,
        fill: color.fill,
        stroke: color.stroke,
      }
    })

    // Group edges by source / target for even distribution along node edges.
    const bySrc = new Map<string, number[]>()
    const byDst = new Map<string, number[]>()
    edges.forEach((e, idx) => {
      if (!bySrc.has(e.start)) bySrc.set(e.start, [])
      bySrc.get(e.start)!.push(idx)
      if (!byDst.has(e.end)) byDst.set(e.end, [])
      byDst.get(e.end)!.push(idx)
    })

    // Orthogonal edge routing: resolve anchor side, distribute evenly,
    // then add intermediate waypoints so all lines exit perpendicular to the
    // source edge and enter perpendicular to the target edge.
    const convertedEdges: ConvertedEdge[] = edges.map((e, edgeIdx) => {
      const sp = pos.get(e.start), ep = pos.get(e.end)
      if (!sp || !ep) {
        return { startId: e.start, endId: e.end, label: e.label, points: [{ x: 0, y: 0 }, { x: 100, y: 50 }], startPoint: { x: 0, y: 0 }, endPoint: { x: 100, y: 50 }, strokeStyle: e.style ?? 'solid' }
      }

      // Which side of each node does this edge use?
      const anchorSide = (n: Pos, other: Pos) => {
        const dx = (other.x + other.width / 2) - (n.x + n.width / 2)
        const dy = (other.y + n.height / 2) - (n.y + n.height / 2)
        if (Math.abs(dx) >= Math.abs(dy)) {
          return dx >= 0 ? 'right' : 'left'
        }
        return dy >= 0 ? 'bottom' : 'top'
      }

      const startSide = anchorSide(sp, ep)
      const endSide = anchorSide(ep, sp)

      // Position along each side (1 … total, evenly spaced).
      const srcIndices = bySrc.get(e.start)!
      const dstIndices = byDst.get(e.end)!
      const srcPos = srcIndices.indexOf(edgeIdx)
      const dstPos = dstIndices.indexOf(edgeIdx)
      const srcTotal = srcIndices.length
      const dstTotal = dstIndices.length

      const alongSide = (p: Pos, side: 'right' | 'left' | 'bottom' | 'top', idx: number, total: number) => {
        if (total === 1) {
          return side === 'right'  ? { x: p.x + p.width, y: p.y + p.height / 2 }
               : side === 'left'   ? { x: p.x,             y: p.y + p.height / 2 }
               : side === 'bottom' ? { x: p.x + p.width / 2, y: p.y + p.height }
               :                    { x: p.x + p.width / 2, y: p.y }
        }
        const t = (idx + 1) / (total + 1)
        if (side === 'right' || side === 'left') {
          return { x: side === 'right' ? p.x + p.width : p.x, y: p.y + p.height * t }
        } else {
          return { x: p.x + p.width * t, y: side === 'bottom' ? p.y + p.height : p.y }
        }
      }

      const sp2 = alongSide(sp, startSide, srcPos, srcTotal)
      const ep2 = alongSide(ep, endSide, dstPos, dstTotal)

      // Orthogonal waypoints: leave the source perpendicular to the edge,
      // then (optionally) bend, then enter the target perpendicular.
      // If the exit and entry faces are the same (e.g. bottom→bottom),
      // add a vertical midpoint. Otherwise route around with up to 2 bends.
      const gap = 10
      const waypoints: { x: number; y: number }[] = []
      const startX = sp2.x, startY = sp2.y
      const endX = ep2.x, endY = ep2.y

      const exitIsRight = startSide === 'right'
      const exitIsLeft  = startSide === 'left'
      const exitIsBottom = startSide === 'bottom'
      const exitIsTop    = startSide === 'top'
      const entryIsRight = endSide === 'right'
      const entryIsLeft  = endSide === 'left'
      const entryIsBottom = endSide === 'bottom'
      const entryIsTop    = endSide === 'top'

      if (exitIsRight || exitIsLeft || exitIsBottom || exitIsTop) {
        if (exitIsRight && entryIsBottom) {
          const midX = startX + gap
          waypoints.push({ x: midX, y: startY }, { x: midX, y: endY })
        } else if (exitIsRight && entryIsTop) {
          const midX = startX + gap
          waypoints.push({ x: midX, y: startY }, { x: midX, y: endY })
        } else if (exitIsRight && entryIsRight) {
          const midX = Math.max(startX, endX) + gap
          const midY = (startY + endY) / 2
          waypoints.push({ x: midX, y: startY }, { x: midX, y: midY }, { x: midX, y: endY })
        } else if (exitIsRight && entryIsLeft) {
          const midX = (startX + endX) / 2
          waypoints.push({ x: midX, y: startY }, { x: midX, y: endY })
        } else if (exitIsBottom && entryIsRight) {
          const midY = startY + gap
          waypoints.push({ x: startX, y: midY }, { x: endX, y: midY })
        } else if (exitIsBottom && entryIsLeft) {
          const midY = startY + gap
          waypoints.push({ x: startX, y: midY }, { x: endX, y: midY })
        } else if (exitIsBottom && entryIsBottom) {
          const midY = Math.max(startY, endY) + gap
          const midX = (startX + endX) / 2
          waypoints.push({ x: startX, y: midY }, { x: midX, y: midY }, { x: endX, y: midY })
        } else if (exitIsBottom && entryIsTop) {
          const midY = (startY + endY) / 2
          waypoints.push({ x: startX, y: midY }, { x: endX, y: midY })
        } else if (exitIsLeft && entryIsBottom) {
          const midX = startX - gap
          waypoints.push({ x: midX, y: startY }, { x: midX, y: endY })
        } else if (exitIsLeft && entryIsTop) {
          const midX = startX - gap
          waypoints.push({ x: midX, y: startY }, { x: midX, y: endY })
        } else if (exitIsLeft && entryIsLeft) {
          const midX = Math.min(startX, endX) - gap
          const midY = (startY + endY) / 2
          waypoints.push({ x: midX, y: startY }, { x: midX, y: midY }, { x: midX, y: endY })
        } else if (exitIsLeft && entryIsRight) {
          const midX = (startX + endX) / 2
          waypoints.push({ x: midX, y: startY }, { x: midX, y: endY })
        } else if (exitIsTop && entryIsRight) {
          const midY = startY - gap
          waypoints.push({ x: startX, y: midY }, { x: endX, y: midY })
        } else if (exitIsTop && entryIsLeft) {
          const midY = startY - gap
          waypoints.push({ x: startX, y: midY }, { x: endX, y: midY })
        } else if (exitIsTop && entryIsTop) {
          const midY = Math.min(startY, endY) - gap
          const midX = (startX + endX) / 2
          waypoints.push({ x: startX, y: midY }, { x: midX, y: midY }, { x: endX, y: midY })
        } else if (exitIsTop && entryIsBottom) {
          const midY = (startY + endY) / 2
          waypoints.push({ x: startX, y: midY }, { x: endX, y: midY })
        }
      }

      return {
        startId: e.start,
        endId: e.end,
        label: e.label,
        points: [sp2, ...waypoints, ep2],
        startPoint: sp2,
        endPoint: ep2,
        strokeStyle: e.style ?? 'solid',
      }
    })

    return { nodes: convertedNodes, edges: convertedEdges, bounds: { x: offsetX, y: offsetY, width: 0, height: 0 } }
  } catch (err) {
    console.warn('[mermaidToElements]', err)
    return null
  }
}
