import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import type { Edge } from 'reactflow'
import { getTargetedLayoutImpact } from '@/lib/workflows/autolayout'
import type { BlockWithDiff } from '@/lib/workflows/diff/types'
import { isValidKey } from '@/lib/workflows/sanitization/key-validation'
import { isUuid } from '@/executor/constants'
import { mergeSubblockState } from '@/stores/workflows/utils'
import type { BlockState, WorkflowState } from '@/stores/workflows/workflow/types'

const logger = createLogger('WorkflowDiffEngine')

function parseWorkflowStateJson(jsonContent: string): WorkflowState {
  const parsed = JSON.parse(jsonContent) as unknown
  if (!parsed || typeof parsed !== 'object') {
    throw new Error('Diff content must be a workflow state object')
  }

  const candidate = parsed as Partial<WorkflowState>
  if (
    !candidate.blocks ||
    typeof candidate.blocks !== 'object' ||
    Array.isArray(candidate.blocks) ||
    !Array.isArray(candidate.edges)
  ) {
    throw new Error('Diff content is missing workflow blocks or edges')
  }

  return {
    blocks: candidate.blocks,
    edges: candidate.edges,
    loops: candidate.loops ?? {},
    parallels: candidate.parallels ?? {},
  }
}

// Helper function to check if a block has changed
function hasBlockChanged(currentBlock: BlockState, proposedBlock: BlockState): boolean {
  // Compare key fields that indicate a change
  if (currentBlock.type !== proposedBlock.type) return true
  if (currentBlock.name !== proposedBlock.name) return true
  if (currentBlock.enabled !== proposedBlock.enabled) return true
  if (currentBlock.triggerMode !== proposedBlock.triggerMode) return true
  if (!!currentBlock.locked !== !!proposedBlock.locked) return true
  if ((currentBlock.data?.parentId ?? null) !== (proposedBlock.data?.parentId ?? null)) return true

  // Compare subBlocks
  const currentSubKeys = Object.keys(currentBlock.subBlocks || {})
  const proposedSubKeys = Object.keys(proposedBlock.subBlocks || {})

  if (currentSubKeys.length !== proposedSubKeys.length) return true

  for (const key of currentSubKeys) {
    if (!proposedSubKeys.includes(key)) return true
    const currentSub = currentBlock.subBlocks[key]
    const proposedSub = proposedBlock.subBlocks?.[key]
    if (!proposedSub) return true
    if (JSON.stringify(currentSub.value) !== JSON.stringify(proposedSub.value)) return true
  }

  return false
}

// Helper function to compute field differences between blocks
function computeFieldDiff(
  currentBlock: BlockState,
  proposedBlock: BlockState
): {
  changedFields: string[]
  unchangedFields: string[]
} {
  const changedFields: string[] = []
  const unchangedFields: string[] = []

  // Check basic fields
  const fieldsToCheck = [
    'type',
    'name',
    'enabled',
    'triggerMode',
    'horizontalHandles',
    'locked',
  ] as const
  for (const field of fieldsToCheck) {
    const currentValue = currentBlock[field]
    const proposedValue = proposedBlock[field]
    if (
      field === 'locked'
        ? !!currentValue !== !!proposedValue
        : JSON.stringify(currentValue) !== JSON.stringify(proposedValue)
    ) {
      changedFields.push(field)
    } else if (currentValue !== undefined) {
      unchangedFields.push(field)
    }
  }

  if ((currentBlock.data?.parentId ?? null) !== (proposedBlock.data?.parentId ?? null)) {
    changedFields.push('parentId')
  } else {
    unchangedFields.push('parentId')
  }

  // Check subBlocks - use just the key name for UI compatibility
  const currentSubKeys = Object.keys(currentBlock.subBlocks || {})
  const proposedSubKeys = Object.keys(proposedBlock.subBlocks || {})
  const allSubKeys = new Set([...currentSubKeys, ...proposedSubKeys])

  for (const key of allSubKeys) {
    const currentSub = currentBlock.subBlocks?.[key]
    const proposedSub = proposedBlock.subBlocks?.[key]

    if (!currentSub && proposedSub) {
      // New subblock
      changedFields.push(key)
    } else if (currentSub && !proposedSub) {
      // Deleted subblock
      changedFields.push(key)
    } else if (currentSub && proposedSub) {
      // Check if value changed
      if (JSON.stringify(currentSub.value) !== JSON.stringify(proposedSub.value)) {
        changedFields.push(key)
      } else {
        unchangedFields.push(key)
      }
    }
  }

  return { changedFields, unchangedFields }
}

interface DiffMetadata {
  source: string
  timestamp: number
}

interface EdgeDiff {
  new_edges: string[]
  deleted_edges: string[]
  unchanged_edges: string[]
}

export interface DiffAnalysis {
  new_blocks: string[]
  edited_blocks: string[]
  deleted_blocks: string[]
  field_diffs?: Record<string, { changed_fields: string[]; unchanged_fields: string[] }>
  edge_diff?: EdgeDiff
}

export interface WorkflowDiff {
  proposedState: WorkflowState
  diffAnalysis?: DiffAnalysis
  metadata: DiffMetadata
}

export interface DiffResult {
  success: boolean
  diff?: WorkflowDiff
  errors?: string[]
}

/**
 * Clean diff engine that handles workflow diff operations
 * without polluting core workflow stores
 */
export class WorkflowDiffEngine {
  private currentDiff: WorkflowDiff | undefined = undefined

  /**
   * Create a diff from workflow state
   */
  async createDiff(jsonContent: string, diffAnalysis?: DiffAnalysis): Promise<DiffResult> {
    try {
      logger.info('WorkflowDiffEngine.createDiff called with:', {
        jsonContentLength: jsonContent.length,
        diffAnalysis: diffAnalysis,
        diffAnalysisType: typeof diffAnalysis,
        diffAnalysisUndefined: diffAnalysis === undefined,
        diffAnalysisNull: diffAnalysis === null,
      })

      // Get current workflow state for comparison
      const { useWorkflowStore } = await import('@/stores/workflows/workflow/store')
      const currentWorkflowState = useWorkflowStore.getState().getWorkflowState()

      logger.info('WorkflowDiffEngine current workflow state:', {
        blockCount: Object.keys(currentWorkflowState.blocks || {}).length,
        edgeCount: currentWorkflowState.edges?.length || 0,
        hasLoops: Object.keys(currentWorkflowState.loops || {}).length > 0,
        hasParallels: Object.keys(currentWorkflowState.parallels || {}).length > 0,
      })

      // Merge subblock values from subblock store to ensure manual edits are included in baseline
      let mergedBaseline: WorkflowState = currentWorkflowState
      try {
        mergedBaseline = {
          ...currentWorkflowState,
          blocks: mergeSubblockState(currentWorkflowState.blocks),
        }
        logger.info('Merged subblock values into baseline for diff creation', {
          blockCount: Object.keys(mergedBaseline.blocks || {}).length,
        })
      } catch (mergeError) {
        logger.warn('Failed to merge subblock values into baseline; proceeding with raw state', {
          error: toError(mergeError).message,
        })
      }

      const proposedState = parseWorkflowStateJson(jsonContent)
      return this.createDiffFromWorkflowState(proposedState, diffAnalysis, mergedBaseline)
    } catch (error) {
      logger.error('Failed to create diff:', error)
      return {
        success: false,
        errors: [error instanceof Error ? error.message : 'Failed to create diff'],
      }
    }
  }

  /**
   * Create a diff from a WorkflowState object directly (more efficient than YAML)
   * This follows the same logic as sim-agent's YamlDiffCreate handler
   */
  async createDiffFromWorkflowState(
    proposedState: WorkflowState,
    diffAnalysis?: DiffAnalysis,
    baselineOverride?: WorkflowState
  ): Promise<DiffResult & { diff?: WorkflowDiff }> {
    try {
      logger.info('WorkflowDiffEngine.createDiffFromWorkflowState called with:', {
        blockCount: Object.keys(proposedState.blocks || {}).length,
        edgeCount: proposedState.edges?.length || 0,
        hasDiffAnalysis: !!diffAnalysis,
      })

      // Determine baseline for comparison
      const { useWorkflowStore } = await import('@/stores/workflows/workflow/store')
      const currentWorkflowState = useWorkflowStore.getState().getWorkflowState()

      const hasBaselineOverride = !!baselineOverride
      const baselineForComparison =
        baselineOverride ?? this.currentDiff?.proposedState ?? currentWorkflowState
      const isEditingOnTopOfDiff = !baselineOverride && !!this.currentDiff

      if (isEditingOnTopOfDiff) {
        logger.info('Editing on top of existing diff - using diff as baseline for comparison', {
          diffBlockCount: Object.keys(this.currentDiff!.proposedState.blocks).length,
        })
      }

      // Merge subblock values from subblock store to ensure manual edits are included
      let mergedBaseline: WorkflowState = baselineForComparison

      // Only merge subblock values if we're comparing against original workflow
      // If editing on top of diff or using an explicit override, trust provided values
      if (!isEditingOnTopOfDiff && !hasBaselineOverride) {
        try {
          mergedBaseline = {
            ...baselineForComparison,
            blocks: mergeSubblockState(baselineForComparison.blocks),
          }
          logger.info('Merged subblock values into baseline for diff creation', {
            blockCount: Object.keys(mergedBaseline.blocks || {}).length,
          })
        } catch (mergeError) {
          logger.warn('Failed to merge subblock values into baseline; proceeding with raw state', {
            error: toError(mergeError).message,
          })
        }
      } else {
        logger.info(
          'Using diff state as baseline without merging subblocks (editing on top of diff)'
        )
      }

      // Build a map of existing blocks by type:name for matching
      const existingBlockMap: Record<string, { id: string; block: BlockState }> = {}
      for (const [id, block] of Object.entries(mergedBaseline.blocks)) {
        const key = `${block.type}:${block.name}`
        existingBlockMap[key] = { id, block }
      }

      // Create ID mapping - preserve existing IDs where blocks match by type:name
      const idMap: Record<string, string> = {}
      const finalBlocks: Record<string, BlockState & BlockWithDiff> = {}

      // First pass: build ID mappings
      for (const [proposedId, proposedBlock] of Object.entries(proposedState.blocks)) {
        // CRITICAL: Skip invalid block IDs to prevent "undefined" keys in workflow state
        if (!isValidKey(proposedId)) {
          logger.error('Invalid proposedId detected in proposed state', {
            proposedId,
            proposedId_type: typeof proposedId,
            blockType: proposedBlock?.type,
            blockName: proposedBlock?.name,
          })
          continue
        }

        const key = `${proposedBlock.type}:${proposedBlock.name}`

        // Check if this block exists in current state by type:name
        if (existingBlockMap[key]) {
          // Preserve existing ID
          idMap[proposedId] = existingBlockMap[key].id
        } else if (isUuid(proposedId)) {
          // New block with a valid UUID (e.g., from server's DB save) — keep it.
          // Minting a new UUID would create a mismatch between the client store
          // and the database, causing foreign key violations on subsequent
          // socket operations.
          idMap[proposedId] = proposedId
        } else {
          // New block with a non-UUID ID (e.g., from YAML parsing) — mint a UUID
          idMap[proposedId] = generateId()
        }
      }

      // Second pass: build final blocks with mapped IDs
      for (const [proposedId, proposedBlock] of Object.entries(proposedState.blocks)) {
        // CRITICAL: Skip invalid block IDs to prevent "undefined" keys in workflow state
        if (!isValidKey(proposedId)) {
          logger.error('Invalid proposedId detected in proposed state (second pass)', {
            proposedId,
            proposedId_type: typeof proposedId,
            blockType: proposedBlock?.type,
            blockName: proposedBlock?.name,
          })
          continue
        }

        const finalId = idMap[proposedId]

        // CRITICAL: Validate finalId before using as key
        if (!isValidKey(finalId)) {
          logger.error('Invalid finalId generated from idMap', {
            proposedId,
            finalId,
            finalId_type: typeof finalId,
            blockType: proposedBlock?.type,
            blockName: proposedBlock?.name,
          })
          continue
        }

        const key = `${proposedBlock.type}:${proposedBlock.name}`
        const existingBlock = existingBlockMap[key]?.block

        // Merge with existing block if found, otherwise use proposed.
        // Position strategy depends on whether the block's scope changed:
        // - Same scope (parentId unchanged): preserve existing position so
        //   unchanged blocks anchor correctly for targeted layout.
        // - Different scope (parentId changed): use proposed position because
        //   the coordinate system changed (absolute ↔ relative-to-container).
        const existingParent = existingBlock?.data?.parentId ?? null
        const proposedParentRaw = proposedBlock.data?.parentId ?? null
        const proposedParent = proposedParentRaw
          ? (idMap[proposedParentRaw] ?? proposedParentRaw)
          : null
        const scopeChanged = existingBlock ? existingParent !== proposedParent : false

        const finalBlock: BlockState & BlockWithDiff = existingBlock
          ? {
              ...existingBlock,
              ...proposedBlock,
              id: finalId,
              position: scopeChanged ? proposedBlock.position : existingBlock.position,
            }
          : {
              ...proposedBlock,
              id: finalId,
            }

        // Update parentId in data if it exists and has been remapped
        if (finalBlock.data?.parentId && idMap[finalBlock.data.parentId]) {
          finalBlock.data = {
            ...finalBlock.data,
            parentId: idMap[finalBlock.data.parentId],
          }
        }

        finalBlocks[finalId] = finalBlock
      }

      // Map edges with new IDs and standardized handles
      const edgeMap = new Map<string, Edge>()

      proposedState.edges.forEach((edge) => {
        const source = idMap[edge.source] || edge.source
        const target = idMap[edge.target] || edge.target
        const sourceHandle = edge.sourceHandle || 'source'
        const targetHandle = edge.targetHandle || 'target'

        // Create a unique key for deduplication
        const edgeKey = `${source}-${sourceHandle}-${target}-${targetHandle}`

        // Only add if we haven't seen this edge combination before
        if (!edgeMap.has(edgeKey)) {
          edgeMap.set(edgeKey, {
            ...edge,
            id: generateId(), // Use UUID for unique edge IDs
            source,
            target,
            sourceHandle,
            targetHandle,
            type: edge.type || 'workflowEdge',
          })
        }
      })

      const finalEdges: Edge[] = Array.from(edgeMap.values())

      // Build final proposed state
      // Always regenerate loops and parallels from finalBlocks because the block IDs may have
      // been remapped (via idMap) and the server's loops/parallels would have stale references.
      // This ensures the nodes arrays in loops/parallels contain the correct (remapped) block IDs,
      // which is critical for variable resolution in the tag dropdown.
      const { generateLoopBlocks, generateParallelBlocks } = await import(
        '@/stores/workflows/workflow/utils'
      )

      // Build the proposed state
      const finalProposedState: WorkflowState = {
        blocks: finalBlocks,
        edges: finalEdges,
        loops: generateLoopBlocks(finalBlocks),
        parallels: generateParallelBlocks(finalBlocks),
        lastSaved: Date.now(),
      }

      // Use the proposed state directly - validation happens at the source
      const fullyCleanedState = finalProposedState

      // Transfer block heights from baseline workflow for better measurements in diff view
      // If editing on top of diff, this transfers from the diff (which already has good heights)
      // Otherwise transfers from original workflow
      logger.info('Transferring block heights from baseline workflow', {
        isEditingOnTopOfDiff,
        baselineBlockCount: Object.keys(mergedBaseline.blocks).length,
      })
      try {
        const { transferBlockHeights } = await import('@/lib/workflows/autolayout')
        transferBlockHeights(mergedBaseline.blocks, finalBlocks)
      } catch (error) {
        logger.warn('Failed to transfer block heights', {
          error: toError(error).message,
        })
      }

      // Apply autolayout to the proposed state
      logger.info('Applying autolayout to proposed workflow state')
      try {
        const { layoutBlockIds, resizedBlockIds, shiftSourceBlockIds } = getTargetedLayoutImpact({
          before: mergedBaseline,
          after: fullyCleanedState,
        })

        const totalBlocks = Object.keys(finalBlocks).length

        if (
          layoutBlockIds.length === 0 &&
          resizedBlockIds.length === 0 &&
          shiftSourceBlockIds.length === 0
        ) {
          logger.info('No blocks need layout; skipping autolayout', {
            totalBlocks,
          })
        } else {
          // Always use targeted layout for copilot edits. When anchors exist
          // (some blocks unchanged), they preserve user positions. When no
          // anchors exist (all blocks are new), targeted layout degrades
          // gracefully to a full layout from the padding origin — same result
          // as applyAutoLayout but with one unified code path.
          logger.info('Using targeted layout for copilot edits', {
            blocksNeedingLayout: layoutBlockIds.length,
            resizedAnchorBlocks: resizedBlockIds.length,
            shiftSourceBlocks: shiftSourceBlockIds.length,
            anchors: totalBlocks - layoutBlockIds.length,
            totalBlocks,
          })

          const { applyTargetedLayout } = await import('@/lib/workflows/autolayout')
          const { DEFAULT_HORIZONTAL_SPACING, DEFAULT_VERTICAL_SPACING } = await import(
            '@/lib/workflows/autolayout/constants'
          )

          const layoutedBlocks = applyTargetedLayout(finalBlocks, fullyCleanedState.edges, {
            changedBlockIds: layoutBlockIds,
            resizedBlockIds,
            shiftSourceBlockIds,
            horizontalSpacing: DEFAULT_HORIZONTAL_SPACING,
            verticalSpacing: DEFAULT_VERTICAL_SPACING,
          })

          Object.entries(layoutedBlocks).forEach(([id, layoutBlock]) => {
            if (finalBlocks[id]) {
              finalBlocks[id].position = layoutBlock.position

              if (layoutBlock.data) {
                finalBlocks[id].data = {
                  ...finalBlocks[id].data,
                  ...layoutBlock.data,
                }
              }

              if (layoutBlock.layout) {
                finalBlocks[id].layout = {
                  ...finalBlocks[id].layout,
                  ...layoutBlock.layout,
                }
              }

              if (typeof layoutBlock.height === 'number') {
                finalBlocks[id].height = layoutBlock.height
              }
            }
          })

          logger.info('Successfully applied targeted layout to proposed state', {
            blocksLayouted: Object.keys(layoutedBlocks).length,
            blocksNeedingLayout: layoutBlockIds.length,
          })
        }
      } catch (layoutError) {
        logger.warn('Error applying autolayout, using default positions', {
          error: toError(layoutError).message,
        })
      }

      // Compute diff analysis if not provided
      let computed = diffAnalysis
      if (!computed) {
        // Generate diff analysis between current and proposed states
        const currentIds = new Set(Object.keys(mergedBaseline.blocks))
        const proposedIds = new Set(Object.keys(finalBlocks))

        const newBlocks: string[] = []
        const editedBlocks: string[] = []
        const deletedBlocks: string[] = []

        // Find new and edited blocks
        for (const [id, block] of Object.entries(finalBlocks)) {
          if (!currentIds.has(id)) {
            newBlocks.push(id)
          } else {
            // Check if block was edited by comparing key fields
            const currentBlock = mergedBaseline.blocks[id]
            if (hasBlockChanged(currentBlock, block)) {
              editedBlocks.push(id)
            }
          }
        }

        // Find deleted blocks
        for (const id of currentIds) {
          if (!proposedIds.has(id)) {
            deletedBlocks.push(id)
          }
        }

        // Compute field diffs for edited blocks
        const fieldDiffs: Record<string, { changed_fields: string[]; unchanged_fields: string[] }> =
          {}
        for (const id of editedBlocks) {
          const currentBlock = mergedBaseline.blocks[id]
          const proposedBlock = finalBlocks[id]
          const { changedFields, unchangedFields } = computeFieldDiff(currentBlock, proposedBlock)
          if (changedFields.length > 0) {
            fieldDiffs[id] = {
              changed_fields: changedFields,
              unchanged_fields: unchangedFields,
            }
          }
        }

        // Compute edge diffs
        const currentEdgeSet = new Set<string>()
        const proposedEdgeSet = new Set<string>()

        // Create edge identifiers for current state (using sim-agent format)
        mergedBaseline.edges.forEach((edge: Edge) => {
          const edgeId = `${edge.source}-${edge.sourceHandle || 'source'}-${edge.target}-${edge.targetHandle || 'target'}`
          currentEdgeSet.add(edgeId)
        })

        // Create edge identifiers for proposed state
        fullyCleanedState.edges.forEach((edge) => {
          const edgeId = `${edge.source}-${edge.sourceHandle || 'source'}-${edge.target}-${edge.targetHandle || 'target'}`
          proposedEdgeSet.add(edgeId)
        })

        // Classify edges
        const newEdges: string[] = []
        const deletedEdges: string[] = []
        const unchangedEdges: string[] = []

        // Find new edges (in proposed but not current)
        proposedEdgeSet.forEach((edgeId) => {
          if (!currentEdgeSet.has(edgeId)) {
            newEdges.push(edgeId)
          } else {
            unchangedEdges.push(edgeId)
          }
        })

        // Find deleted edges (in current but not proposed)
        currentEdgeSet.forEach((edgeId) => {
          if (!proposedEdgeSet.has(edgeId)) {
            deletedEdges.push(edgeId)
          }
        })

        computed = {
          new_blocks: newBlocks,
          edited_blocks: editedBlocks,
          deleted_blocks: deletedBlocks,
          field_diffs: Object.keys(fieldDiffs).length > 0 ? fieldDiffs : undefined,
          edge_diff: {
            new_edges: newEdges,
            deleted_edges: deletedEdges,
            unchanged_edges: unchangedEdges,
          },
        }
      }

      // Apply diff markers to blocks in the fully cleaned state
      if (computed) {
        for (const id of computed.new_blocks || []) {
          if (fullyCleanedState.blocks[id]) {
            ;(fullyCleanedState.blocks[id] as any).is_diff = 'new'
          }
        }
        for (const id of computed.edited_blocks || []) {
          if (fullyCleanedState.blocks[id]) {
            ;(fullyCleanedState.blocks[id] as any).is_diff = 'edited'

            // Also mark specific subblocks that changed
            if (computed.field_diffs?.[id]) {
              const fieldDiff = computed.field_diffs[id]
              const block = fullyCleanedState.blocks[id]

              // Apply diff markers to changed subblocks
              for (const changedField of fieldDiff.changed_fields) {
                if (block.subBlocks?.[changedField]) {
                  // Add a diff marker to the subblock itself
                  ;(block.subBlocks[changedField] as any).is_diff = 'changed'
                }
              }
            }
          }
        }
        // Note: We don't remove deleted blocks from fullyCleanedState, just mark them
      }

      // Store the diff with the fully sanitized state
      this.currentDiff = {
        proposedState: fullyCleanedState,
        diffAnalysis: computed,
        metadata: {
          source: 'workflow_state',
          timestamp: Date.now(),
        },
      }

      logger.info('Successfully created diff from workflow state', {
        blockCount: Object.keys(fullyCleanedState.blocks).length,
        edgeCount: fullyCleanedState.edges.length,
        hasLoops: Object.keys(fullyCleanedState.loops || {}).length > 0,
        hasParallels: Object.keys(fullyCleanedState.parallels || {}).length > 0,
        newBlocks: computed?.new_blocks?.length || 0,
        editedBlocks: computed?.edited_blocks?.length || 0,
        deletedBlocks: computed?.deleted_blocks?.length || 0,
        newEdges: computed?.edge_diff?.new_edges?.length || 0,
        deletedEdges: computed?.edge_diff?.deleted_edges?.length || 0,
        unchangedEdges: computed?.edge_diff?.unchanged_edges?.length || 0,
      })

      if (computed?.edge_diff?.deleted_edges && computed.edge_diff.deleted_edges.length > 0) {
        logger.info('Deleted edges detected:', {
          deletedEdges: computed.edge_diff.deleted_edges,
        })
      }

      return {
        success: true,
        diff: this.currentDiff,
      }
    } catch (error) {
      logger.error('Failed to create diff from workflow state:', error)
      return {
        success: false,
        errors: [
          error instanceof Error ? error.message : 'Failed to create diff from workflow state',
        ],
      }
    }
  }

  /**
   * Merge new workflow state into existing diff
   * Used for cumulative updates within the same message
   */
  async mergeDiff(jsonContent: string, diffAnalysis?: DiffAnalysis): Promise<DiffResult> {
    try {
      logger.info('Merging diff from workflow state')

      // If no existing diff, create a new one
      if (!this.currentDiff) {
        logger.info('No existing diff, creating new diff')
        return this.createDiff(jsonContent, diffAnalysis)
      }

      const proposedState = parseWorkflowStateJson(jsonContent)
      const result = await this.createDiffFromWorkflowState(
        proposedState,
        diffAnalysis,
        this.currentDiff.proposedState
      )

      if (result.success && result.diff) {
        logger.info('Diff merged successfully', {
          totalBlocksCount: Object.keys(result.diff.proposedState.blocks).length,
          totalEdgesCount: result.diff.proposedState.edges.length,
        })
      }

      return result
    } catch (error) {
      logger.error('Failed to merge diff:', error)
      return {
        success: false,
        errors: [error instanceof Error ? error.message : 'Failed to merge diff'],
      }
    }
  }

  /**
   * Get the current diff
   */
  getCurrentDiff(): WorkflowDiff | undefined {
    return this.currentDiff
  }

  /**
   * Clear the current diff
   */
  clearDiff(): void {
    this.currentDiff = undefined
    logger.info('Diff cleared')
  }

  /**
   * Check if a diff is active
   */
  hasDiff(): boolean {
    return this.currentDiff !== undefined
  }

  /**
   * Get the workflow state for display (either diff or provided state)
   */
  getDisplayState(currentState: WorkflowState): WorkflowState {
    if (this.currentDiff) {
      return this.currentDiff.proposedState
    }
    return currentState
  }

  /**
   * Accept the diff and return the clean state
   */
  acceptDiff(): WorkflowState | null {
    if (!this.currentDiff) {
      logger.warn('No diff to accept')
      return null
    }

    try {
      // Clean up the proposed state by removing diff markers
      const cleanState = stripWorkflowDiffMarkers(this.currentDiff.proposedState)

      logger.info('Diff accepted', {
        blocksCount: Object.keys(cleanState.blocks).length,
        edgesCount: cleanState.edges.length,
        loopsCount: Object.keys(cleanState.loops).length,
        parallelsCount: Object.keys(cleanState.parallels).length,
      })

      this.clearDiff()
      return cleanState
    } catch (error) {
      logger.error('Failed to accept diff:', error)
      return null
    }
  }
}

/**
 * Removes diff metadata from a workflow state so it can be persisted or re-used safely.
 */
export function stripWorkflowDiffMarkers(state: WorkflowState): WorkflowState {
  const cleanBlocks: Record<string, BlockState> = {}

  for (const [blockId, block] of Object.entries(state.blocks || {})) {
    // Validate block ID at the source - skip invalid IDs
    if (!isValidKey(blockId)) {
      logger.error('Invalid blockId detected in stripWorkflowDiffMarkers', {
        blockId,
        blockId_type: typeof blockId,
        blockType: block?.type,
        blockName: block?.name,
      })
      continue
    }

    const cleanBlock: BlockState = structuredClone(block)
    const blockWithDiff = cleanBlock as BlockState & BlockWithDiff
    blockWithDiff.is_diff = undefined
    blockWithDiff.field_diffs = undefined

    if (cleanBlock.subBlocks) {
      Object.values(cleanBlock.subBlocks).forEach((subBlock) => {
        if (subBlock && typeof subBlock === 'object') {
          ;(subBlock as any).is_diff = undefined
        }
      })
    }

    if (cleanBlock.outputs === undefined || cleanBlock.outputs === null) {
      cleanBlock.outputs = {}
    }

    cleanBlocks[blockId] = cleanBlock
  }

  return {
    ...state,
    blocks: cleanBlocks,
    edges: structuredClone(state.edges || []),
    loops: structuredClone(state.loops || {}),
    parallels: structuredClone(state.parallels || {}),
  }
}
