import { Extension } from "@tiptap/core"
import type { Editor } from "@tiptap/react"
import type { Node as PMNode } from "@tiptap/pm/model"
import { Plugin, PluginKey } from "@tiptap/pm/state"
import { Decoration, DecorationSet, type EditorView } from "@tiptap/pm/view"

export const PAGE_FLOW_PLUGIN_KEY = new PluginKey<DecorationSet>("docsPageFlowDecoration")

type FlowUnit = { height: number; offset: number }

const LIST_TYPES = new Set(["bulletList", "orderedList", "taskList"])

function decorationSetsEqual(
  current: DecorationSet | undefined,
  next: DecorationSet
): boolean {
  if (!current || current === DecorationSet.empty) {
    return next === DecorationSet.empty || next.find(0, Number.MAX_SAFE_INTEGER).length === 0
  }
  const a = current.find(0, Number.MAX_SAFE_INTEGER)
  const b = next.find(0, Number.MAX_SAFE_INTEGER)
  if (a.length !== b.length) return false
  for (let i = 0; i < a.length; i++) {
    if (a[i].from !== b[i].from) return false
    const aPush = (a[i].spec as { pushPx?: number }).pushPx
    const bPush = (b[i].spec as { pushPx?: number }).pushPx
    if (aPush !== bPush) return false
  }
  return true
}

export function readPageFlowMetrics(prose: HTMLElement): {
  bodyAreaH: number
  interPageSpacer: number
} | null {
  const surface = prose.closest(".ultidrive-docs-editor-surface")
  if (!surface) return null
  const styles = getComputedStyle(surface)
  const bodyAreaH = parseFloat(styles.getPropertyValue("--docs-body-area-h"))
  const interPageSpacer = parseFloat(styles.getPropertyValue("--docs-inter-page-spacer"))
  if (!Number.isFinite(bodyAreaH) || bodyAreaH <= 0) return null
  if (!Number.isFinite(interPageSpacer) || interPageSpacer <= 0) return null
  return { bodyAreaH, interPageSpacer }
}

function blockDomAtOffset(view: EditorView, offset: number): HTMLElement | null {
  const dom = view.nodeDOM(offset)
  if (dom instanceof HTMLElement) {
    if (dom.parentElement === view.dom) return dom
    let el: HTMLElement | null = dom
    while (el && el.parentElement !== view.dom) el = el.parentElement
    return el
  }
  return null
}

function nestedDomAt(view: EditorView, offset: number, selector: string): HTMLElement | null {
  const dom = view.nodeDOM(offset)
  if (!(dom instanceof HTMLElement)) return null
  if (dom.matches(selector)) return dom
  return dom.closest(selector)
}

/** Block height in prose px (offsetHeight + bottom margin; immune to canvas scale). */
function measureBlockFlowHeight(dom: HTMLElement): number {
  const style = getComputedStyle(dom)
  const marginBottom = parseFloat(style.marginBottom) || 0
  return dom.offsetHeight + marginBottom
}

function measureVisualLineHeights(dom: HTMLElement): Array<{ top: number; height: number }> {
  const blockTop = dom.getBoundingClientRect().top
  const range = document.createRange()
  const lines: Array<{ top: number; height: number }> = []

  const walker = document.createTreeWalker(dom, NodeFilter.SHOW_TEXT)
  while (walker.nextNode()) {
    const text = walker.currentNode as Text
    if (!text.nodeValue?.length) continue
    range.selectNodeContents(text)
    for (const rect of range.getClientRects()) {
      if (rect.width <= 0 || rect.height <= 0) continue
      const relTop = rect.top - blockTop
      const last = lines[lines.length - 1]
      if (!last || Math.abs(last.top - relTop) > 2) {
        lines.push({ top: relTop, height: rect.height })
      } else {
        last.height = Math.max(last.height, rect.height)
      }
    }
  }

  if (lines.length === 0) {
    const h = dom.offsetHeight
    if (h > 0) lines.push({ top: 0, height: h })
  }

  return lines
}

function findPosAtVisualLine(
  view: EditorView,
  dom: HTMLElement,
  blockRect: DOMRect,
  relTop: number
): number | null {
  const coords = view.posAtCoords({
    left: blockRect.left + Math.min(Math.max(1, blockRect.width / 2), blockRect.width - 1),
    top: blockRect.top + relTop + 1,
  })
  return coords?.pos ?? null
}

function estimatedLineUnits(
  view: EditorView,
  node: PMNode,
  offset: number,
  dom: HTMLElement,
  totalH: number
): FlowUnit[] {
  const style = getComputedStyle(dom)
  let lineHeight = parseFloat(style.lineHeight)
  if (!Number.isFinite(lineHeight) || style.lineHeight === "normal") {
    lineHeight = (parseFloat(style.fontSize) || 16) * 1.25
  }
  const lineCount = Math.max(1, Math.ceil(totalH / lineHeight))
  const textLen = Math.max(1, node.content.size)
  const units: FlowUnit[] = []

  for (let i = 0; i < lineCount; i++) {
    const h =
      i === lineCount - 1
        ? Math.max(1, totalH - lineHeight * (lineCount - 1))
        : lineHeight
    const pos =
      i === 0
        ? offset
        : offset + Math.min(textLen - 1, Math.floor((textLen * i) / lineCount))
    units.push({ height: h, offset: pos })
  }

  return units
}

function expandTallLeafUnits(
  view: EditorView,
  node: PMNode,
  offset: number,
  dom: HTMLElement,
  totalH: number,
  bodyAreaH: number
): FlowUnit[] {
  if (totalH <= bodyAreaH) {
    return [{ height: totalH, offset }]
  }

  const lineSegments = measureVisualLineHeights(dom)
  if (lineSegments.length <= 1) {
    return estimatedLineUnits(view, node, offset, dom, totalH)
  }

  const blockRect = dom.getBoundingClientRect()
  return lineSegments.map((segment, lineIndex) => ({
    height: segment.height,
    offset:
      lineIndex === 0
        ? offset
        : (findPosAtVisualLine(view, dom, blockRect, segment.top) ?? offset),
  }))
}

function expandLeafBlockUnits(
  view: EditorView,
  node: PMNode,
  offset: number,
  units: FlowUnit[],
  bodyAreaH: number
): void {
  const dom = blockDomAtOffset(view, offset)
  if (!dom) return
  const totalH = measureBlockFlowHeight(dom)
  if (totalH <= 0) return
  units.push(...expandTallLeafUnits(view, node, offset, dom, totalH, bodyAreaH))
}

function expandNodeUnits(
  view: EditorView,
  node: PMNode,
  offset: number,
  units: FlowUnit[],
  bodyAreaH: number
): void {
  if (!node.isBlock) return

  if (LIST_TYPES.has(node.type.name)) {
    let itemOffset = offset + 1
    for (let j = 0; j < node.childCount; j++) {
      const item = node.child(j)
      const liDom = nestedDomAt(view, itemOffset, "li")
      const h = liDom ? measureBlockFlowHeight(liDom) : 0
      if (h > 0) {
        units.push(...expandTallLeafUnits(view, item, itemOffset, liDom, h, bodyAreaH))
      }
      itemOffset += item.nodeSize
    }
    return
  }

  if (node.type.name === "table") {
    let rowOffset = offset + 1
    for (let j = 0; j < node.childCount; j++) {
      const row = node.child(j)
      if (row.type.name !== "tableRow") {
        rowOffset += row.nodeSize
        continue
      }
      const rowDom = nestedDomAt(view, rowOffset, "tr")
      const h = rowDom ? measureBlockFlowHeight(rowDom) : 0
      if (h > 0) {
        units.push(...expandTallLeafUnits(view, row, rowOffset, rowDom, h, bodyAreaH))
      }
      rowOffset += row.nodeSize
    }
    return
  }

  if (node.type.name === "blockquote") {
    let innerOffset = offset + 1
    for (let j = 0; j < node.childCount; j++) {
      const child = node.child(j)
      if (child.isBlock) {
        expandLeafBlockUnits(view, child, innerOffset, units, bodyAreaH)
      }
      innerOffset += child.nodeSize
    }
    return
  }

  expandLeafBlockUnits(view, node, offset, units, bodyAreaH)
}

function collectFlowUnits(view: EditorView, bodyAreaH: number): FlowUnit[] {
  const units: FlowUnit[] = []
  const doc = view.state.doc
  let offset = 0

  for (let i = 0; i < doc.childCount; i++) {
    const node = doc.child(i)
    expandNodeUnits(view, node, offset, units, bodyAreaH)
    offset += node.nodeSize
  }

  return units
}

function createSpacerElement(height: number): HTMLElement {
  const el = document.createElement("div")
  el.className = "docs-page-flow-spacer"
  el.style.cssText = `display:block;width:100%;height:${height}px;margin:0;padding:0;border:0;flex-shrink:0`
  el.setAttribute("aria-hidden", "true")
  el.contentEditable = "false"
  return el
}

/** Simulate vertical flow without reading pushed DOM positions (avoids clearing decorations). */
export function computePageFlowPushes(
  blocks: Array<{ height: number }>,
  bodyAreaH: number,
  interPageSpacer: number
): Array<{ blockIndex: number; pushPx: number; breakY: number }> {
  const pageStep = bodyAreaH + interPageSpacer
  const pushes: Array<{ blockIndex: number; pushPx: number; breakY: number }> = []
  let simulatedY = 0
  let nextBreakY = bodyAreaH

  blocks.forEach(({ height: blockH }, blockIndex) => {
    if (blockH <= 0) return

    while (simulatedY + blockH > nextBreakY) {
      const nextBodyStart = nextBreakY + interPageSpacer

      if (simulatedY < nextBreakY) {
        const pushPx = nextBreakY - simulatedY + interPageSpacer
        pushes.push({ blockIndex, pushPx, breakY: nextBreakY })
        simulatedY = nextBodyStart
        nextBreakY += pageStep
        break
      }

      if (simulatedY < nextBodyStart) {
        const pushPx = nextBodyStart - simulatedY
        pushes.push({ blockIndex, pushPx, breakY: nextBreakY })
        simulatedY = nextBodyStart
        nextBreakY += pageStep
        break
      }

      nextBreakY += pageStep
    }

    simulatedY += blockH
  })

  return pushes
}

export function computeSimulatedLayoutHeight(
  blocks: Array<{ height: number }>,
  bodyAreaH: number,
  interPageSpacer: number
): number {
  const pageStep = bodyAreaH + interPageSpacer
  let simulatedY = 0
  let nextBreakY = bodyAreaH

  for (const { height: blockH } of blocks) {
    if (blockH <= 0) continue
    while (simulatedY + blockH > nextBreakY) {
      const nextBodyStart = nextBreakY + interPageSpacer

      if (simulatedY < nextBreakY) {
        simulatedY = nextBodyStart
        nextBreakY += pageStep
        break
      }

      if (simulatedY < nextBodyStart) {
        simulatedY = nextBodyStart
        nextBreakY += pageStep
        break
      }

      nextBreakY += pageStep
    }
    simulatedY += blockH
  }

  return simulatedY
}

/** Build page-flow spacer widgets for blocks that cross a page body boundary. */
export function buildPageFlowDecorations(view: EditorView): DecorationSet {
  const metrics = readPageFlowMetrics(view.dom)
  if (!metrics) return DecorationSet.empty

  const { bodyAreaH, interPageSpacer } = metrics
  const units = collectFlowUnits(view, bodyAreaH)

  const pushes = computePageFlowPushes(
    units.map((u) => ({ height: u.height })),
    bodyAreaH,
    interPageSpacer
  )

  const decorations = pushes.map(({ blockIndex, pushPx, breakY }) => {
    const unit = units[blockIndex]
    return Decoration.widget(
      unit.offset,
      () => createSpacerElement(pushPx),
      { side: -1, key: `page-flow-spacer-${unit.offset}-${breakY}`, pushPx }
    )
  })

  return DecorationSet.create(view.state.doc, decorations)
}

/** Measure paginated prose height using the same flow units as layout. */
export function measureFlowContentHeight(view: EditorView): number {
  const metrics = readPageFlowMetrics(view.dom)
  if (!metrics) return 0

  const units = collectFlowUnits(view, metrics.bodyAreaH)
  if (units.length === 0) return 0

  const simulated = computeSimulatedLayoutHeight(
    units.map((u) => ({ height: u.height })),
    metrics.bodyAreaH,
    metrics.interPageSpacer
  )

  let maxBottom = 0
  for (const child of view.dom.children) {
    const el = child as HTMLElement
    maxBottom = Math.max(maxBottom, el.offsetTop + el.offsetHeight)
  }

  return Math.max(simulated, maxBottom)
}

/** Apply page-flow layout once. Returns true if decorations changed. */
export function applyPageFlowLayout(editor: Editor): boolean {
  if (editor.isDestroyed || !editor.isInitialized) return false
  const view = editor.view
  const next = buildPageFlowDecorations(view)
  const current = PAGE_FLOW_PLUGIN_KEY.getState(view.state)
  if (decorationSetsEqual(current, next)) return false
  const tr = view.state.tr.setMeta(PAGE_FLOW_PLUGIN_KEY, next)
  tr.setMeta("addToHistory", false)
  view.dispatch(tr)
  return true
}

/** Pure helper for tests: return count of inter-page pushes. */
export function countPageFlowSpacers(
  blocks: Array<{ height: number }>,
  bodyAreaH: number,
  interPageSpacer: number
): number {
  return computePageFlowPushes(blocks, bodyAreaH, interPageSpacer).length
}

/** State-only plugin: layout is driven from docs-page-view (no view plugin / observers). */
export const DocsPageFlowDecoration = Extension.create({
  name: "docsPageFlowDecoration",

  addProseMirrorPlugins() {
    return [
      new Plugin({
        key: PAGE_FLOW_PLUGIN_KEY,
        state: {
          init: () => DecorationSet.empty,
          apply(tr, set) {
            const next = tr.getMeta(PAGE_FLOW_PLUGIN_KEY)
            if (next instanceof DecorationSet) return next
            return set.map(tr.mapping, tr.doc)
          },
        },
        props: {
          decorations(state) {
            return PAGE_FLOW_PLUGIN_KEY.getState(state) ?? DecorationSet.empty
          },
        },
      }),
    ]
  },
})
