import { useEffect, useRef, useState, useCallback, forwardRef, useImperativeHandle } from 'react'
import BpmnViewer from 'bpmn-js/lib/Viewer'
import { activityColor } from '@/lib/colors'
import { formatCurrency, formatPercent, formatMinutes } from '@/lib/format'
import type { HeatmapMode } from '@/lib/colors'
import type { ActivitySimResult } from '@/domain/types'

interface TooltipData {
  visible: boolean
  x: number
  y: number
  activity: ActivitySimResult | null
}

interface HeatmapCanvasProps {
  xml: string
  perActivity: ActivitySimResult[]
  mode: HeatmapMode
  currency: string
  selectedId: string | null
  onActivityClick: (bpmnElementId: string) => void
  minHeight?: number
}

export interface HeatmapCanvasHandle {
  highlightElement: (bpmnElementId: string | null) => void
  captureImage: () => Promise<string | null>
}

function applyHeatmapToViewer(
  viewer: any,
  perActivity: ActivitySimResult[],
  mode: HeatmapMode
) {
  const elementRegistry = viewer.get('elementRegistry') as any
  if (!elementRegistry) return

  for (const act of perActivity) {
    const gfx: SVGElement | null = elementRegistry.getGraphics(act.bpmnElementId)
    if (!gfx) continue

    // activityColor devuelve NEUTRAL_HEATMAP_COLOR cuando no hay varianza significativa,
    // evitando una falsa diferenciación en procesos con costos uniformes.
    const color = activityColor(act, perActivity, mode)

    const shape = gfx.querySelector('.djs-visual rect') as SVGElement | null
    if (shape) {
      // Usar style.fill (inline CSS) en lugar de setAttribute: el CSS inline tiene
      // mayor especificidad que los estilos de bpmn-js, garantizando que html2canvas
      // capture el color real (getAttribute devuelve el atributo, pero el estilo
      // computado — que usa html2canvas — podría estar sobreescrito por CSS de bpmn-js).
      ;(shape as any).style.fill = color
      ;(shape as any).style.fillOpacity = '0.82'
      ;(shape as any).style.transition = 'fill 0.35s ease, fill-opacity 0.35s ease'
    }
  }
}


export const HeatmapCanvas = forwardRef<HeatmapCanvasHandle, HeatmapCanvasProps>(
  ({ xml, perActivity, mode, currency, selectedId, onActivityClick, minHeight = 500 }, ref) => {
    const containerRef = useRef<HTMLDivElement>(null)
    const viewerRef = useRef<any>(null)
    const [isImported, setIsImported] = useState(false)
    // Ref que espeja isImported — permite que captureImage() lea el valor live
    // desde dentro de una closure asíncrona (las closures capturan state por valor).
    const isImportedRef = useRef(false)
    const [tooltip, setTooltip] = useState<TooltipData>({ visible: false, x: 0, y: 0, activity: null })

    const activityMap = useRef(new Map<string, ActivitySimResult>())
    useEffect(() => {
      activityMap.current = new Map(perActivity.map((a) => [a.bpmnElementId, a]))
    }, [perActivity])

    // Exponer métodos para resaltar y capturar imagen (para el PDF)
    useImperativeHandle(ref, () => ({
      highlightElement: (bpmnElementId: string | null) => {
        const viewer = viewerRef.current
        if (!viewer) return
        const canvas = viewer.get('canvas') as any
        const reg = viewer.get('elementRegistry') as any
        reg.forEach((el: any) => canvas.removeMarker(el.id, 'bpmn-selected'))
        if (bpmnElementId) {
          try { canvas.addMarker(bpmnElementId, 'bpmn-selected') } catch { /* ignorar */ }
        }
      },
      captureImage: async () => {
        if (!containerRef.current) return null

        // Esperar a que bpmn-js termine de importar y renderizar (máx. 5 s).
        // isImportedRef.current se actualiza sincrónicamente en el .then() de importXML,
        // por lo que es visible aquí aunque esta closure se haya creado antes.
        if (!isImportedRef.current) {
          const TIMEOUT_MS = 5_000
          const POLL_MS = 100
          let elapsed = 0
          while (!isImportedRef.current && elapsed < TIMEOUT_MS) {
            await new Promise<void>((resolve) => setTimeout(resolve, POLL_MS))
            elapsed += POLL_MS
          }
          if (!isImportedRef.current) {
            throw new Error(
              'El diagrama aún no terminó de cargar. Esperá unos segundos y reintentá.'
            )
          }
        }

        // Esperar a que React corra el useEffect de applyHeatmapToViewer Y que la
        // transición CSS de los fills termine (fill 0.35s ease → necesitamos > 350ms).
        await new Promise<void>((resolve) => setTimeout(resolve, 500))

        const { default: html2canvas } = await import('html2canvas')
        try {
          const canvas = await html2canvas(containerRef.current, {
            scale: 1.5,
            useCORS: true,
            allowTaint: true,
            backgroundColor: '#f8fafc',
            logging: false,
          })
          return canvas.toDataURL('image/jpeg', 0.82)
        } catch (err) {
          console.warn('html2canvas error:', err)
          return null
        }
      },
    }))

    // Crear viewer
    useEffect(() => {
      if (!containerRef.current) return
      const viewer = new BpmnViewer({ container: containerRef.current })
      viewerRef.current = viewer
      return () => {
        viewer.destroy()
        viewerRef.current = null
        isImportedRef.current = false
        setIsImported(false)
      }
    }, [])

    // Importar XML.
    // bpmn-js 18 puede rechazar la promesa de importXML (error interno de canvas)
    // pero igualmente renderiza el diagrama parcialmente y dispara 'import.done'
    // en el eventBus antes de rechazar. Escuchamos 'import.done' para setear
    // isImported de forma confiable sin depender de que la promesa resuelva.
    useEffect(() => {
      const viewer = viewerRef.current
      if (!viewer || !xml) return
      isImportedRef.current = false
      setIsImported(false)

      const eventBus = viewer.get('eventBus') as any

      const onImportDone = () => {
        eventBus.off('import.done', onImportDone)
        const canvas = viewer.get('canvas') as any
        try { canvas.zoom('fit-viewport', 'auto') } catch { /* canvas puede no estar listo */ }
        isImportedRef.current = true
        setIsImported(true)
      }
      // Usamos .on + .off manual en lugar de .once para compatibilidad con mocks de test.
      eventBus.on('import.done', onImportDone)

      viewer.importXML(xml).catch((err: unknown) => {
        // Puede rechazar aunque el diagrama ya se haya renderizado (error de root layer en bpmn-js 18).
        // 'import.done' ya habrá disparado antes de este catch, por lo que isImported ya es true.
        console.warn('HeatmapCanvas importXML error (diagrama puede estar parcialmente renderizado):', err)
      })

      return () => {
        eventBus.off('import.done', onImportDone)
      }
    }, [xml])

    // Aplicar heatmap cuando el XML está listo o el modo cambia
    useEffect(() => {
      const viewer = viewerRef.current
      if (!isImported || !viewer || perActivity.length === 0) return
      applyHeatmapToViewer(viewer, perActivity, mode)
    }, [isImported, perActivity, mode])

    // Resaltar elemento seleccionado externamente
    useEffect(() => {
      const viewer = viewerRef.current
      if (!isImported || !viewer) return
      const canvas = viewer.get('canvas') as any
      const reg = viewer.get('elementRegistry') as any
      reg.forEach((el: any) => canvas.removeMarker(el.id, 'bpmn-selected'))
      if (selectedId) {
        try { canvas.addMarker(selectedId, 'bpmn-selected') } catch { /* ignorar */ }
      }
    }, [isImported, selectedId])

    // Event listeners de bpmn-js para click y hover
    useEffect(() => {
      const viewer = viewerRef.current
      if (!isImported || !viewer) return

      const eventBus = viewer.get('eventBus') as any
      const TASK_TYPES = new Set([
        'bpmn:Task', 'bpmn:UserTask', 'bpmn:ServiceTask', 'bpmn:ScriptTask',
        'bpmn:ManualTask', 'bpmn:BusinessRuleTask', 'bpmn:SubProcess',
      ])

      const onHover = (event: any) => {
        const el = event.element
        if (!TASK_TYPES.has(el?.type)) { setTooltip((t) => ({ ...t, visible: false })); return }
        const act = activityMap.current.get(el.id)
        if (!act) return
        setTooltip((t) => ({ ...t, visible: true, activity: act }))
      }
      const onOut = () => setTooltip((t) => ({ ...t, visible: false }))
      const onClick = (event: any) => {
        const el = event.element
        if (!el || !TASK_TYPES.has(el.type)) return
        onActivityClick(el.id)
      }

      eventBus.on('element.hover', onHover)
      eventBus.on('element.out', onOut)
      eventBus.on('element.click', onClick)
      return () => {
        eventBus.off('element.hover', onHover)
        eventBus.off('element.out', onOut)
        eventBus.off('element.click', onClick)
      }
    }, [isImported, onActivityClick])

    const onMouseMove = useCallback((e: React.MouseEvent) => {
      setTooltip((t) => ({ ...t, x: e.clientX, y: e.clientY }))
    }, [])

    return (
      <div className="relative" style={{ minHeight }}>
        {/* Canvas bpmn-js */}
        <div
          ref={containerRef}
          onMouseMove={onMouseMove}
          className="w-full bpmn-container"
          style={{ minHeight, height: '100%' }}
        />

        {/* Tooltip custom */}
        {tooltip.visible && tooltip.activity && (
          <div
            className="fixed z-50 pointer-events-none bg-white border border-slate-200 shadow-lg rounded-lg p-3 w-56"
            style={{ left: tooltip.x + 14, top: tooltip.y - 70 }}
          >
            <p className="text-xs font-semibold text-slate-800 mb-2 leading-tight">
              {tooltip.activity.activityName}
            </p>
            <div className="space-y-1 text-xs">
              <div className="flex justify-between">
                <span className="text-slate-500">Costo esperado</span>
                <span className="font-mono font-medium">{formatCurrency(tooltip.activity.expectedTotalCost, currency)}</span>
              </div>
              <div className="flex justify-between">
                <span className="text-slate-500">% del total</span>
                <span className="font-mono">{formatPercent(tooltip.activity.percentOfTotal)}</span>
              </div>
              <div className="flex justify-between">
                <span className="text-slate-500">Prob. ejecución</span>
                <span className="font-mono">{formatPercent(tooltip.activity.executionProbability * 100)}</span>
              </div>
              {tooltip.activity.executionTimeMinutes > 0 && (
                <div className="flex justify-between">
                  <span className="text-slate-500">Tiempo</span>
                  <span className="font-mono">{formatMinutes(tooltip.activity.executionTimeMinutes)}</span>
                </div>
              )}
            </div>
          </div>
        )}
      </div>
    )
  }
)
HeatmapCanvas.displayName = 'HeatmapCanvas'
