import { nextTick, type Ref } from "vue"
import type { FocusPosition, FocusRegistry } from "@/composables/useFocusRegistry"
import type { EditorBlockData } from "@/lib/block-parser"
import { generateBlockId } from "@/lib/block-parser"
import {
  deleteIfEmpty as opDeleteIfEmpty,
  indentBlock as opIndentBlock,
  insertBlock as opInsertBlock,
  mergePrevious as opMergePrevious,
  moveBlock,
  outdentBlock as opOutdentBlock,
  splitBlocks as opSplitBlocks,
} from "@/lib/editor-operations"
import { createOperationHistory, type EditorOperation, type OperationHistory } from "@/lib/operation-history"

export interface BlockOperationsDeps {
  blocks: Ref<EditorBlockData[]>
  registry: FocusRegistry
  zoneRefs: Map<number, HTMLElement>
  history: OperationHistory
  canUndo: Ref<boolean>
  canRedo: Ref<boolean>
  liveAnnouncement: Ref<string>
  onBlockContentChange: (blockId?: string, newContent?: string) => void
  syncBlockContent: (id: string, content: string) => void
  log: ReturnType<typeof import("@/lib/logger").createLogger>
}

function record(deps: BlockOperationsDeps, op: EditorOperation) {
  void deps.history.execute(op)
  deps.canUndo.value = deps.history.canUndo
  deps.canRedo.value = deps.history.canRedo
}

function applyBlocks(
  deps: BlockOperationsDeps,
  blocks: EditorBlockData[],
  blockId?: string,
  newContent?: string,
) {
  deps.blocks.value = blocks
  deps.zoneRefs.clear()
  deps.onBlockContentChange(blockId, newContent)
}

function focusNext(deps: BlockOperationsDeps, id: string, pos: FocusPosition) {
  nextTick(() => deps.registry.focus(id, pos))
}

export function useBlockOperations(deps: BlockOperationsDeps) {
  function onSplit(index: number, before: string, after: string) {
    const result = opSplitBlocks(deps.blocks.value, index, before, after, generateBlockId)
    if (!result.newBlock) return
    deps.liveAnnouncement.value = "Block split"
    applyBlocks(deps, result.blocks)
    record(deps, {
      type: "split-block",
      index,
      splitBlockId: result.newBlock.id,
      depth: result.newBlock.depth,
      beforeContent: before,
      afterContent: after,
      timestamp: Date.now(),
    })
    focusNext(deps, result.newBlock.id, "start")
  }

  function onMergePrevious(index: number) {
    const result = opMergePrevious(deps.blocks.value, index)
    if (result.blocks.length >= deps.blocks.value.length) return
    deps.liveAnnouncement.value = "Merged with previous block"

    const prev = deps.blocks.value[index - 1]
    const current = deps.blocks.value[index]
    if (!current || !prev) return

    // Directly sync the prev block's PM content so the debounced watch
    // sees currentContent === newContent and is a no-op.
    deps.syncBlockContent(prev.id, result.mergedContent)

    // Defensively ensure the result block's content matches mergedContent.
    const prevIndex = result.blocks.findIndex((b) => b.id === prev.id)
    if (prevIndex !== -1) result.blocks[prevIndex]!.content = result.mergedContent

    applyBlocks(deps, result.blocks)
    record(deps, {
      type: "merge-block",
      index,
      removedBlockId: current.id,
      removedBlockDepth: current.depth,
      prevContent: prev.content,
      appendedContent: current.content,
      timestamp: Date.now(),
    })
    focusNext(deps, prev.id, "end")
  }

  function onDeleteIfEmpty(index: number) {
    const result = opDeleteIfEmpty(deps.blocks.value, index)
    if (!result.nextId || result.blocks.length >= deps.blocks.value.length) return
    const removedBlock = deps.blocks.value[index]
    if (!removedBlock) return
    deps.liveAnnouncement.value = "Block deleted"

    applyBlocks(deps, result.blocks)
    record(deps, {
      type: "delete-block",
      index,
      blockId: removedBlock.id,
      content: removedBlock.content,
      depth: removedBlock.depth,
      timestamp: Date.now(),
    })

    const prevBlock = index > 0 ? deps.blocks.value[index - 1] : null
    const focusId = prevBlock?.id ?? result.nextId
    focusNext(deps, focusId, prevBlock ? "end" : "start")
  }

  function onIndent(index: number) {
    const block = deps.blocks.value[index]
    if (!block) return
    const result = opIndentBlock(deps.blocks.value, index)
    if (result.childIndices.length === 0 && block.depth >= 10) return

    const childEntries = result.childIndices.map((i) => ({
      index: i,
      previousDepth: deps.blocks.value[i]?.depth ?? 0,
    }))

    applyBlocks(deps, result.blocks)
    record(deps, {
      type: "indent-block",
      index,
      previousDepth: block.depth,
      childrenPreviousDepths: childEntries.length > 0 ? childEntries : undefined,
      timestamp: Date.now(),
    })
  }

  function onOutdent(index: number) {
    const block = deps.blocks.value[index]
    if (!block) return
    const result = opOutdentBlock(deps.blocks.value, index)
    if (result.childIndices.length === 0 && block.depth <= 0) return

    const childEntries = result.childIndices.map((i) => ({
      index: i,
      previousDepth: deps.blocks.value[i]?.depth ?? 0,
    }))

    applyBlocks(deps, result.blocks)
    record(deps, {
      type: "outdent-block",
      index,
      previousDepth: block.depth,
      childrenPreviousDepths: childEntries.length > 0 ? childEntries : undefined,
      timestamp: Date.now(),
    })
  }

  function onDropOnZone(zoneIndex: number, dragState: { fromIndex: number; blockId: string; content: string; depth: number } | null) {
    if (!dragState) return
    const targetIndex = dragState.fromIndex < zoneIndex ? zoneIndex - 1 : zoneIndex
    if (dragState.fromIndex === targetIndex) return

    deps.blocks.value = moveBlock(deps.blocks.value, dragState.fromIndex, targetIndex)
    record(deps, {
      type: "move-block",
      fromIndex: dragState.fromIndex,
      toIndex: targetIndex,
      blockId: dragState.blockId,
      content: dragState.content,
      depth: dragState.depth,
      timestamp: Date.now(),
    })
    deps.zoneRefs.clear()
    deps.onBlockContentChange()
  }

  function onZoneActivate(index: number, content: string) {
    const lines = content.split("\n")
    if (lines.length > 1) {
      // Multi-line paste: create a block for each non-empty line.
      // Only the first insert is recorded in history — undoing a paste should
      // remove all inserted blocks at once, not step through them one by one.
      let insertedId: string | undefined
      let insertIndex = index
      let firstInsertRecorded = false
      for (let i = 0; i < lines.length; i++) {
        const line = lines[i]
        if (line === undefined) continue
        if (line === "" && i < lines.length - 1) continue
        const result = opInsertBlock(deps.blocks.value, insertIndex, line, 0, generateBlockId)
        deps.blocks.value = result.blocks
        insertedId = result.newBlock.id
        if (!firstInsertRecorded) {
          void deps.history.execute({
            type: "insert-block",
            index: insertIndex,
            blockId: result.newBlock.id,
            content: line,
            depth: 0,
            timestamp: Date.now(),
          })
          firstInsertRecorded = true
        }
        insertIndex++
      }
      deps.zoneRefs.clear()
      deps.onBlockContentChange()
      deps.canUndo.value = deps.history.canUndo
      deps.canRedo.value = deps.history.canRedo
      if (insertedId) {
        deps.liveAnnouncement.value = "Inserted block"
        focusNext(deps, insertedId, "end")
      }
    } else {
      const result = opInsertBlock(deps.blocks.value, index, content, 0, generateBlockId)
      deps.blocks.value = result.blocks
      deps.zoneRefs.clear()
      deps.onBlockContentChange()
      record(deps, {
        type: "insert-block",
        index,
        blockId: result.newBlock.id,
        content,
        depth: 0,
        timestamp: Date.now(),
      })
      focusNext(deps, result.newBlock.id, content ? "end" : "start")
    }
  }

  return {
    onSplit,
    onMergePrevious,
    onDeleteIfEmpty,
    onIndent,
    onOutdent,
    onDropOnZone,
    onZoneActivate,
  }
}
