import { useCallback, useState } from 'react'
import {
  BaseEdge,
  EdgeLabelRenderer,
  getBezierPath,
  getSmoothStepPath,
  useReactFlow,
  useStore,
  type EdgeProps,
  type Edge,
} from '@xyflow/react'
import type { EdgeData, EdgeLineStyle, EdgeType, Waypoint } from '@/types'
import { useThemeStore } from '@/stores/themeStore'
import { useCanvasStore } from '@/stores/canvasStore'
import { THEMES } from '@/utils/themes'
import { MARKER_GEOMETRY, normalizeMarker, type NonNoneMarkerShape } from '@/utils/edgeMarkers'
import { clampWidthMult, dashArrayFor } from '@/utils/edgeLineStyle'
import { buildWaypointPath, getAddWaypointHandlePosition, getWaypointLabelPosition, snap45, snap45both } from './waypointUtils'

const VLAN_COLORS = ['#00d4ff', '#a855f7', '#39d353', '#ff6e00', '#e3b341', '#f85149']

function getVlanColor(vlanId?: number): string {
  if (!vlanId) return '#00d4ff'
  return VLAN_COLORS[vlanId % VLAN_COLORS.length]
}

/** Inner SVG element for an edge <marker>, drawn in a 0..10 viewBox. */
function markerInnerElement(shape: NonNoneMarkerShape, color: string): React.ReactElement {
  switch (shape) {
    case 'arrow':
      return <path d="M 0 0 L 10 5 L 0 10 z" fill={color} />
    case 'arrow-open':
      return <path d="M 1 1 L 9 5 L 1 9" fill="none" stroke={color} strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" />
    case 'circle':
      return <circle cx={5} cy={5} r={4} fill={color} />
    case 'diamond':
      return <path d="M 5 0.5 L 9.5 5 L 5 9.5 L 0.5 5 z" fill={color} />
    case 'square':
      return <rect x={1} y={1} width={8} height={8} fill={color} />
  }
}

// ── Waypoint drag handle ─────────────────────────────────────────────────────

interface WaypointHandleProps {
  edgeId: string
  index: number
  waypoint: Waypoint
  waypoints: Waypoint[]
  color: string
  pathStyle?: string
  prevPoint: Waypoint
  nextPoint: Waypoint
}

function WaypointHandle({ edgeId, index, waypoint, waypoints, color, pathStyle, prevPoint, nextPoint }: WaypointHandleProps) {
  const { screenToFlowPosition } = useReactFlow()
  const updateEdge = useCanvasStore((s) => s.updateEdge)

  const handlePointerDown = useCallback((e: React.PointerEvent) => {
    e.stopPropagation()
    e.currentTarget.setPointerCapture(e.pointerId)
  }, [])

  const handlePointerMove = useCallback((e: React.PointerEvent) => {
    if (e.buttons !== 1) return
    let pos = screenToFlowPosition({ x: e.clientX, y: e.clientY })
    if (pathStyle === 'smooth') {
      // Find the intersection of 45°-rays from both adjacent points so that
      // ALL segments (prev→this and this→next) snap to 45° simultaneously.
      pos = snap45both(prevPoint, nextPoint, pos)
    }
    const next = [...waypoints]
    next[index] = pos
    updateEdge(edgeId, { waypoints: next })
  }, [screenToFlowPosition, waypoints, index, edgeId, updateEdge, pathStyle, prevPoint, nextPoint])

  const handlePointerUp = useCallback((e: React.PointerEvent) => {
    e.currentTarget.releasePointerCapture(e.pointerId)
  }, [])

  const handleDoubleClick = useCallback((e: React.MouseEvent) => {
    e.stopPropagation()
    updateEdge(edgeId, { waypoints: waypoints.filter((_, i) => i !== index) })
  }, [edgeId, waypoints, index, updateEdge])

  return (
    <div
      style={{
        position: 'absolute',
        transform: `translate(-50%, -50%) translate(${waypoint.x}px, ${waypoint.y}px)`,
        width: 10,
        height: 10,
        borderRadius: '50%',
        background: color,
        border: '2px solid #0d1117',
        cursor: 'grab',
        pointerEvents: 'all',
        zIndex: 10,
      }}
      onPointerDown={handlePointerDown}
      onPointerMove={handlePointerMove}
      onPointerUp={handlePointerUp}
      onDoubleClick={handleDoubleClick}
      title="Drag to move · Double-click to remove"
    />
  )
}

// ── Add waypoint handle (+ button at segment midpoints) ──────────────────────

interface AddWaypointHandleProps {
  edgeId: string
  insertIndex: number
  x: number
  y: number
  waypoints: Waypoint[]
  color: string
  pathStyle?: string
  prevPoint: Waypoint
}

function AddWaypointHandle({ edgeId, insertIndex, x, y, waypoints, color, pathStyle, prevPoint }: AddWaypointHandleProps) {
  const updateEdge = useCanvasStore((s) => s.updateEdge)

  const handleClick = useCallback((e: React.MouseEvent) => {
    e.stopPropagation()
    let pos = { x, y }
    if (pathStyle === 'smooth') pos = snap45(prevPoint, pos)
    const next = [...waypoints.slice(0, insertIndex), pos, ...waypoints.slice(insertIndex)]
    updateEdge(edgeId, { waypoints: next })
  }, [edgeId, insertIndex, x, y, waypoints, updateEdge, pathStyle, prevPoint])

  return (
    <div
      onClick={handleClick}
      style={{
        position: 'absolute',
        transform: `translate(-50%, -50%) translate(${x}px, ${y}px)`,
        width: 14,
        height: 14,
        borderRadius: '50%',
        background: '#0d1117',
        border: `1.5px solid ${color}`,
        color,
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        fontSize: 12,
        lineHeight: 1,
        cursor: 'crosshair',
        pointerEvents: 'all',
        zIndex: 9,
        opacity: 0.7,
      }}
      title="Click to add waypoint"
    >
      +
    </div>
  )
}

// ── Segment midpoints ────────────────────────────────────────────────────────

/**
 * Compute + handle positions for each path segment.
 * For smooth style: bias the first + handle to the source handle axis and the
 * last + handle to the target handle axis, so clicking always gives a clean
 * perpendicular exit/entry (no diagonal guesswork near the nodes).
 */
function segmentMidpoints(
  sourceX: number, sourceY: number,
  waypoints: Waypoint[],
  targetX: number, targetY: number,
  pathStyle?: string,
  sourcePosition?: string,
): { x: number; y: number; insertIndex: number }[] {
  const pts = [{ x: sourceX, y: sourceY }, ...waypoints, { x: targetX, y: targetY }]
  const isSmooth = pathStyle === 'smooth'

  return pts.slice(0, -1).map((a, i) => {
    const base = getAddWaypointHandlePosition(sourceX, sourceY, waypoints, targetX, targetY, i, pathStyle)
    let mx = base.x
    const my = base.y

    // For smooth style with no existing waypoints, bias the single + handle onto
    // the source handle axis so clicking it creates a perpendicular exit.
    // Only applies to bottom/top handles (vertical exits) and only when the edge
    // has no waypoints yet — once waypoints exist, all + handles stay at the
    // real segment midpoint so they remain visually on the edge.
    if (isSmooth && i === 0 && pts.length === 2) {
      const vertSrc = sourcePosition === 'bottom' || sourcePosition === 'top'
      if (vertSrc) mx = a.x  // same X as source → + sits directly below/above node
    }

    return { x: mx, y: my, insertIndex: i }
  })
}

// ── Endpoint dot (interactive reconnection handle pinned to handle) ──────────

interface EndpointDotProps {
  edgeId: string
  role: 'source' | 'target'
  x: number
  y: number
  position?: string
  color: string
  source: string
  target: string
  sourceHandle: string | null | undefined
  targetHandle: string | null | undefined
  onDrag: (pos: { x: number; y: number } | null) => void
}

/**
 * Interactive endpoint marker rendered above the node layer (via
 * EdgeLabelRenderer). On pointerup it inspects the element under the cursor
 * for a React Flow handle (`[data-handleid]`) and calls `reconnectEdge` with
 * the new endpoint. Drop on empty space leaves the edge unchanged.
 *
 * Handles are nudged 3px inward (toward the node) because React Flow's edge
 * endpoint coords sit at the outer edge of the handle box, not its center.
 */
function EndpointDot({ edgeId, role, x, y, position, color, source, target, sourceHandle, targetHandle, onDrag }: EndpointDotProps) {
  const reconnectEdge = useCanvasStore((s) => s.reconnectEdge)
  const { screenToFlowPosition } = useReactFlow()

  const offset = 3
  let dx = 0, dy = 0
  if (position === 'bottom') dy = -offset
  else if (position === 'top') dy = offset
  else if (position === 'left') dx = offset
  else if (position === 'right') dx = -offset

  const onPointerDown = useCallback((e: React.PointerEvent) => {
    e.stopPropagation()
    e.currentTarget.setPointerCapture(e.pointerId)
  }, [])

  const onPointerMove = useCallback((e: React.PointerEvent) => {
    if (e.buttons !== 1) return
    onDrag(screenToFlowPosition({ x: e.clientX, y: e.clientY }))
  }, [onDrag, screenToFlowPosition])

  const onPointerUp = useCallback((e: React.PointerEvent) => {
    e.currentTarget.releasePointerCapture(e.pointerId)
    // Find the topmost handle under cursor, skipping the dragged dot itself.
    const stack = document.elementsFromPoint(e.clientX, e.clientY)
    let handleEl: HTMLElement | null = null
    for (const node of stack) {
      const h = (node as HTMLElement).closest?.('[data-handleid]') as HTMLElement | null
      if (h) { handleEl = h; break }
    }
    onDrag(null)
    if (!handleEl) return  // dropped on empty space → keep edge unchanged
    const newHandleId = handleEl.getAttribute('data-handleid')
    const newNodeId = handleEl.getAttribute('data-nodeid')
    if (!newHandleId || !newNodeId) return
    if (role === 'source') {
      reconnectEdge(edgeId, { source: newNodeId, target, sourceHandle: newHandleId, targetHandle: targetHandle ?? null })
    } else {
      reconnectEdge(edgeId, { source, target: newNodeId, sourceHandle: sourceHandle ?? null, targetHandle: newHandleId })
    }
  }, [edgeId, role, source, target, sourceHandle, targetHandle, reconnectEdge, onDrag])

  return (
    <div
      onPointerDown={onPointerDown}
      onPointerMove={onPointerMove}
      onPointerUp={onPointerUp}
      style={{
        position: 'absolute',
        transform: `translate(-50%, -50%) translate(${x + dx}px, ${y + dy}px)`,
        width: 15,
        height: 15,
        borderRadius: '50%',
        background: color,
        border: '2px solid #0d1117',
        cursor: 'grab',
        pointerEvents: 'all',
        zIndex: 1000,
        touchAction: 'none',
      }}
      title="Drag to reconnect"
    />
  )
}

// ── Main edge component ──────────────────────────────────────────────────────

export function HomelableEdge({ id, source, target, sourceHandleId, targetHandleId, sourceX: rawSourceX, sourceY: rawSourceY, targetX: rawTargetX, targetY: rawTargetY, sourcePosition, targetPosition, data, selected }: EdgeProps<Edge<EdgeData>>) {
  const [drag, setDrag] = useState<{ role: 'source' | 'target'; x: number; y: number } | null>(null)

  const sourceX = drag?.role === 'source' ? drag.x : rawSourceX
  const sourceY = drag?.role === 'source' ? drag.y : rawSourceY
  const targetX = drag?.role === 'target' ? drag.x : rawTargetX
  const targetY = drag?.role === 'target' ? drag.y : rawTargetY

  const onSourceDrag = useCallback((pos: { x: number; y: number } | null) => {
    setDrag(pos ? { role: 'source', x: pos.x, y: pos.y } : null)
  }, [])
  const onTargetDrag = useCallback((pos: { x: number; y: number } | null) => {
    setDrag(pos ? { role: 'target', x: pos.x, y: pos.y } : null)
  }, [])

  const activeTheme = useThemeStore((s) => s.activeTheme)
  const theme = THEMES[activeTheme]
  const sourceType = useStore((s) => s.nodeLookup.get(source)?.type)
  const targetType = useStore((s) => s.nodeLookup.get(target)?.type)
  const isBidirectional = sourceType === 'proxmox' && targetType === 'proxmox'

  const waypoints: Waypoint[] = Array.isArray(data?.waypoints) && data.waypoints.length > 0
    ? data.waypoints as Waypoint[]
    : []

  const hasWaypoints = waypoints.length > 0

  const pathStyle = data?.path_style as string | undefined

  const pathArgs = { sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition }
  const [autoPath, labelX] = pathStyle === 'smooth'
    ? getSmoothStepPath({ ...pathArgs, borderRadius: 8 })
    : getBezierPath(pathArgs)

  const edgePath = hasWaypoints
    ? buildWaypointPath(sourceX, sourceY, waypoints, targetX, targetY, pathStyle)
    : autoPath

  const labelPosition = hasWaypoints
    ? getWaypointLabelPosition(sourceX, sourceY, waypoints, targetX, targetY, pathStyle)
    : { x: labelX, y: (sourceY + targetY) / 2 }

  const edgeType: EdgeType = data?.type ?? 'ethernet'
  const edgeColors = theme.colors.edgeColors

  const BASE_STYLES: Record<EdgeType, React.CSSProperties> = {
    ethernet: { stroke: edgeColors.ethernet, strokeWidth: 2 },
    wifi:     { stroke: edgeColors.wifi,     strokeWidth: 1.5, strokeDasharray: '6 3' },
    iot:      { stroke: edgeColors.iot,      strokeWidth: 1.5, strokeDasharray: '2 4' },
    vlan:     { strokeWidth: 2.5 },
    virtual:  { stroke: edgeColors.virtual,  strokeWidth: 1,   strokeDasharray: '4 4' },
    cluster:  { stroke: edgeColors.cluster,  strokeWidth: 2.5, strokeDasharray: '8 3' },
    fibre:    { stroke: edgeColors.fibre,    strokeWidth: 2.5, filter: `drop-shadow(0 0 3px ${edgeColors.fibre}aa)` },
    electrical: { stroke: edgeColors.electrical, strokeWidth: 2 },
  }

  const customColor = data?.custom_color as string | undefined
  const strokeColor: string = selected
    ? theme.colors.edgeSelectedColor
    : customColor
    ?? (edgeType === 'vlan' ? getVlanColor(data?.vlan_id as number | undefined) : (BASE_STYLES[edgeType].stroke as string ?? edgeColors.ethernet))

  // Per-edge line render overrides (custom style editor). Width multiplies the
  // type's base width; line style overrides the preset dash pattern. Both are
  // optional — unset leaves the type default from BASE_STYLES untouched.
  const baseWidth = (BASE_STYLES[edgeType].strokeWidth as number) ?? 2
  const widthMult = clampWidthMult(data?.width_mult as number | undefined)
  const resolvedWidth = baseWidth * widthMult
  const lineStyleOverride = data?.line_style as EdgeLineStyle | undefined

  const style: React.CSSProperties = {
    ...BASE_STYLES[edgeType],
    strokeWidth: resolvedWidth,
    ...(lineStyleOverride
      ? {
          strokeDasharray: dashArrayFor(lineStyleOverride, resolvedWidth),
          strokeLinecap: lineStyleOverride === 'dotted' ? 'round' : 'butt',
        }
      : {}),
    ...(edgeType === 'vlan' ? { stroke: getVlanColor(data?.vlan_id as number | undefined) } : {}),
    ...(customColor ? { stroke: customColor } : {}),
    ...(selected ? { stroke: theme.colors.edgeSelectedColor, filter: `drop-shadow(0 0 4px ${theme.colors.edgeSelectedColor}88)` } : {}),
  }

  const animMode: 'none' | 'snake' | 'flow' | 'basic' =
    data?.animated === true || data?.animated === 'snake' ? 'snake' :
    data?.animated === 'flow' ? 'flow' :
    data?.animated === 'basic' ? 'basic' : 'none'

  const animColor = customColor ?? (edgeType === 'vlan' ? getVlanColor(data?.vlan_id as number | undefined) : edgeColors[edgeType as keyof typeof edgeColors] as string)

  const midpoints = selected
    ? segmentMidpoints(sourceX, sourceY, waypoints, targetX, targetY, pathStyle, sourcePosition)
    : []

  // ── Endpoint markers ───────────────────────────────────────────────────────
  // Custom inline <marker> defs filled with the live strokeColor so they recolor
  // reactively (custom_color / vlan / selected). Sized from the stroke width.
  // Each end picks its own shape (arrow / arrow-open / circle / diamond / square)
  // independently; 'none' renders no marker.
  const startShape = normalizeMarker(data?.marker_start)
  const endShape = normalizeMarker(data?.marker_end)
  const hasMarkers = startShape !== 'none' || endShape !== 'none'
  const strokeW = (style.strokeWidth as number) ?? 2
  const markerSize = 6 + strokeW * 2
  const startMarkerId = `arrow-start-${id}`
  const endMarkerId = `arrow-end-${id}`

  const arrowMarker = (markerId: string, shape: NonNoneMarkerShape, orient: string) => {
    const geo = MARKER_GEOMETRY[shape]
    return (
      <marker
        id={markerId}
        viewBox="0 0 10 10"
        refX={geo.refX}
        refY={5}
        markerWidth={markerSize}
        markerHeight={markerSize}
        markerUnits="userSpaceOnUse"
        orient={geo.directional ? orient : '0'}
      >
        {markerInnerElement(shape, strokeColor)}
      </marker>
    )
  }

  return (
    <>
      {hasMarkers && (
        <defs>
          {startShape !== 'none' && arrowMarker(startMarkerId, startShape, 'auto-start-reverse')}
          {endShape !== 'none' && arrowMarker(endMarkerId, endShape, 'auto')}
        </defs>
      )}

      <BaseEdge
        id={id}
        path={edgePath}
        style={animMode === 'basic' ? { ...style, stroke: 'transparent' } : style}
        interactionWidth={16}
        markerStart={startShape !== 'none' ? `url(#${startMarkerId})` : undefined}
        markerEnd={endShape !== 'none' ? `url(#${endMarkerId})` : undefined}
      />

      {animMode === 'basic' && (
        <path
          d={edgePath}
          fill="none"
          stroke={strokeColor}
          strokeWidth={style.strokeWidth as number ?? 2}
          strokeDasharray="5"
          style={{
            pointerEvents: 'none',
            animation: 'homelable-basic-dash 0.5s linear infinite',
            animationDirection: sourceY <= targetY ? 'normal' : 'reverse',
          }}
        />
      )}

      {animMode === 'snake' && (
        <path
          d={edgePath}
          fill="none"
          stroke={animColor}
          strokeWidth={((style.strokeWidth as number ?? 2) + 1.5) * 2}
          strokeDasharray="20 10000"
          strokeLinecap="round"
          style={{
            pointerEvents: 'none',
            // CSS (not SMIL) so it pauses when the tab is hidden — see index.css.
            // Bidirectional yo-yos via `alternate` (10s each way = 20s round trip,
            // matching the old SMIL keyTimes); unidirectional loops in one direction.
            animation: `homelable-snake 10s linear infinite${isBidirectional ? ' alternate' : ''}`,
          }}
        />
      )}
      {animMode === 'flow' && (
        <path
          d={edgePath}
          fill="none"
          stroke={animColor}
          strokeWidth={Math.max(3, (style.strokeWidth as number ?? 2) * 1.8)}
          strokeDasharray="6 12"
          strokeLinecap="round"
          strokeOpacity={0.85}
          style={{
            pointerEvents: 'none',
            animation: 'homelable-flow 1.2s linear infinite',
          }}
        />
      )}

      <EdgeLabelRenderer>
        {data?.label && (
          <div
            className="absolute pointer-events-none font-mono text-[10px] px-1.5 py-0.5 rounded whitespace-pre-line text-center"
            style={{
              transform: `translate(-50%, -50%) translate(${labelPosition.x}px, ${labelPosition.y}px)`,
              background: theme.colors.edgeLabelBackground,
              color:      theme.colors.edgeLabelColor,
              border:     `1px solid ${theme.colors.edgeLabelBorder}`,
            }}
          >
            {data.label as string}
          </div>
        )}

        {/* Endpoint dots — visual indicators for reconnection targets */}
        {selected && (
          <>
            <EndpointDot
              edgeId={id}
              role="source"
              x={sourceX}
              y={sourceY}
              position={sourcePosition}
              color={strokeColor}
              source={source}
              target={target}
              sourceHandle={sourceHandleId}
              targetHandle={targetHandleId}
              onDrag={onSourceDrag}
            />
            <EndpointDot
              edgeId={id}
              role="target"
              x={targetX}
              y={targetY}
              position={targetPosition}
              color={strokeColor}
              source={source}
              target={target}
              sourceHandle={sourceHandleId}
              targetHandle={targetHandleId}
              onDrag={onTargetDrag}
            />
          </>
        )}

        {/* Existing waypoint drag handles */}
        {selected && waypoints.map((wp, idx) => {
          const prevPoint = idx === 0 ? { x: sourceX, y: sourceY } : waypoints[idx - 1]
          const nextPoint = idx === waypoints.length - 1 ? { x: targetX, y: targetY } : waypoints[idx + 1]
          return (
            <WaypointHandle
              key={`wp-${idx}`}
              edgeId={id}
              index={idx}
              waypoint={wp}
              waypoints={waypoints}
              color={strokeColor}
              pathStyle={pathStyle}
              prevPoint={prevPoint}
              nextPoint={nextPoint}
            />
          )
        })}

        {/* + handles at segment midpoints to add new waypoints */}
        {selected && midpoints.map((mp) => {
          const prevPoint = mp.insertIndex === 0
            ? { x: sourceX, y: sourceY }
            : waypoints[mp.insertIndex - 1]
          return (
            <AddWaypointHandle
              key={`add-${mp.insertIndex}`}
              edgeId={id}
              insertIndex={mp.insertIndex}
              x={mp.x}
              y={mp.y}
              waypoints={waypoints}
              color={strokeColor}
              pathStyle={pathStyle}
              prevPoint={prevPoint}
            />
          )
        })}
      </EdgeLabelRenderer>
    </>
  )
}
