import { type ClipAnchor } from './clipTypes'

type TextNodeIndex = {
  node: Text
  start: number
  end: number
}

function collectTextNodeIndex(): TextNodeIndex[] {
  const root = document.body || document.documentElement
  if (!root) return []
  const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT)
  const nodes: TextNodeIndex[] = []
  let cursor = 0
  let current = walker.nextNode()
  while (current) {
    const text = current as Text
    const value = text.nodeValue || ''
    const length = value.length
    if (length > 0) {
      nodes.push({
        node: text,
        start: cursor,
        end: cursor + length,
      })
      cursor += length
    }
    current = walker.nextNode()
  }
  return nodes
}

function fullTextFromNodes(nodes: TextNodeIndex[]): string {
  return nodes.map((item) => item.node.nodeValue || '').join('')
}

function xpathFromNode(node: Node): string {
  const root = document.body || document.documentElement
  if (!root) return ''
  if (node === root) return '/body'
  const segments: string[] = []
  let current: Node | null = node.nodeType === Node.TEXT_NODE ? node.parentNode : node
  while (current && current !== root && current.nodeType === Node.ELEMENT_NODE) {
    const element = current as Element
    const tag = element.tagName.toLowerCase()
    let index = 1
    let sibling = element.previousElementSibling
    while (sibling) {
      if (sibling.tagName.toLowerCase() === tag) index += 1
      sibling = sibling.previousElementSibling
    }
    segments.unshift(`${tag}[${index}]`)
    current = element.parentElement
  }
  return `/body/${segments.join('/')}`
}

function nodeFromXpath(xpath: string): Node | null {
  if (!xpath) return null
  try {
    const result = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null)
    return result.singleNodeValue
  } catch {
    return null
  }
}

function isRangeSelectable(range: Range): boolean {
  const common = range.commonAncestorContainer
  const element = common.nodeType === Node.ELEMENT_NODE ? (common as Element) : common.parentElement
  if (!element) return false
  if (element.closest('input, textarea, select, button, script, style')) return false
  if (element.closest('[contenteditable="true"]')) return false
  return true
}

function buildTextContext(raw: string, start: number, end: number): { prefix: string; suffix: string } {
  const prefix = raw.slice(Math.max(0, start - 40), start).trim()
  const suffix = raw.slice(end, Math.min(raw.length, end + 40)).trim()
  return {
    prefix,
    suffix,
  }
}

function rangeOffsets(range: Range, nodes: TextNodeIndex[]): { start: number; end: number } | null {
  const startNode = range.startContainer
  const endNode = range.endContainer
  if (startNode.nodeType !== Node.TEXT_NODE || endNode.nodeType !== Node.TEXT_NODE) return null
  const startHit = nodes.find((item) => item.node === startNode)
  const endHit = nodes.find((item) => item.node === endNode)
  if (!startHit || !endHit) return null
  return {
    start: startHit.start + range.startOffset,
    end: endHit.start + range.endOffset,
  }
}

function rangeFromAbsoluteOffsets(start: number, end: number): Range | null {
  if (!Number.isFinite(start) || !Number.isFinite(end) || start >= end) return null
  const nodes = collectTextNodeIndex()
  const startHit = nodes.find((item) => start >= item.start && start < item.end)
  const endHit = nodes.find((item) => end > item.start && end <= item.end)
  if (!startHit || !endHit) return null
  const range = document.createRange()
  range.setStart(startHit.node, Math.max(0, Math.min(startHit.node.length, start - startHit.start)))
  range.setEnd(endHit.node, Math.max(0, Math.min(endHit.node.length, end - endHit.start)))
  return range
}

function rangeFromXpathOffsets(anchor: ClipAnchor): Range | null {
  if (!anchor.xpathStart || !anchor.xpathEnd) return null
  const startNode = nodeFromXpath(anchor.xpathStart)
  const endNode = nodeFromXpath(anchor.xpathEnd)
  if (!startNode || !endNode) return null
  if (startNode.nodeType !== Node.ELEMENT_NODE || endNode.nodeType !== Node.ELEMENT_NODE) return null

  const firstText = (element: Element): Text | null => {
    const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT)
    const hit = walker.nextNode()
    return hit && hit.nodeType === Node.TEXT_NODE ? (hit as Text) : null
  }

  const startText = firstText(startNode as Element)
  const endText = firstText(endNode as Element)
  if (!startText || !endText || startText.nodeType !== Node.TEXT_NODE || endText.nodeType !== Node.TEXT_NODE) return null
  if (!Number.isInteger(anchor.startOffset) || !Number.isInteger(anchor.endOffset)) return null

  const range = document.createRange()
  range.setStart(startText, Math.min((startText.nodeValue || '').length, Math.max(0, anchor.startOffset || 0)))
  range.setEnd(endText, Math.min((endText.nodeValue || '').length, Math.max(0, anchor.endOffset || 0)))
  return range
}

function findQuoteOffsets(anchor: ClipAnchor): { start: number; end: number } | null {
  const exact = String(anchor.exact || '')
  if (!exact) return null
  const nodes = collectTextNodeIndex()
  const raw = fullTextFromNodes(nodes)
  let idx = raw.indexOf(exact)
  if (idx < 0) return null

  const prefix = String(anchor.prefix || '')
  const suffix = String(anchor.suffix || '')
  if (!prefix && !suffix) return { start: idx, end: idx + exact.length }

  while (idx >= 0) {
    const left = raw.slice(Math.max(0, idx - prefix.length), idx).trim()
    const right = raw.slice(idx + exact.length, idx + exact.length + suffix.length).trim()
    const prefixOk = !prefix || left === prefix
    const suffixOk = !suffix || right === suffix
    if (prefixOk && suffixOk) return { start: idx, end: idx + exact.length }
    idx = raw.indexOf(exact, idx + exact.length)
  }
  return null
}

export function createAnchorFromSelection(selection: Selection): { anchor: ClipAnchor; quote: string } | null {
  if (!selection.rangeCount) return null
  const range = selection.getRangeAt(0)
  if (range.collapsed) return null
  if (!isRangeSelectable(range)) return null

  const quote = selection.toString()
  if (!quote.trim()) return null
  const nodes = collectTextNodeIndex()
  const offsets = rangeOffsets(range, nodes)
  const anchor: ClipAnchor = { exact: quote }

  if (range.startContainer.nodeType === Node.TEXT_NODE && range.endContainer.nodeType === Node.TEXT_NODE) {
    const startText = range.startContainer as Text
    const endText = range.endContainer as Text
    anchor.xpathStart = xpathFromNode(startText)
    anchor.xpathEnd = xpathFromNode(endText)
    anchor.startOffset = range.startOffset
    anchor.endOffset = range.endOffset
  }

  if (offsets) {
    anchor.startTextOffset = offsets.start
    anchor.endTextOffset = offsets.end
    const fullText = fullTextFromNodes(nodes)
    const context = buildTextContext(fullText, offsets.start, offsets.end)
    if (context.prefix) anchor.prefix = context.prefix
    if (context.suffix) anchor.suffix = context.suffix
  }

  return { anchor, quote }
}

export function resolveAnchorToRange(anchor: ClipAnchor): Range | null {
  if (Number.isInteger(anchor.startTextOffset) && Number.isInteger(anchor.endTextOffset)) {
    const fromOffsets = rangeFromAbsoluteOffsets(anchor.startTextOffset || 0, anchor.endTextOffset || 0)
    if (fromOffsets && !fromOffsets.collapsed) return fromOffsets
  }

  const quoteOffsets = findQuoteOffsets(anchor)
  if (quoteOffsets) {
    const fromQuote = rangeFromAbsoluteOffsets(quoteOffsets.start, quoteOffsets.end)
    if (fromQuote && !fromQuote.collapsed) return fromQuote
  }

  const fromXpath = rangeFromXpathOffsets(anchor)
  if (fromXpath && !fromXpath.collapsed) return fromXpath
  return null
}
