import { useCallback, useEffect, useRef, useState } from 'react'
import { createLogger } from '@sim/logger'
import { useQueryClient } from '@tanstack/react-query'
import { v4 as uuidv4 } from 'uuid'
import { useShallow } from 'zustand/react/shallow'
import { buildTraceSpans } from '@/lib/logs/execution/trace-spans/trace-spans'
import { processStreamingBlockLogs } from '@/lib/tokenization'
import {
  extractTriggerMockPayload,
  selectBestTrigger,
  triggerNeedsMockPayload,
} from '@/lib/workflows/triggers/trigger-utils'
import {
  resolveStartCandidates,
  StartBlockPath,
  TriggerUtils,
} from '@/lib/workflows/triggers/triggers'
import { useCurrentWorkflow } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-current-workflow'
import {
  addHttpErrorConsoleEntry,
  type BlockEventHandlerConfig,
  createBlockEventHandlers,
  addExecutionErrorConsoleEntry as sharedAddExecutionErrorConsoleEntry,
  handleExecutionCancelledConsole as sharedHandleExecutionCancelledConsole,
  handleExecutionErrorConsole as sharedHandleExecutionErrorConsole,
} from '@/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils'
import { getBlock } from '@/blocks'
import type { SerializableExecutionState } from '@/executor/execution/types'
import type { BlockLog, BlockState, ExecutionResult, StreamingExecution } from '@/executor/types'
import { hasExecutionResult } from '@/executor/utils/errors'
import { coerceValue } from '@/executor/utils/start-block'
import { subscriptionKeys } from '@/hooks/queries/subscription'
import { useExecutionStream } from '@/hooks/use-execution-stream'
import { WorkflowValidationError } from '@/serializer'
import { useCurrentWorkflowExecution, useExecutionStore } from '@/stores/execution'
import { useNotificationStore } from '@/stores/notifications'
import { useVariablesStore } from '@/stores/panel'
import { useEnvironmentStore } from '@/stores/settings/environment'
import {
  clearExecutionPointer,
  consolePersistence,
  loadExecutionPointer,
  saveExecutionPointer,
  useTerminalConsoleStore,
} from '@/stores/terminal'
import { useWorkflowDiffStore } from '@/stores/workflow-diff'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { mergeSubblockState } from '@/stores/workflows/utils'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'

const logger = createLogger('useWorkflowExecution')

/**
 * Module-level Set tracking which workflows have an active reconnection effect.
 * Prevents multiple hook instances (from different components) from starting
 * concurrent reconnection streams for the same workflow during the same mount cycle.
 */
const activeReconnections = new Set<string>()

interface DebugValidationResult {
  isValid: boolean
  error?: string
}

const WORKFLOW_EXECUTION_FAILURE_MESSAGE = 'Workflow execution failed'

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === 'object' && value !== null
}

function sanitizeMessage(value: unknown): string | undefined {
  if (typeof value !== 'string') return undefined
  const trimmed = value.trim()
  if (!trimmed || trimmed === 'undefined (undefined)') return undefined
  return trimmed
}

function normalizeErrorMessage(error: unknown): string {
  if (error instanceof Error) {
    const message = sanitizeMessage(error.message)
    if (message) return message
  } else if (typeof error === 'string') {
    const message = sanitizeMessage(error)
    if (message) return message
  }

  if (isRecord(error)) {
    const directMessage = sanitizeMessage(error.message)
    if (directMessage) return directMessage

    const nestedError = error.error
    if (isRecord(nestedError)) {
      const nestedMessage = sanitizeMessage(nestedError.message)
      if (nestedMessage) return nestedMessage
    } else {
      const nestedMessage = sanitizeMessage(nestedError)
      if (nestedMessage) return nestedMessage
    }
  }

  return WORKFLOW_EXECUTION_FAILURE_MESSAGE
}

export function useWorkflowExecution() {
  const queryClient = useQueryClient()
  const currentWorkflow = useCurrentWorkflow()
  const { activeWorkflowId, workflows } = useWorkflowRegistry(
    useShallow((s) => ({ activeWorkflowId: s.activeWorkflowId, workflows: s.workflows }))
  )
  const { toggleConsole, addConsole, updateConsole, cancelRunningEntries, clearExecutionEntries } =
    useTerminalConsoleStore(
      useShallow((s) => ({
        toggleConsole: s.toggleConsole,
        addConsole: s.addConsole,
        updateConsole: s.updateConsole,
        cancelRunningEntries: s.cancelRunningEntries,
        clearExecutionEntries: s.clearExecutionEntries,
      }))
    )
  const hasHydrated = useTerminalConsoleStore((s) => s._hasHydrated)
  const getAllVariables = useEnvironmentStore((s) => s.getAllVariables)
  const { getVariablesByWorkflowId, variables } = useVariablesStore(
    useShallow((s) => ({
      getVariablesByWorkflowId: s.getVariablesByWorkflowId,
      variables: s.variables,
    }))
  )
  const { isExecuting, isDebugging, pendingBlocks, executor, debugContext } =
    useCurrentWorkflowExecution()
  const setCurrentExecutionId = useExecutionStore((s) => s.setCurrentExecutionId)
  const getCurrentExecutionId = useExecutionStore((s) => s.getCurrentExecutionId)
  const rawSetIsExecuting = useExecutionStore((s) => s.setIsExecuting)

  const setIsExecuting = useCallback(
    (workflowId: string, executing: boolean) => {
      if (executing) {
        consolePersistence.executionStarted()
      } else {
        consolePersistence.executionEnded()
        clearExecutionPointer(workflowId)
      }
      rawSetIsExecuting(workflowId, executing)
    },
    [rawSetIsExecuting]
  )
  const setIsDebugging = useExecutionStore((s) => s.setIsDebugging)
  const setPendingBlocks = useExecutionStore((s) => s.setPendingBlocks)
  const setExecutor = useExecutionStore((s) => s.setExecutor)
  const setDebugContext = useExecutionStore((s) => s.setDebugContext)
  const setActiveBlocks = useExecutionStore((s) => s.setActiveBlocks)
  const setBlockRunStatus = useExecutionStore((s) => s.setBlockRunStatus)
  const setEdgeRunStatus = useExecutionStore((s) => s.setEdgeRunStatus)
  const setLastExecutionSnapshot = useExecutionStore((s) => s.setLastExecutionSnapshot)
  const getLastExecutionSnapshot = useExecutionStore((s) => s.getLastExecutionSnapshot)
  const clearLastExecutionSnapshot = useExecutionStore((s) => s.clearLastExecutionSnapshot)
  const [executionResult, setExecutionResult] = useState<ExecutionResult | null>(null)
  const executionStream = useExecutionStream()
  const currentChatExecutionIdRef = useRef<string | null>(null)
  const lastSeenEventIdRef = useRef<number>(0)
  const isViewingDiff = useWorkflowDiffStore((state) => state.isShowingDiff)
  const addNotification = useNotificationStore((state) => state.addNotification)

  /**
   * Validates debug state before performing debug operations
   */
  const validateDebugState = useCallback((): DebugValidationResult => {
    if (!executor || !debugContext || pendingBlocks.length === 0) {
      const missing = []
      if (!executor) missing.push('executor')
      if (!debugContext) missing.push('debugContext')
      if (pendingBlocks.length === 0) missing.push('pendingBlocks')

      return {
        isValid: false,
        error: `Cannot perform debug operation - missing: ${missing.join(', ')}. Try restarting debug mode.`,
      }
    }
    return { isValid: true }
  }, [executor, debugContext, pendingBlocks])

  /**
   * Resets all debug-related state
   */
  const resetDebugState = useCallback(() => {
    if (!activeWorkflowId) return
    setIsExecuting(activeWorkflowId, false)
    setIsDebugging(activeWorkflowId, false)
    setDebugContext(activeWorkflowId, null)
    setExecutor(activeWorkflowId, null)
    setPendingBlocks(activeWorkflowId, [])
    setActiveBlocks(activeWorkflowId, new Set())
  }, [
    activeWorkflowId,
    setIsExecuting,
    setIsDebugging,
    setDebugContext,
    setExecutor,
    setPendingBlocks,
    setActiveBlocks,
  ])

  const handleExecutionErrorConsole = useCallback(
    (params: {
      workflowId?: string
      executionId?: string
      error?: string
      durationMs?: number
      blockLogs: BlockLog[]
      isPreExecutionError?: boolean
    }) => {
      if (!params.workflowId) return
      sharedHandleExecutionErrorConsole(addConsole, cancelRunningEntries, {
        ...params,
        workflowId: params.workflowId,
      })
    },
    [addConsole, cancelRunningEntries]
  )

  const handleExecutionCancelledConsole = useCallback(
    (params: { workflowId?: string; executionId?: string; durationMs?: number }) => {
      if (!params.workflowId) return
      sharedHandleExecutionCancelledConsole(addConsole, cancelRunningEntries, {
        ...params,
        workflowId: params.workflowId,
      })
    },
    [addConsole, cancelRunningEntries]
  )

  const buildBlockEventHandlers = useCallback(
    (config: BlockEventHandlerConfig) =>
      createBlockEventHandlers(config, {
        addConsole,
        updateConsole,
        setActiveBlocks,
        setBlockRunStatus,
        setEdgeRunStatus,
      }),
    [addConsole, updateConsole, setActiveBlocks, setBlockRunStatus, setEdgeRunStatus]
  )

  /**
   * Checks if debug session is complete based on execution result
   */
  const isDebugSessionComplete = useCallback((result: ExecutionResult): boolean => {
    return (
      !result.metadata?.isDebugSession ||
      !result.metadata.pendingBlocks ||
      result.metadata.pendingBlocks.length === 0
    )
  }, [])

  /**
   * Handles debug session completion
   */
  const handleDebugSessionComplete = useCallback(
    async (result: ExecutionResult) => {
      logger.info('Debug session complete')
      setExecutionResult(result)

      // Persist logs
      await persistLogs(uuidv4(), result)

      // Reset debug state
      resetDebugState()
    },
    [activeWorkflowId, resetDebugState]
  )

  /**
   * Handles debug session continuation
   */
  const handleDebugSessionContinuation = useCallback(
    (result: ExecutionResult) => {
      if (!activeWorkflowId) return
      logger.info('Debug step completed, next blocks pending', {
        nextPendingBlocks: result.metadata?.pendingBlocks?.length || 0,
      })

      // Update debug context and pending blocks
      if (result.metadata?.context) {
        setDebugContext(activeWorkflowId, result.metadata.context)
      }
      if (result.metadata?.pendingBlocks) {
        setPendingBlocks(activeWorkflowId, result.metadata.pendingBlocks)
      }
    },
    [activeWorkflowId, setDebugContext, setPendingBlocks]
  )

  /**
   * Handles debug execution errors
   */
  const handleDebugExecutionError = useCallback(
    async (error: any, operation: string) => {
      logger.error(`Debug ${operation} Error:`, error)

      const errorMessage = error instanceof Error ? error.message : String(error)
      const errorResult = {
        success: false,
        output: {},
        error: errorMessage,
        logs: debugContext?.blockLogs || [],
      }

      setExecutionResult(errorResult)

      // Persist logs
      await persistLogs(uuidv4(), errorResult)

      // Reset debug state
      resetDebugState()
    },
    [debugContext, activeWorkflowId, resetDebugState]
  )

  const persistLogs = async (
    executionId: string,
    result: ExecutionResult,
    streamContent?: string
  ) => {
    try {
      // Build trace spans from execution logs
      const { traceSpans, totalDuration } = buildTraceSpans(result)

      // Add trace spans to the execution result
      const enrichedResult = {
        ...result,
        traceSpans,
        totalDuration,
      }

      // If this was a streaming response and we have the final content, update it
      if (streamContent && result.output && typeof streamContent === 'string') {
        // Update the content with the final streaming content
        enrichedResult.output.content = streamContent

        // Also update any block logs to include the content where appropriate
        if (enrichedResult.logs) {
          // Get the streaming block ID from metadata if available
          const streamingBlockId = (result.metadata as any)?.streamingBlockId || null

          for (const log of enrichedResult.logs) {
            // Only update the specific LLM block (agent/router) that was streamed
            const isStreamingBlock = streamingBlockId && log.blockId === streamingBlockId
            if (
              isStreamingBlock &&
              (log.blockType === 'agent' || log.blockType === 'router') &&
              log.output
            )
              log.output.content = streamContent
          }
        }
      }

      const response = await fetch(`/api/workflows/${activeWorkflowId}/log`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          executionId,
          result: enrichedResult,
        }),
      })

      if (!response.ok) {
        throw new Error('Failed to persist logs')
      }

      return executionId
    } catch (error) {
      logger.error('Error persisting logs:', error)
      return executionId
    }
  }

  const handleRunWorkflow = useCallback(
    async (workflowInput?: any, enableDebug = false) => {
      if (!activeWorkflowId) return

      // Sandbox exercises have no real workflow — signal the SandboxCanvasProvider
      // to run mock execution by setting isExecuting, then bail out immediately.
      if (workflows[activeWorkflowId]?.isSandbox) {
        setIsExecuting(activeWorkflowId, true)
        return
      }

      // Get workspaceId from workflow metadata
      const workspaceId = workflows[activeWorkflowId]?.workspaceId

      if (!workspaceId) {
        logger.error('Cannot execute workflow without workspaceId')
        return
      }

      // Reset execution result and set execution state
      setExecutionResult(null)
      setIsExecuting(activeWorkflowId, true)

      // Set debug mode only if explicitly requested
      if (enableDebug) {
        setIsDebugging(activeWorkflowId, true)
      }

      // Determine if this is a chat execution
      const isChatExecution =
        workflowInput && typeof workflowInput === 'object' && 'input' in workflowInput

      // For chat executions, we'll use a streaming approach
      if (isChatExecution) {
        let isCancelled = false
        const executionId = uuidv4()
        currentChatExecutionIdRef.current = executionId
        const stream = new ReadableStream({
          async start(controller) {
            const { encodeSSE } = await import('@/lib/core/utils/sse')
            const streamedChunks = new Map<string, string[]>()
            const streamReadingPromises: Promise<void>[] = []

            const safeEnqueue = (data: Uint8Array) => {
              if (!isCancelled) {
                try {
                  controller.enqueue(data)
                } catch {
                  isCancelled = true
                }
              }
            }

            // Handle file uploads if present
            const uploadedFiles: any[] = []
            interface UploadErrorCapableInput {
              onUploadError: (message: string) => void
            }
            const isUploadErrorCapable = (value: unknown): value is UploadErrorCapableInput =>
              !!value &&
              typeof value === 'object' &&
              'onUploadError' in (value as any) &&
              typeof (value as any).onUploadError === 'function'
            if (workflowInput.files && Array.isArray(workflowInput.files)) {
              try {
                for (const fileData of workflowInput.files) {
                  // Create FormData for upload
                  const formData = new FormData()
                  formData.append('file', fileData.file)
                  formData.append('context', 'execution')
                  formData.append('workflowId', activeWorkflowId)
                  formData.append('executionId', executionId)
                  formData.append('workspaceId', workspaceId)

                  // Upload the file
                  const response = await fetch('/api/files/upload', {
                    method: 'POST',
                    body: formData,
                  })

                  if (response.ok) {
                    const uploadResult = await response.json()
                    // Convert upload result to clean UserFile format
                    const processUploadResult = (result: any) => ({
                      id:
                        result.id ||
                        `file_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`,
                      name: result.name,
                      url: result.url,
                      size: result.size,
                      type: result.type,
                      key: result.key,
                      uploadedAt: result.uploadedAt,
                      expiresAt: result.expiresAt,
                    })

                    // The API returns the file directly for single uploads
                    // or { files: [...] } for multiple uploads
                    if (uploadResult.files && Array.isArray(uploadResult.files)) {
                      uploadedFiles.push(...uploadResult.files.map(processUploadResult))
                    } else if (uploadResult.path || uploadResult.url) {
                      // Single file upload - the result IS the file object
                      uploadedFiles.push(processUploadResult(uploadResult))
                    } else {
                      logger.error('Unexpected upload response format:', uploadResult)
                    }
                  } else {
                    const errorText = await response.text()
                    const message = `Failed to upload ${fileData.name}: ${response.status} ${errorText}`
                    logger.error(message)
                    if (isUploadErrorCapable(workflowInput)) {
                      try {
                        workflowInput.onUploadError(message)
                      } catch {}
                    }
                  }
                }
                // Update workflow input with uploaded files
                workflowInput.files = uploadedFiles
              } catch (error) {
                logger.error('Error uploading files:', error)
                if (isUploadErrorCapable(workflowInput)) {
                  try {
                    workflowInput.onUploadError('Unexpected error uploading files')
                  } catch {}
                }
                // Continue execution even if file upload fails
                workflowInput.files = []
              }
            }

            const streamCompletionTimes = new Map<string, number>()
            const processedFirstChunk = new Set<string>()

            const onStream = async (streamingExecution: StreamingExecution) => {
              const promise = (async () => {
                if (!streamingExecution.stream) return
                const reader = streamingExecution.stream.getReader()
                const blockId = (streamingExecution.execution as any)?.blockId

                if (blockId && !streamedChunks.has(blockId)) {
                  streamedChunks.set(blockId, [])
                }

                try {
                  while (true) {
                    const { done, value } = await reader.read()
                    if (done) {
                      if (blockId) {
                        streamCompletionTimes.set(blockId, Date.now())
                      }
                      break
                    }
                    const chunk = new TextDecoder().decode(value)
                    if (blockId) {
                      streamedChunks.get(blockId)!.push(chunk)
                    }

                    let chunkToSend = chunk
                    if (blockId && !processedFirstChunk.has(blockId)) {
                      processedFirstChunk.add(blockId)
                      if (streamedChunks.size > 1) {
                        chunkToSend = `\n\n${chunk}`
                      }
                    }

                    safeEnqueue(encodeSSE({ blockId, chunk: chunkToSend }))
                  }
                } catch (error) {
                  logger.error('Error reading from stream:', error)
                  controller.error(error)
                }
              })()
              streamReadingPromises.push(promise)
            }

            // Handle non-streaming blocks (like Function blocks)
            const onBlockComplete = async (blockId: string, output: any) => {
              // Skip if this block already had streaming content (avoid duplicates)
              if (streamedChunks.has(blockId)) {
                logger.debug('[handleRunWorkflow] Skipping onBlockComplete for streaming block', {
                  blockId,
                })
                return
              }

              // Get selected outputs from chat store
              const chatStore = await import('@/stores/chat/store').then((mod) => mod.useChatStore)
              const selectedOutputs = chatStore
                .getState()
                .getSelectedWorkflowOutput(activeWorkflowId)

              if (!selectedOutputs?.length) return

              const { extractBlockIdFromOutputId, extractPathFromOutputId, traverseObjectPath } =
                await import('@/lib/core/utils/response-format')

              // Check if this block's output is selected
              const matchingOutputs = selectedOutputs.filter(
                (outputId) => extractBlockIdFromOutputId(outputId) === blockId
              )

              if (!matchingOutputs.length) return

              // Process each selected output from this block
              for (const outputId of matchingOutputs) {
                const path = extractPathFromOutputId(outputId, blockId)
                const outputValue = traverseObjectPath(output, path)

                if (outputValue !== undefined) {
                  const formattedOutput =
                    typeof outputValue === 'string'
                      ? outputValue
                      : JSON.stringify(outputValue, null, 2)

                  // Add separator if this isn't the first output
                  const separator = streamedChunks.size > 0 ? '\n\n' : ''

                  // Send the non-streaming block output as a chunk
                  safeEnqueue(encodeSSE({ blockId, chunk: separator + formattedOutput }))

                  // Track that we've sent output for this block
                  streamedChunks.set(blockId, [formattedOutput])
                }
              }
            }

            try {
              const result = await executeWorkflow(
                workflowInput,
                onStream,
                executionId,
                onBlockComplete,
                'chat'
              )

              // Check if execution was cancelled
              if (result && 'status' in result && result.status === 'cancelled') {
                safeEnqueue(encodeSSE({ event: 'cancelled', data: result }))
                return
              }

              await Promise.all(streamReadingPromises)

              if (result && 'success' in result) {
                if (!result.metadata) {
                  result.metadata = { duration: 0, startTime: new Date().toISOString() }
                }
                ;(result.metadata as any).source = 'chat'

                // Update block logs with actual stream completion times
                if (result.logs && streamCompletionTimes.size > 0) {
                  result.logs.forEach((log: BlockLog) => {
                    if (streamCompletionTimes.has(log.blockId)) {
                      const completionTime = streamCompletionTimes.get(log.blockId)!
                      const startTime = new Date(log.startedAt).getTime()

                      // Update the log with actual stream completion time
                      log.endedAt = new Date(completionTime).toISOString()
                      log.durationMs = completionTime - startTime
                    }
                  })
                }

                // Resolve chunks to final strings for consumption
                const streamedContent = new Map<string, string>()
                for (const [id, chunks] of streamedChunks) {
                  streamedContent.set(id, chunks.join(''))
                }

                // Update streamed content and apply tokenization
                if (result.logs) {
                  result.logs.forEach((log: BlockLog) => {
                    if (streamedContent.has(log.blockId)) {
                      // For console display, show the actual structured block output instead of formatted streaming content
                      // This ensures console logs match the block state structure
                      // Use replaceOutput to completely replace the output instead of merging
                      // Use the executionId from this execution context
                      useTerminalConsoleStore.getState().updateConsole(
                        log.blockId,
                        {
                          executionOrder: log.executionOrder,
                          replaceOutput: log.output,
                          success: true,
                        },
                        executionId
                      )
                    }
                  })

                  // Process all logs for streaming tokenization
                  const processedCount = processStreamingBlockLogs(result.logs, streamedContent)
                  logger.info(`Processed ${processedCount} blocks for streaming tokenization`)
                }

                // Invalidate subscription queries to update usage
                setTimeout(() => {
                  queryClient.invalidateQueries({ queryKey: subscriptionKeys.all })
                }, 1000)

                safeEnqueue(encodeSSE({ event: 'final', data: result }))
                // Note: Logs are already persisted server-side via execution-core.ts
              }
            } catch (error: any) {
              // Create a proper error result for logging
              const errorResult = {
                success: false,
                error: error.message || 'Workflow execution failed',
                output: {},
                logs: [],
                metadata: {
                  duration: 0,
                  startTime: new Date().toISOString(),
                  source: 'chat' as const,
                },
              }

              // Send the error as final event so downstream handlers can treat it uniformly
              safeEnqueue(encodeSSE({ event: 'final', data: errorResult }))

              // Do not error the controller to allow consumers to process the final event
            } finally {
              if (!isCancelled) {
                controller.close()
              }
              if (currentChatExecutionIdRef.current === executionId) {
                setIsExecuting(activeWorkflowId, false)
                setIsDebugging(activeWorkflowId, false)
                setActiveBlocks(activeWorkflowId, new Set())
              }
            }
          },
          cancel() {
            isCancelled = true
          },
        })
        return { success: true, stream }
      }

      const manualExecutionId = uuidv4()
      try {
        const result = await executeWorkflow(
          workflowInput,
          undefined,
          manualExecutionId,
          undefined,
          'manual'
        )
        if (result && 'metadata' in result && result.metadata?.isDebugSession) {
          setDebugContext(activeWorkflowId, result.metadata.context || null)
          if (result.metadata.pendingBlocks) {
            setPendingBlocks(activeWorkflowId, result.metadata.pendingBlocks)
          }
        }
        return result
      } catch (error: any) {
        const errorResult = handleExecutionError(error, { executionId: manualExecutionId })
        return errorResult
      }
    },
    [
      activeWorkflowId,
      currentWorkflow,
      toggleConsole,
      getAllVariables,
      getVariablesByWorkflowId,
      setIsExecuting,
      setIsDebugging,
      setDebugContext,
      setExecutor,
      setPendingBlocks,
      setActiveBlocks,
      workflows,
      queryClient,
    ]
  )

  const executeWorkflow = async (
    workflowInput?: any,
    onStream?: (se: StreamingExecution) => Promise<void>,
    executionId?: string,
    onBlockComplete?: (blockId: string, output: any) => Promise<void>,
    overrideTriggerType?: 'chat' | 'manual' | 'api',
    stopAfterBlockId?: string
  ): Promise<ExecutionResult | StreamingExecution> => {
    // Use diff workflow for execution when available, regardless of canvas view state
    const executionWorkflowState = null as {
      blocks?: any
      edges?: any
      loops?: any
      parallels?: any
    } | null
    const usingDiffForExecution = false

    // Read blocks and edges directly from store to ensure we get the latest state,
    // even if React hasn't re-rendered yet after adding blocks/edges
    const latestWorkflowState = useWorkflowStore.getState().getWorkflowState()
    const workflowBlocks = (executionWorkflowState?.blocks ??
      latestWorkflowState.blocks) as typeof currentWorkflow.blocks
    const workflowEdges = (executionWorkflowState?.edges ??
      latestWorkflowState.edges) as typeof currentWorkflow.edges

    // Filter out blocks without type (these are layout-only blocks) and disabled blocks
    const validBlocks = Object.entries(workflowBlocks).reduce(
      (acc, [blockId, block]) => {
        if (block?.type && block.enabled !== false) {
          acc[blockId] = block
        }
        return acc
      },
      {} as typeof workflowBlocks
    )

    const isExecutingFromChat = overrideTriggerType === 'chat'

    logger.info('Executing workflow', {
      isDiffMode: currentWorkflow.isDiffMode,
      usingDiffForExecution,
      isViewingDiff,
      executingDiffWorkflow: usingDiffForExecution && isViewingDiff,
      isExecutingFromChat,
      totalBlocksCount: Object.keys(workflowBlocks).length,
      validBlocksCount: Object.keys(validBlocks).length,
      edgesCount: workflowEdges.length,
    })

    // Debug: Check for blocks with undefined types before merging
    Object.entries(workflowBlocks).forEach(([blockId, block]) => {
      if (!block || !block.type) {
        logger.error('Found block with undefined type before merging:', { blockId, block })
      }
    })

    // Merge subblock states from the appropriate store (scoped to active workflow)
    const mergedStates = mergeSubblockState(validBlocks, activeWorkflowId ?? undefined)

    // Debug: Check for blocks with undefined types after merging
    Object.entries(mergedStates).forEach(([blockId, block]) => {
      if (!block || !block.type) {
        logger.error('Found block with undefined type after merging:', { blockId, block })
      }
    })

    // Filter out blocks without type and disabled blocks
    const filteredStates = Object.entries(mergedStates).reduce(
      (acc, [id, block]) => {
        if (!block || !block.type) {
          logger.warn(`Skipping block with undefined type: ${id}`, block)
          return acc
        }
        // Skip disabled blocks to prevent them from being passed to executor
        if (block.enabled === false) {
          logger.warn(`Skipping disabled block: ${id}`)
          return acc
        }
        acc[id] = block
        return acc
      },
      {} as typeof mergedStates
    )

    // If this is a chat execution, get the selected outputs
    let selectedOutputs: string[] | undefined
    if (isExecutingFromChat && activeWorkflowId) {
      // Get selected outputs from chat store
      const chatStore = await import('@/stores/chat/store').then((mod) => mod.useChatStore)
      selectedOutputs = chatStore.getState().getSelectedWorkflowOutput(activeWorkflowId)
    }

    // Helper to extract test values from inputFormat subblock
    const extractTestValuesFromInputFormat = (inputFormatValue: any): Record<string, any> => {
      const testInput: Record<string, any> = {}

      if (Array.isArray(inputFormatValue)) {
        inputFormatValue.forEach((field: any) => {
          if (field && typeof field === 'object' && field.name && field.value !== undefined) {
            testInput[field.name] = coerceValue(field.type, field.value)
          }
        })
      }

      return testInput
    }

    // Determine start block and workflow input based on execution type
    let startBlockId: string | undefined
    let finalWorkflowInput = workflowInput

    if (isExecutingFromChat) {
      // For chat execution, find the appropriate chat trigger
      const startBlock = TriggerUtils.findStartBlock(filteredStates, 'chat')

      if (!startBlock) {
        throw new WorkflowValidationError(
          TriggerUtils.getTriggerValidationMessage('chat', 'missing'),
          'validation',
          'validation',
          'Workflow Validation'
        )
      }

      startBlockId = startBlock.blockId
    } else {
      // Manual execution: detect and group triggers by paths
      const candidates = resolveStartCandidates(filteredStates, {
        execution: 'manual',
      })

      if (candidates.length === 0) {
        const error = new WorkflowValidationError(
          'Workflow requires at least one trigger block to execute',
          'validation',
          'validation',
          'Workflow Validation'
        )
        logger.error('No trigger blocks found for manual run', {
          allBlockTypes: Object.values(filteredStates).map((b) => b.type),
        })
        if (activeWorkflowId) setIsExecuting(activeWorkflowId, false)
        throw error
      }

      // Check for multiple API triggers (still not allowed)
      const apiCandidates = candidates.filter(
        (candidate) => candidate.path === StartBlockPath.SPLIT_API
      )
      if (apiCandidates.length > 1) {
        const error = new WorkflowValidationError(
          'Multiple API Trigger blocks found. Keep only one.',
          'validation',
          'validation',
          'Workflow Validation'
        )
        logger.error('Multiple API triggers found')
        if (activeWorkflowId) setIsExecuting(activeWorkflowId, false)
        throw error
      }

      // Select the best trigger
      // Priority: Start Block > Schedules > External Triggers > Legacy
      const selectedTriggers = selectBestTrigger(candidates, workflowEdges)

      // Execute the first/highest priority trigger
      const selectedCandidate = selectedTriggers[0]
      startBlockId = selectedCandidate.blockId
      const selectedTrigger = selectedCandidate.block

      // Validate outgoing connections for non-legacy triggers
      if (selectedCandidate.path !== StartBlockPath.LEGACY_STARTER) {
        const outgoingConnections = workflowEdges.filter((edge) => edge.source === startBlockId)
        if (outgoingConnections.length === 0) {
          const triggerName = selectedTrigger.name || selectedTrigger.type
          const error = new WorkflowValidationError(
            `${triggerName} must be connected to other blocks to execute`,
            'validation',
            'validation',
            'Workflow Validation'
          )
          logger.error('Trigger has no outgoing connections', { triggerName, startBlockId })
          if (activeWorkflowId) setIsExecuting(activeWorkflowId, false)
          throw error
        }
      }

      // Prepare input based on trigger type
      if (triggerNeedsMockPayload(selectedCandidate)) {
        const mockPayload = extractTriggerMockPayload(selectedCandidate)
        finalWorkflowInput = mockPayload
      } else if (
        selectedCandidate.path === StartBlockPath.SPLIT_API ||
        selectedCandidate.path === StartBlockPath.SPLIT_INPUT ||
        selectedCandidate.path === StartBlockPath.UNIFIED
      ) {
        const inputFormatValue = selectedTrigger.subBlocks?.inputFormat?.value
        const testInput = extractTestValuesFromInputFormat(inputFormatValue)
        if (Object.keys(testInput).length > 0) {
          finalWorkflowInput = testInput
        }
      }
    }

    // If we don't have a valid startBlockId at this point, throw an error
    if (!startBlockId) {
      const error = new WorkflowValidationError(
        'No valid trigger block found to start execution',
        'validation',
        'validation',
        'Workflow Validation'
      )
      logger.error('No startBlockId found after trigger search')
      if (activeWorkflowId) setIsExecuting(activeWorkflowId, false)
      throw error
    }

    // Log the final startBlockId
    logger.info('Final execution setup:', {
      startBlockId,
      isExecutingFromChat,
      hasWorkflowInput: !!workflowInput,
    })

    // SERVER-SIDE EXECUTION (always)
    if (activeWorkflowId) {
      logger.info('Using server-side executor')

      const executionIdRef = { current: '' }

      let executionResult: ExecutionResult = {
        success: false,
        output: {},
        logs: [],
      }

      const activeBlocksSet = new Set<string>()
      const activeBlockRefCounts = new Map<string, number>()
      const streamedChunks = new Map<string, string[]>()
      const accumulatedBlockLogs: BlockLog[] = []
      const accumulatedBlockStates = new Map<string, BlockState>()
      const executedBlockIds = new Set<string>()

      // Execute the workflow
      try {
        const blockHandlers = buildBlockEventHandlers({
          workflowId: activeWorkflowId,
          executionIdRef,
          workflowEdges,
          activeBlocksSet,
          activeBlockRefCounts,
          accumulatedBlockLogs,
          accumulatedBlockStates,
          executedBlockIds,
          consoleMode: 'update',
          includeStartConsoleEntry: true,
          onBlockCompleteCallback: onBlockComplete,
        })

        const clientWorkflowState = executionWorkflowState || {
          blocks: filteredStates,
          edges: workflowEdges,
          loops: latestWorkflowState.loops,
          parallels: latestWorkflowState.parallels,
        }

        await executionStream.execute({
          workflowId: activeWorkflowId,
          input: finalWorkflowInput,
          startBlockId,
          selectedOutputs,
          triggerType: overrideTriggerType || 'manual',
          useDraftState: true,
          isClientSession: true,
          stopAfterBlockId,
          workflowStateOverride: {
            blocks: clientWorkflowState.blocks,
            edges: clientWorkflowState.edges,
            loops: clientWorkflowState.loops,
            parallels: clientWorkflowState.parallels,
          },
          onExecutionId: (id) => {
            executionIdRef.current = id
            setCurrentExecutionId(activeWorkflowId, id)
            saveExecutionPointer({
              workflowId: activeWorkflowId,
              executionId: id,
              lastEventId: 0,
            })
          },
          callbacks: {
            onEventId: (eventId) => {
              lastSeenEventIdRef.current = eventId
            },

            onExecutionStarted: (data) => {
              logger.info('Server execution started:', data)
            },

            onBlockStarted: blockHandlers.onBlockStarted,
            onBlockCompleted: blockHandlers.onBlockCompleted,
            onBlockError: blockHandlers.onBlockError,
            onBlockChildWorkflowStarted: blockHandlers.onBlockChildWorkflowStarted,

            onStreamChunk: (data) => {
              if (!streamedChunks.has(data.blockId)) {
                streamedChunks.set(data.blockId, [])
              }
              streamedChunks.get(data.blockId)!.push(data.chunk)

              // Call onStream callback if provided (create a fake StreamingExecution)
              if (onStream && isExecutingFromChat) {
                const stream = new ReadableStream({
                  start(controller) {
                    controller.enqueue(new TextEncoder().encode(data.chunk))
                    controller.close()
                  },
                })

                const streamingExec: StreamingExecution = {
                  stream,
                  execution: {
                    success: true,
                    output: { content: '' },
                    blockId: data.blockId,
                  } as any,
                }

                onStream(streamingExec).catch((error) => {
                  logger.error('Error in onStream callback:', error)
                })
              }
            },

            onStreamDone: (data) => {
              logger.info('Stream done for block:', data.blockId)
            },

            onExecutionCompleted: (data) => {
              if (
                activeWorkflowId &&
                executionIdRef.current &&
                useExecutionStore.getState().getCurrentExecutionId(activeWorkflowId) !==
                  executionIdRef.current
              )
                return

              if (activeWorkflowId) {
                setCurrentExecutionId(activeWorkflowId, null)
              }

              executionResult = {
                success: data.success,
                output: data.output,
                metadata: {
                  duration: data.duration,
                  startTime: data.startTime,
                  endTime: data.endTime,
                },
                logs: accumulatedBlockLogs,
              }

              // Add trigger block to executed blocks so downstream blocks can use run-from-block
              if (data.success && startBlockId) {
                executedBlockIds.add(startBlockId)
              }

              if (data.success && activeWorkflowId) {
                if (stopAfterBlockId) {
                  const existingSnapshot = getLastExecutionSnapshot(activeWorkflowId)
                  const mergedBlockStates = {
                    ...(existingSnapshot?.blockStates || {}),
                    ...Object.fromEntries(accumulatedBlockStates),
                  }
                  const mergedExecutedBlocks = new Set([
                    ...(existingSnapshot?.executedBlocks || []),
                    ...executedBlockIds,
                  ])
                  const snapshot: SerializableExecutionState = {
                    blockStates: mergedBlockStates,
                    executedBlocks: Array.from(mergedExecutedBlocks),
                    blockLogs: [...(existingSnapshot?.blockLogs || []), ...accumulatedBlockLogs],
                    decisions: existingSnapshot?.decisions || { router: {}, condition: {} },
                    completedLoops: existingSnapshot?.completedLoops || [],
                    activeExecutionPath: Array.from(mergedExecutedBlocks),
                  }
                  setLastExecutionSnapshot(activeWorkflowId, snapshot)
                  logger.info('Merged execution snapshot after run-until-block', {
                    workflowId: activeWorkflowId,
                    newBlocksExecuted: executedBlockIds.size,
                    totalExecutedBlocks: mergedExecutedBlocks.size,
                  })
                } else {
                  const snapshot: SerializableExecutionState = {
                    blockStates: Object.fromEntries(accumulatedBlockStates),
                    executedBlocks: Array.from(executedBlockIds),
                    blockLogs: accumulatedBlockLogs,
                    decisions: { router: {}, condition: {} },
                    completedLoops: [],
                    activeExecutionPath: Array.from(executedBlockIds),
                  }
                  setLastExecutionSnapshot(activeWorkflowId, snapshot)
                  logger.info('Stored execution snapshot for run-from-block', {
                    workflowId: activeWorkflowId,
                    executedBlocksCount: executedBlockIds.size,
                  })
                }
              }

              const workflowExecState = activeWorkflowId
                ? useExecutionStore.getState().getWorkflowExecution(activeWorkflowId)
                : null
              if (activeWorkflowId && !workflowExecState?.isDebugging) {
                setExecutionResult(executionResult)
                // For chat executions, don't set isExecuting=false here — the chat's
                // client-side stream wrapper still has buffered data to deliver.
                // The chat's finally block handles cleanup after the stream is fully consumed.
                if (!isExecutingFromChat) {
                  setIsExecuting(activeWorkflowId, false)
                  setActiveBlocks(activeWorkflowId, new Set())
                }
                setTimeout(() => {
                  queryClient.invalidateQueries({ queryKey: subscriptionKeys.all })
                }, 1000)
              }
            },

            onExecutionError: (data) => {
              if (
                activeWorkflowId &&
                executionIdRef.current &&
                useExecutionStore.getState().getCurrentExecutionId(activeWorkflowId) !==
                  executionIdRef.current
              )
                return

              if (activeWorkflowId) {
                setCurrentExecutionId(activeWorkflowId, null)
              }

              executionResult = {
                success: false,
                output: {},
                error: data.error,
                metadata: {
                  duration: data.duration,
                },
                logs: accumulatedBlockLogs,
              }

              const isPreExecutionError = accumulatedBlockLogs.length === 0
              handleExecutionErrorConsole({
                workflowId: activeWorkflowId,
                executionId: executionIdRef.current,
                error: data.error,
                durationMs: data.duration,
                blockLogs: accumulatedBlockLogs,
                isPreExecutionError,
              })

              if (activeWorkflowId && !isExecutingFromChat) {
                setIsExecuting(activeWorkflowId, false)
                setIsDebugging(activeWorkflowId, false)
                setActiveBlocks(activeWorkflowId, new Set())
              }
            },

            onExecutionCancelled: (data) => {
              if (
                activeWorkflowId &&
                executionIdRef.current &&
                useExecutionStore.getState().getCurrentExecutionId(activeWorkflowId) !==
                  executionIdRef.current
              )
                return

              if (activeWorkflowId) {
                setCurrentExecutionId(activeWorkflowId, null)
              }

              handleExecutionCancelledConsole({
                workflowId: activeWorkflowId,
                executionId: executionIdRef.current,
                durationMs: data?.duration,
              })

              if (activeWorkflowId && !isExecutingFromChat) {
                setIsExecuting(activeWorkflowId, false)
                setIsDebugging(activeWorkflowId, false)
                setActiveBlocks(activeWorkflowId, new Set())
              }
            },
          },
        })

        return executionResult
      } catch (error: any) {
        if (error.name === 'AbortError' || error.message?.includes('aborted')) {
          logger.info('Execution aborted by user')
          return executionResult
        }

        logger.error('Server-side execution failed:', error)
        throw error
      }
    }

    throw new Error('Server-side execution is required')
  }

  const handleExecutionError = (error: unknown, options?: { executionId?: string }) => {
    const normalizedMessage = normalizeErrorMessage(error)

    let errorResult: ExecutionResult

    if (hasExecutionResult(error)) {
      const executionResultFromError = error.executionResult
      const logs = Array.isArray(executionResultFromError.logs) ? executionResultFromError.logs : []

      errorResult = {
        ...executionResultFromError,
        success: false,
        error: executionResultFromError.error ?? normalizedMessage,
        logs,
      }
    } else {
      if (!executor) {
        try {
          const httpStatus =
            isRecord(error) && typeof error.httpStatus === 'number' ? error.httpStatus : undefined
          const storeAddConsole = useTerminalConsoleStore.getState().addConsole

          if (httpStatus && activeWorkflowId) {
            addHttpErrorConsoleEntry(storeAddConsole, {
              workflowId: activeWorkflowId,
              executionId: options?.executionId,
              error: normalizedMessage,
              httpStatus,
            })
          } else if (error instanceof WorkflowValidationError) {
            storeAddConsole({
              input: {},
              output: {},
              success: false,
              error: normalizedMessage,
              durationMs: 0,
              startedAt: new Date().toISOString(),
              executionOrder: Number.MAX_SAFE_INTEGER,
              endedAt: new Date().toISOString(),
              workflowId: activeWorkflowId || '',
              blockId: error.blockId || 'serialization',
              executionId: options?.executionId,
              blockName: error.blockName || 'Workflow',
              blockType: error.blockType || 'serializer',
            })
          } else {
            sharedAddExecutionErrorConsoleEntry(storeAddConsole, {
              workflowId: activeWorkflowId || '',
              executionId: options?.executionId,
              error: normalizedMessage,
              blockLogs: [],
              isPreExecutionError: true,
            })
          }
        } catch {}
      }

      errorResult = {
        success: false,
        output: {},
        error: normalizedMessage,
        logs: [],
      }
    }

    setExecutionResult(errorResult)
    if (activeWorkflowId) {
      setIsExecuting(activeWorkflowId, false)
      setIsDebugging(activeWorkflowId, false)
      setActiveBlocks(activeWorkflowId, new Set())
    }

    let notificationMessage = WORKFLOW_EXECUTION_FAILURE_MESSAGE
    if (isRecord(error) && isRecord(error.request) && sanitizeMessage(error.request.url)) {
      notificationMessage += `: Request to ${(error.request.url as string).trim()} failed`
      if ('status' in error && typeof error.status === 'number') {
        notificationMessage += ` (Status: ${error.status})`
      }
    } else if (sanitizeMessage(errorResult.error)) {
      notificationMessage += `: ${errorResult.error}`
    }

    return errorResult
  }

  /**
   * Handles stepping through workflow execution in debug mode
   */
  const handleStepDebug = useCallback(async () => {
    logger.info('Step Debug requested', {
      hasExecutor: !!executor,
      hasContext: !!debugContext,
      pendingBlockCount: pendingBlocks.length,
    })

    // Validate debug state
    const validation = validateDebugState()
    if (!validation.isValid) {
      resetDebugState()
      return
    }

    try {
      logger.info('Executing debug step with blocks:', pendingBlocks)
      const result = await executor!.continueExecution(pendingBlocks, debugContext!)
      logger.info('Debug step execution result:', result)

      if (isDebugSessionComplete(result)) {
        await handleDebugSessionComplete(result)
      } else {
        handleDebugSessionContinuation(result)
      }
    } catch (error: any) {
      await handleDebugExecutionError(error, 'step')
    }
  }, [
    executor,
    debugContext,
    pendingBlocks,
    activeWorkflowId,
    validateDebugState,
    resetDebugState,
    isDebugSessionComplete,
    handleDebugSessionComplete,
    handleDebugSessionContinuation,
    handleDebugExecutionError,
  ])

  /**
   * Handles resuming execution in debug mode until completion
   */
  const handleResumeDebug = useCallback(async () => {
    logger.info('Resume Debug requested', {
      hasExecutor: !!executor,
      hasContext: !!debugContext,
      pendingBlockCount: pendingBlocks.length,
    })

    // Validate debug state
    const validation = validateDebugState()
    if (!validation.isValid) {
      resetDebugState()
      return
    }

    try {
      logger.info('Resuming workflow execution until completion')

      let currentResult: ExecutionResult = {
        success: true,
        output: {},
        logs: debugContext!.blockLogs,
      }

      // Create copies to avoid mutation issues
      let currentContext = { ...debugContext! }
      let currentPendingBlocks = [...pendingBlocks]

      logger.info('Starting resume execution with blocks:', currentPendingBlocks)

      // Continue execution until there are no more pending blocks
      let iterationCount = 0
      const maxIterations = 500 // Safety to prevent infinite loops

      while (currentPendingBlocks.length > 0 && iterationCount < maxIterations) {
        logger.info(
          `Resume iteration ${iterationCount + 1}, executing ${currentPendingBlocks.length} blocks`
        )

        currentResult = await executor!.continueExecution(currentPendingBlocks, currentContext)

        logger.info('Resume iteration result:', {
          success: currentResult.success,
          hasPendingBlocks: !!currentResult.metadata?.pendingBlocks,
          pendingBlockCount: currentResult.metadata?.pendingBlocks?.length || 0,
        })

        // Update context for next iteration
        if (currentResult.metadata?.context) {
          currentContext = currentResult.metadata.context
        } else {
          logger.info('No context in result, ending resume')
          break
        }

        // Update pending blocks for next iteration
        if (currentResult.metadata?.pendingBlocks) {
          currentPendingBlocks = currentResult.metadata.pendingBlocks
        } else {
          logger.info('No pending blocks in result, ending resume')
          break
        }

        // If we don't have a debug session anymore, we're done
        if (!currentResult.metadata?.isDebugSession) {
          logger.info('Debug session ended, ending resume')
          break
        }

        iterationCount++
      }

      if (iterationCount >= maxIterations) {
        logger.warn('Resume execution reached maximum iteration limit')
      }

      logger.info('Resume execution complete', {
        iterationCount,
        success: currentResult.success,
      })

      // Handle completion
      await handleDebugSessionComplete(currentResult)
    } catch (error: any) {
      await handleDebugExecutionError(error, 'resume')
    }
  }, [
    executor,
    debugContext,
    pendingBlocks,
    activeWorkflowId,
    validateDebugState,
    resetDebugState,
    handleDebugSessionComplete,
    handleDebugExecutionError,
  ])

  /**
   * Handles cancelling the current debugging session
   */
  const handleCancelDebug = useCallback(() => {
    logger.info('Debug session cancelled')
    resetDebugState()
  }, [resetDebugState])

  /**
   * Handles cancelling the current workflow execution
   */
  const handleCancelExecution = useCallback(() => {
    if (!activeWorkflowId) return
    logger.info('Workflow execution cancellation requested')

    const storedExecutionId = getCurrentExecutionId(activeWorkflowId)

    if (storedExecutionId) {
      setCurrentExecutionId(activeWorkflowId, null)
      fetch(`/api/workflows/${activeWorkflowId}/executions/${storedExecutionId}/cancel`, {
        method: 'POST',
      }).catch(() => {})
      handleExecutionCancelledConsole({
        workflowId: activeWorkflowId,
        executionId: storedExecutionId,
      })
    }

    executionStream.cancel(activeWorkflowId)
    currentChatExecutionIdRef.current = null
    setIsExecuting(activeWorkflowId, false)
    setIsDebugging(activeWorkflowId, false)
    setActiveBlocks(activeWorkflowId, new Set())

    if (isDebugging) {
      resetDebugState()
    }
  }, [
    executionStream,
    isDebugging,
    resetDebugState,
    setIsExecuting,
    setIsDebugging,
    setActiveBlocks,
    activeWorkflowId,
    getCurrentExecutionId,
    setCurrentExecutionId,
    handleExecutionCancelledConsole,
  ])

  /**
   * Handles running workflow from a specific block using cached outputs
   */
  const handleRunFromBlock = useCallback(
    async (blockId: string, workflowId: string) => {
      const snapshot = getLastExecutionSnapshot(workflowId)
      const workflowEdges = useWorkflowStore.getState().edges
      const incomingEdges = workflowEdges.filter((edge) => edge.target === blockId)
      const isTriggerBlock = incomingEdges.length === 0

      // Check if each source block is either executed OR is a trigger block (triggers don't need prior execution)
      const isSourceSatisfied = (sourceId: string) => {
        if (snapshot?.executedBlocks.includes(sourceId)) return true
        // Check if source is a trigger (has no incoming edges itself)
        const sourceIncomingEdges = workflowEdges.filter((edge) => edge.target === sourceId)
        return sourceIncomingEdges.length === 0
      }

      // Non-trigger blocks need a snapshot to exist (so upstream outputs are available)
      if (!snapshot && !isTriggerBlock) {
        logger.error('No execution snapshot available for run-from-block', { workflowId, blockId })
        return
      }

      const dependenciesSatisfied =
        isTriggerBlock || incomingEdges.every((edge) => isSourceSatisfied(edge.source))

      if (!dependenciesSatisfied) {
        logger.error('Upstream dependencies not satisfied for run-from-block', {
          workflowId,
          blockId,
        })
        return
      }

      // For trigger blocks, always use empty snapshot to prevent stale data from different
      // execution paths from being resolved. For non-trigger blocks, use the existing snapshot.
      const emptySnapshot: SerializableExecutionState = {
        blockStates: {},
        executedBlocks: [],
        blockLogs: [],
        decisions: { router: {}, condition: {} },
        completedLoops: [],
        activeExecutionPath: [],
      }
      const effectiveSnapshot: SerializableExecutionState = isTriggerBlock
        ? emptySnapshot
        : snapshot || emptySnapshot

      // Extract mock payload for trigger blocks
      let workflowInput: any
      if (isTriggerBlock) {
        const workflowBlocks = useWorkflowStore.getState().blocks
        const mergedStates = mergeSubblockState(workflowBlocks, workflowId)
        const candidates = resolveStartCandidates(mergedStates, { execution: 'manual' })
        const candidate = candidates.find((c) => c.blockId === blockId)

        if (candidate) {
          if (triggerNeedsMockPayload(candidate)) {
            workflowInput = extractTriggerMockPayload(candidate)
          } else if (
            candidate.path === StartBlockPath.SPLIT_API ||
            candidate.path === StartBlockPath.SPLIT_INPUT ||
            candidate.path === StartBlockPath.UNIFIED
          ) {
            const inputFormatValue = candidate.block.subBlocks?.inputFormat?.value
            if (Array.isArray(inputFormatValue)) {
              const testInput: Record<string, any> = {}
              inputFormatValue.forEach((field: any) => {
                if (field && typeof field === 'object' && field.name && field.value !== undefined) {
                  testInput[field.name] = coerceValue(field.type, field.value)
                }
              })
              if (Object.keys(testInput).length > 0) {
                workflowInput = testInput
              }
            }
          }
        } else {
          // Fallback: block is trigger by position but not classified as start candidate
          const block = mergedStates[blockId]
          if (block) {
            const blockConfig = getBlock(block.type)
            const hasTriggers = blockConfig?.triggers?.available?.length

            if (hasTriggers || block.triggerMode) {
              workflowInput = extractTriggerMockPayload({
                blockId,
                block,
                path: StartBlockPath.EXTERNAL_TRIGGER,
              })
            }
          }
        }
      }

      setIsExecuting(workflowId, true)
      const executionIdRef = { current: '' }
      const accumulatedBlockLogs: BlockLog[] = []
      const accumulatedBlockStates = new Map<string, BlockState>()
      const executedBlockIds = new Set<string>()
      const activeBlocksSet = new Set<string>()
      const activeBlockRefCounts = new Map<string, number>()

      try {
        const blockHandlers = buildBlockEventHandlers({
          workflowId,
          executionIdRef,
          workflowEdges,
          activeBlocksSet,
          activeBlockRefCounts,
          accumulatedBlockLogs,
          accumulatedBlockStates,
          executedBlockIds,
          consoleMode: 'update',
          includeStartConsoleEntry: true,
        })

        await executionStream.executeFromBlock({
          workflowId,
          startBlockId: blockId,
          sourceSnapshot: effectiveSnapshot,
          input: workflowInput,
          onExecutionId: (id) => {
            executionIdRef.current = id
            setCurrentExecutionId(workflowId, id)
          },
          callbacks: {
            onBlockStarted: blockHandlers.onBlockStarted,
            onBlockCompleted: blockHandlers.onBlockCompleted,
            onBlockError: blockHandlers.onBlockError,
            onBlockChildWorkflowStarted: blockHandlers.onBlockChildWorkflowStarted,

            onExecutionCompleted: (data) => {
              if (data.success) {
                executedBlockIds.add(blockId)

                const mergedBlockStates: Record<string, BlockState> = {
                  ...effectiveSnapshot.blockStates,
                }
                for (const [bId, state] of accumulatedBlockStates) {
                  mergedBlockStates[bId] = state
                }

                const mergedExecutedBlocks = new Set([
                  ...effectiveSnapshot.executedBlocks,
                  ...executedBlockIds,
                ])

                const updatedSnapshot: SerializableExecutionState = {
                  ...effectiveSnapshot,
                  blockStates: mergedBlockStates,
                  executedBlocks: Array.from(mergedExecutedBlocks),
                  blockLogs: [...effectiveSnapshot.blockLogs, ...accumulatedBlockLogs],
                  activeExecutionPath: Array.from(mergedExecutedBlocks),
                }
                setLastExecutionSnapshot(workflowId, updatedSnapshot)
              }

              setCurrentExecutionId(workflowId, null)
              setIsExecuting(workflowId, false)
              setActiveBlocks(workflowId, new Set())
            },

            onExecutionError: (data) => {
              const isWorkflowModified =
                data.error?.includes('Block not found in workflow') ||
                data.error?.includes('Upstream dependency not executed')

              if (isWorkflowModified) {
                clearLastExecutionSnapshot(workflowId)
                addNotification({
                  level: 'error',
                  message:
                    'Workflow was modified. Run the workflow again to enable running from block.',
                  workflowId,
                })
              }

              handleExecutionErrorConsole({
                workflowId,
                executionId: executionIdRef.current,
                error: data.error,
                durationMs: data.duration,
                blockLogs: accumulatedBlockLogs,
              })

              setCurrentExecutionId(workflowId, null)
              setIsExecuting(workflowId, false)
              setActiveBlocks(workflowId, new Set())
            },

            onExecutionCancelled: (data) => {
              handleExecutionCancelledConsole({
                workflowId,
                executionId: executionIdRef.current,
                durationMs: data?.duration,
              })

              setCurrentExecutionId(workflowId, null)
              setIsExecuting(workflowId, false)
              setActiveBlocks(workflowId, new Set())
            },
          },
        })
      } catch (error) {
        if ((error as Error).name !== 'AbortError') {
          logger.error('Run-from-block failed:', error)
        }
      } finally {
        const currentId = getCurrentExecutionId(workflowId)
        if (currentId === null || currentId === executionIdRef.current) {
          setCurrentExecutionId(workflowId, null)
          setIsExecuting(workflowId, false)
          setActiveBlocks(workflowId, new Set())
        }
      }
    },
    [
      getLastExecutionSnapshot,
      setLastExecutionSnapshot,
      clearLastExecutionSnapshot,
      getCurrentExecutionId,
      setCurrentExecutionId,
      setIsExecuting,
      setActiveBlocks,
      setBlockRunStatus,
      setEdgeRunStatus,
      addNotification,
      buildBlockEventHandlers,
      handleExecutionErrorConsole,
      handleExecutionCancelledConsole,
      executionStream,
    ]
  )

  /**
   * Handles running workflow until a specific block (stops after that block completes)
   */
  const handleRunUntilBlock = useCallback(
    async (blockId: string, workflowId: string) => {
      if (!workflowId || workflowId !== activeWorkflowId) {
        logger.error('Invalid workflow ID for run-until-block', { workflowId, activeWorkflowId })
        return
      }

      logger.info('Starting run-until-block execution', { workflowId, stopAfterBlockId: blockId })

      setExecutionResult(null)
      setIsExecuting(workflowId, true)

      const executionId = uuidv4()
      try {
        await executeWorkflow(undefined, undefined, executionId, undefined, 'manual', blockId)
      } catch (error) {
        const errorResult = handleExecutionError(error, { executionId })
        return errorResult
      } finally {
        setCurrentExecutionId(workflowId, null)
        setIsExecuting(workflowId, false)
        setIsDebugging(workflowId, false)
        setActiveBlocks(workflowId, new Set())
      }
    },
    [
      activeWorkflowId,
      setCurrentExecutionId,
      setExecutionResult,
      setIsExecuting,
      setIsDebugging,
      setActiveBlocks,
    ]
  )

  useEffect(() => {
    if (!activeWorkflowId || !hasHydrated) return
    if (activeReconnections.has(activeWorkflowId)) return

    let cleanupRan = false
    let reconnectionComplete = false
    const reconnectWorkflowId = activeWorkflowId

    const runReconnect = async () => {
      let executionId: string | undefined
      let fromEventId = 0

      try {
        const pointer = await loadExecutionPointer(reconnectWorkflowId)
        if (cleanupRan) return
        if (pointer?.executionId) {
          executionId = pointer.executionId
          fromEventId = pointer.lastEventId
        }
      } catch {
        // fall through to console entries
      }

      if (!executionId) {
        const entries = useTerminalConsoleStore.getState().getWorkflowEntries(reconnectWorkflowId)
        const runningEntries = entries.filter(
          (e) => e.isRunning && e.workflowId === reconnectWorkflowId && e.executionId
        )
        if (runningEntries.length === 0) return

        const sorted = [...runningEntries].sort((a, b) => {
          const aTime = a.startedAt ? new Date(a.startedAt).getTime() : 0
          const bTime = b.startedAt ? new Date(b.startedAt).getTime() : 0
          return bTime - aTime
        })
        executionId = sorted[0].executionId!

        const otherExecutionIds = new Set(
          sorted.filter((e) => e.executionId !== executionId).map((e) => e.executionId!)
        )
        if (otherExecutionIds.size > 0) {
          cancelRunningEntries(reconnectWorkflowId)
          consolePersistence.persist()
        }
      }

      if (!executionId || cleanupRan) return
      if (activeReconnections.has(reconnectWorkflowId)) return
      activeReconnections.add(reconnectWorkflowId)

      executionStream.cancel(reconnectWorkflowId)
      setCurrentExecutionId(reconnectWorkflowId, executionId)
      setIsExecuting(reconnectWorkflowId, true)

      const workflowEdges = useWorkflowStore.getState().edges
      const activeBlocksSet = new Set<string>()
      const activeBlockRefCounts = new Map<string, number>()
      const accumulatedBlockLogs: BlockLog[] = []
      const accumulatedBlockStates = new Map<string, BlockState>()
      const executedBlockIds = new Set<string>()
      const executionIdRef = { current: executionId }

      const handlers = buildBlockEventHandlers({
        workflowId: reconnectWorkflowId,
        executionIdRef,
        workflowEdges,
        activeBlocksSet,
        activeBlockRefCounts,
        accumulatedBlockLogs,
        accumulatedBlockStates,
        executedBlockIds,
        consoleMode: 'update',
        includeStartConsoleEntry: true,
      })

      clearExecutionEntries(executionId)

      const capturedExecutionId = executionId
      const MAX_ATTEMPTS = 5
      const BASE_DELAY_MS = 1000
      const MAX_DELAY_MS = 15000

      const attemptReconnect = async (attempt: number): Promise<void> => {
        if (cleanupRan || reconnectionComplete) return

        if (attempt > 0) {
          const delay = Math.min(BASE_DELAY_MS * 2 ** (attempt - 1), MAX_DELAY_MS)
          await new Promise((resolve) => setTimeout(resolve, delay))
          if (cleanupRan || reconnectionComplete) return
        }

        try {
          await executionStream.reconnect({
            workflowId: reconnectWorkflowId,
            executionId: capturedExecutionId,
            fromEventId,
            callbacks: {
              onEventId: (eid) => {
                fromEventId = eid
              },
              onBlockStarted: handlers.onBlockStarted,
              onBlockCompleted: handlers.onBlockCompleted,
              onBlockError: handlers.onBlockError,
              onBlockChildWorkflowStarted: handlers.onBlockChildWorkflowStarted,
              onExecutionCompleted: () => {
                const currentId = useExecutionStore
                  .getState()
                  .getCurrentExecutionId(reconnectWorkflowId)
                if (currentId !== capturedExecutionId) {
                  reconnectionComplete = true
                  activeReconnections.delete(reconnectWorkflowId)
                  return
                }
                reconnectionComplete = true
                activeReconnections.delete(reconnectWorkflowId)
                setCurrentExecutionId(reconnectWorkflowId, null)
                setIsExecuting(reconnectWorkflowId, false)
                setActiveBlocks(reconnectWorkflowId, new Set())
              },
              onExecutionError: (data) => {
                const currentId = useExecutionStore
                  .getState()
                  .getCurrentExecutionId(reconnectWorkflowId)
                if (currentId !== capturedExecutionId) {
                  reconnectionComplete = true
                  activeReconnections.delete(reconnectWorkflowId)
                  return
                }
                reconnectionComplete = true
                activeReconnections.delete(reconnectWorkflowId)
                setCurrentExecutionId(reconnectWorkflowId, null)
                setIsExecuting(reconnectWorkflowId, false)
                setActiveBlocks(reconnectWorkflowId, new Set())
                handleExecutionErrorConsole({
                  workflowId: reconnectWorkflowId,
                  executionId: capturedExecutionId,
                  error: data.error,
                  blockLogs: accumulatedBlockLogs,
                })
              },
              onExecutionCancelled: () => {
                const currentId = useExecutionStore
                  .getState()
                  .getCurrentExecutionId(reconnectWorkflowId)
                if (currentId !== capturedExecutionId) {
                  reconnectionComplete = true
                  activeReconnections.delete(reconnectWorkflowId)
                  return
                }
                reconnectionComplete = true
                activeReconnections.delete(reconnectWorkflowId)
                setCurrentExecutionId(reconnectWorkflowId, null)
                setIsExecuting(reconnectWorkflowId, false)
                setActiveBlocks(reconnectWorkflowId, new Set())
                handleExecutionCancelledConsole({
                  workflowId: reconnectWorkflowId,
                  executionId: capturedExecutionId,
                })
              },
            },
          })
        } catch (error) {
          logger.warn('Execution reconnection attempt failed', {
            executionId: capturedExecutionId,
            attempt,
            error,
          })
          if (!cleanupRan && !reconnectionComplete && attempt < MAX_ATTEMPTS) {
            return attemptReconnect(attempt + 1)
          }
        }

        if (!reconnectionComplete && !cleanupRan) {
          reconnectionComplete = true
          activeReconnections.delete(reconnectWorkflowId)
          const currentId = useExecutionStore.getState().getCurrentExecutionId(reconnectWorkflowId)
          if (currentId === capturedExecutionId) {
            cancelRunningEntries(reconnectWorkflowId)
            setCurrentExecutionId(reconnectWorkflowId, null)
            setIsExecuting(reconnectWorkflowId, false)
            setActiveBlocks(reconnectWorkflowId, new Set())
          }
        }
      }

      await attemptReconnect(0)
    }

    runReconnect()

    return () => {
      cleanupRan = true
      executionStream.cancel(reconnectWorkflowId)
      activeReconnections.delete(reconnectWorkflowId)
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [activeWorkflowId, hasHydrated])

  return {
    isExecuting,
    isDebugging,
    pendingBlocks,
    executionResult,
    handleRunWorkflow,
    handleStepDebug,
    handleResumeDebug,
    handleCancelDebug,
    handleCancelExecution,
    handleRunFromBlock,
    handleRunUntilBlock,
  }
}
