import type { DimSdkStore } from '#cli/core/store'
import type { ComputedRef, Ref } from 'vue'
import { t } from '#cli/i18n'
import { computed, shallowRef, triggerRef } from 'vue'
import { extractSelectedTextFromProvider } from '../../utils/selection'

function debugHoverLog(message: string): void {
  try {
    const env = (globalThis as any).process?.env
    const hoverDebug = String(env?.DIMCODE_DEBUG_HOVER ?? '')
    if (hoverDebug !== '1' && hoverDebug !== '2') return
    const path = String(
      env?.DIMCODE_DEBUG_HOVER_LOG ??
        env?.DIMCODE_DEBUG_LOG ??
        './dimcode-debug.log',
    )
    // eslint-disable-next-line ts/no-require-imports
    const fs = require('node:fs')
    const timestamp = new Date().toISOString().split('T')[1].slice(0, -1)
    fs.appendFileSync(path, `[${timestamp}] [HOVER] ${message}\n`)
  } catch {
    // ignore
  }
}

export function useChatSelection(
  opts: Readonly<{
    nodeLike: boolean
    store: DimSdkStore
    mainY: number
    mainH: ComputedRef<number>
    scrollTop: Ref<number>
    lineCount?: ComputedRef<number>
    getLine?: (row: number) =>
      | Readonly<{
          text?: string
          messageId?: string
          segments?: Array<{ x: number; w: number; action?: { type: string } }>
        }>
      | undefined
    lines?: ComputedRef<
      readonly Readonly<{
        text?: string
        messageId?: string
        segments?: Array<{ x: number; w: number; action?: { type: string } }>
      }>[]
    >
    focusMode: Ref<'input' | 'messages'>
    hoveredUserMessageId?: Ref<string | null>
    hoveredLink?: Ref<boolean>
    hoveredLinkRange?: Ref<{
      row: number
      startCol: number
      endCol: number
    } | null>
    invalidate?: () => void
    messageActionsTargetId: Ref<string | null>
    showMessageActions: Ref<boolean>
    copyTextToClipboard: (text: string) => Promise<boolean>
    showCopyToast: (text: string) => void
  }>,
): Readonly<{
  messageSelectStart: Ref<{ row: number; col: number } | null>
  messageSelectEnd: Ref<{ row: number; col: number } | null>
  isSelecting: Ref<boolean>
  hasPendingMessageSelection: ComputedRef<boolean>
  hasMessageSelection: ComputedRef<boolean>
  focusBrowserTextarea: () => void
  getSelectedMessageText: () => string
  getNativeSelectionText: () => string
  clearMessageSelection: () => void
  onMessageClick: (e: any) => void
  onMessagePointerDown: (e: any) => void
  onMessagePointerMove: (e: any) => void
  onMessagePointerUp: (e: any) => void
  onMessageContextMenu: (e: any) => void
}> {
  // Selection updates can happen at high frequency (pointer move).
  // Use shallow refs + in-place mutation + triggerRef to reduce allocations.
  const messageSelectStart = shallowRef<{ row: number; col: number } | null>(
    null,
  )
  const messageSelectEnd = shallowRef<{ row: number; col: number } | null>(null)
  const isSelecting = shallowRef(false)
  const pendingSelectStart = shallowRef<{ row: number; col: number } | null>(
    null,
  )
  const lastPointerLocal = { x: 0, y: 0 }
  const lineCountRef =
    opts.lineCount ?? computed(() => opts.lines?.value.length ?? 0)
  const getLine = opts.getLine ?? ((row: number) => opts.lines?.value[row])

  const scheduleFrame: (cb: (ts: number) => void) => any = ((globalThis as any)
    .requestAnimationFrame ??
    ((cb: (ts: number) => void) => setTimeout(() => cb(Date.now()), 16))) as any
  const cancelFrame: (id: any) => void = ((globalThis as any)
    .cancelAnimationFrame ?? clearTimeout) as any

  let autoScrollFrameId: any = null
  let autoScrollLastTs = 0
  let autoScrollAccum = 0
  let autoScrollRunning = false

  const hasMessageSelection = computed(() => {
    const start = messageSelectStart.value
    const end = messageSelectEnd.value
    return Boolean(
      start && end && (start.row !== end.row || start.col !== end.col),
    )
  })
  const hasPendingMessageSelection = computed(() => pendingSelectStart.value !== null)
  const selectionStartThresholdCols = 2

  function setSelectionEnd(row: number, col: number): void {
    const cur = messageSelectEnd.value
    if (!cur) {
      messageSelectEnd.value = { row, col }
      return
    }
    if (cur.row === row && cur.col === col) return
    cur.row = row
    cur.col = col
    triggerRef(messageSelectEnd)
  }

  function getSelectedMessageText(): string {
    return extractSelectedTextFromProvider(
      getLine,
      lineCountRef.value,
      messageSelectStart.value as any,
      messageSelectEnd.value as any,
    )
  }

  function getNativeSelectionText(): string {
    const sel = (globalThis as any).getSelection?.()
    return typeof sel?.toString === 'function' ? String(sel.toString()) : ''
  }

  function focusBrowserTextarea(): void {
    if (opts.nodeLike) return
    try {
      const doc: any = (globalThis as any).document
      const el = doc?.querySelector?.(
        '[data-vt-host] textarea',
      ) as HTMLTextAreaElement | null
      el?.focus?.()
    } catch {
      // ignore
    }
  }

  function clearMessageSelection(): void {
    messageSelectStart.value = null
    messageSelectEnd.value = null
    isSelecting.value = false
    pendingSelectStart.value = null
    if (opts.hoveredUserMessageId) {
      opts.hoveredUserMessageId.value = null
      opts.invalidate?.()
    }
    if (opts.hoveredLink) {
      opts.hoveredLink.value = false
      opts.invalidate?.()
    }
    if (opts.hoveredLinkRange) {
      opts.hoveredLinkRange.value = null
      opts.invalidate?.()
    }
  }

  function onMessagePointerDown(e: any): void {
    opts.focusMode.value = 'messages'
    focusBrowserTextarea()
    // Browser: keep native text selection by default; use Alt+drag for internal selection.
    // CLI: internal selection is the primary way to select/copy text.
    if (!opts.nodeLike && !e?.altKey) return
    if (opts.hoveredLink) opts.hoveredLink.value = false
    if (opts.hoveredLinkRange) opts.hoveredLinkRange.value = null
    const localY = (e.cellY ?? 0) - opts.mainY
    const localX = (e.cellX ?? 0) - 2 // account for x offset
    lastPointerLocal.y = localY
    lastPointerLocal.x = localX
    const row = opts.scrollTop.value + localY
    const col = Math.max(0, localX)
    const line = getLine(row)
    const hasLineAction = Boolean(
      line?.action && (line.action as any).type !== 'none',
    )
    const hasSegmentAction = Boolean(
      line?.segments?.some(
        (segment) =>
          segment?.action &&
          col >= Number(segment.x ?? -1) &&
          col < Number(segment.x ?? -1) + Number(segment.w ?? 0),
      ),
    )
    if (hasLineAction || hasSegmentAction) {
      pendingSelectStart.value = null
      messageSelectStart.value = null
      messageSelectEnd.value = null
      isSelecting.value = false
      return
    }
    pendingSelectStart.value = { row, col }
    messageSelectStart.value = null
    messageSelectEnd.value = null
    isSelecting.value = false
  }

  function onMessagePointerMove(e: any): void {
    if (
      String((globalThis as any).process?.env?.DIMCODE_DEBUG_HOVER ?? '') ===
      '2'
    ) {
      debugHoverLog(`pointermove cell=(${e.cellX ?? '?'},${e.cellY ?? '?'})`)
    }
    if (!isSelecting.value) {
      if (pendingSelectStart.value) {
        const localY = (e.cellY ?? 0) - opts.mainY
        const localX = Math.max(0, (e.cellX ?? 0) - 2)
        const row = opts.scrollTop.value + localY
        lastPointerLocal.y = localY
        lastPointerLocal.x = localX
        const movedRows = row - pendingSelectStart.value.row
        const movedCols = localX - pendingSelectStart.value.col
        if (
          movedRows !== 0 ||
          Math.abs(movedCols) >= selectionStartThresholdCols
        ) {
          messageSelectStart.value = { ...pendingSelectStart.value }
          messageSelectEnd.value = { row, col: localX }
          isSelecting.value = true
          pendingSelectStart.value = null
          maybeStartOrStopAutoScroll()
          return
        }
      }
      const localY = (e.cellY ?? 0) - opts.mainY
      const localX = (e.cellX ?? 0) - 2
      const row = opts.scrollTop.value + localY
      const line = getLine(row)
      const messageId = String((line as any)?.messageId ?? '').trim()
      if (messageId && opts.hoveredUserMessageId) {
        const msg = opts.store.state.messages.find((m) => m.id === messageId)
        if (msg?.role === 'user') {
          if (opts.hoveredUserMessageId.value !== messageId) {
            opts.hoveredUserMessageId.value = messageId
            debugHoverLog(
              `enter messageId=${messageId} row=${row} cell=(${e.cellX ?? '?'},${e.cellY ?? '?'})`,
            )
            opts.invalidate?.()
          }
        } else if (opts.hoveredUserMessageId.value) {
          opts.hoveredUserMessageId.value = null
          debugHoverLog(
            `leave (non-user) row=${row} cell=(${e.cellX ?? '?'},${e.cellY ?? '?'})`,
          )
          opts.invalidate?.()
        }
      } else if (opts.hoveredUserMessageId?.value) {
        opts.hoveredUserMessageId.value = null
        debugHoverLog(
          `leave (no message) row=${row} cell=(${e.cellX ?? '?'},${e.cellY ?? '?'})`,
        )
        opts.invalidate?.()
      }

      if (opts.hoveredLink) {
        if (!opts.nodeLike) {
          let dirty = false
          if (opts.hoveredLink.value) {
            opts.hoveredLink.value = false
            dirty = true
          }
          if (opts.hoveredLinkRange?.value) {
            opts.hoveredLinkRange.value = null
            dirty = true
          }
          if (dirty) opts.invalidate?.()
          return
        }
        const segments = (line as any)?.segments as
          | Array<{ x: number; w: number; action?: { type: string } }>
          | undefined
        let onLink = false
        let nextRange: {
          row: number
          startCol: number
          endCol: number
        } | null = null
        if (segments && Number.isFinite(localX)) {
          for (let i = 0; i < segments.length; i++) {
            const seg = segments[i]
            if (!seg?.action) continue
            const type = String(seg.action.type)
            if (type !== 'openFile' && type !== 'openUrl') continue
            const start = Number(seg.x) || 0
            const end = start + (Number(seg.w) || 0)
            if (localX >= start && localX < end) {
              onLink = true
              nextRange = { row, startCol: start, endCol: end }
              break
            }
          }
        }
        const prevRange = opts.hoveredLinkRange?.value ?? null
        const rangeChanged = Boolean(
          (prevRange && !nextRange) ||
          (!prevRange && nextRange) ||
          (prevRange &&
            nextRange &&
            (prevRange.row !== nextRange.row ||
              prevRange.startCol !== nextRange.startCol ||
              prevRange.endCol !== nextRange.endCol)),
        )
        const shouldInvalidate =
          opts.hoveredLink.value !== onLink || rangeChanged
        if (opts.hoveredLink.value !== onLink) opts.hoveredLink.value = onLink
        if (opts.hoveredLinkRange && rangeChanged)
          opts.hoveredLinkRange.value = nextRange
        if (shouldInvalidate) opts.invalidate?.()
      }
      return
    }
    const localY = (e.cellY ?? 0) - opts.mainY
    const localX = (e.cellX ?? 0) - 2
    lastPointerLocal.y = localY
    lastPointerLocal.x = localX
    const row = opts.scrollTop.value + localY
    setSelectionEnd(row, Math.max(0, localX))

    // Auto-scroll when selection goes beyond screen boundaries.
    // Use a time-based loop so scrolling keeps up smoothly even when the pointer
    // moves outside the component and pointermove events stop firing.
    maybeStartOrStopAutoScroll()
  }

  function onMessagePointerUp(_e: any): void {
    const clickStart = pendingSelectStart.value
    pendingSelectStart.value = null
    if (!isSelecting.value) {
      stopAutoScroll()
      if (opts.hoveredUserMessageId) {
        opts.hoveredUserMessageId.value = null
        opts.invalidate?.()
      }
      if (!opts.nodeLike || !clickStart) return
      const line = getLine(clickStart.row)
      if (!line?.messageId) return
      const msg = opts.store.state.messages.find((m) => m.id === line.messageId)
      if (msg?.role !== 'user') return
      opts.messageActionsTargetId.value = line.messageId
      opts.showMessageActions.value = true
      return
    }
    isSelecting.value = false
    stopAutoScroll()
    if (opts.hoveredUserMessageId) {
      opts.hoveredUserMessageId.value = null
      opts.invalidate?.()
    }

    const start = messageSelectStart.value
    const end = messageSelectEnd.value
    const isDrag =
      start && end && (start.row !== end.row || start.col !== end.col)

    // In the browser we use native selection + click handler; avoid double-triggering actions.
    if (!opts.nodeLike) return

    // Terminal: copy selected text automatically on mouse-up to avoid keybinding conflicts (e.g. Ctrl+C).
    if (isDrag) {
      const text = getSelectedMessageText()
      if (text) {
        void opts
          .copyTextToClipboard(text)
          .then((ok) =>
            opts.showCopyToast(
              ok
                ? t('chatPage.toast.copySuccess')
                : t('chatPage.toast.copyFailed'),
            ),
          )
          .catch(() => opts.showCopyToast(t('chatPage.toast.copyFailed')))
      }
      return
    }

  }

  function onMessageClick(e: any): void {
    if (opts.nodeLike) return
    focusBrowserTextarea()
    opts.focusMode.value = 'messages'
    if (getNativeSelectionText().trim()) return
    if (!e?.shiftKey) return
    if (e.target && e.currentTarget && e.target !== e.currentTarget) return
    const localY = (e.cellY ?? 0) - opts.mainY
    const row = opts.scrollTop.value + localY
    const line = getLine(row)
    if (!line?.messageId) return
    const msg = opts.store.state.messages.find((m) => m.id === line.messageId)
    if (msg?.role !== 'user') return
    opts.messageActionsTargetId.value = line.messageId
    opts.showMessageActions.value = true
  }

  function onMessageContextMenu(e: any): void {
    if (getNativeSelectionText().trim()) return
    e.preventDefault?.()
    const localY = (e.cellY ?? 0) - opts.mainY
    const row = opts.scrollTop.value + localY
    const line = getLine(row)
    if (!line?.messageId) return
    const msg = opts.store.state.messages.find((m) => m.id === line.messageId)
    if (msg?.role !== 'user') return
    opts.messageActionsTargetId.value = line.messageId
    opts.showMessageActions.value = true
  }

  function stopAutoScroll(): void {
    if (autoScrollFrameId != null) {
      cancelFrame(autoScrollFrameId)
      autoScrollFrameId = null
    }
    autoScrollRunning = false
    autoScrollLastTs = 0
    autoScrollAccum = 0
  }

  function maybeStartOrStopAutoScroll(): void {
    if (!isSelecting.value) {
      stopAutoScroll()
      return
    }

    const mainH = Math.max(1, opts.mainH.value)
    const maxScroll = Math.max(0, lineCountRef.value - mainH)
    const localY = lastPointerLocal.y

    const overscrollDown = Math.max(0, localY - (mainH - 1))
    const overscrollUp = Math.max(0, -localY)
    const wantsDown = overscrollDown > 0 && opts.scrollTop.value < maxScroll
    const wantsUp = overscrollUp > 0 && opts.scrollTop.value > 0

    if (!wantsDown && !wantsUp) {
      stopAutoScroll()
      return
    }
    if (autoScrollFrameId == null) {
      if (!autoScrollRunning) {
        autoScrollLastTs = 0
        autoScrollAccum = 0
        autoScrollRunning = true
      }
      autoScrollFrameId = scheduleFrame(autoScrollStep)
    }
  }

  function autoScrollSpeedLinesPerSecond(overscrollCells: number): number {
    // 1 cell beyond edge -> slow; farther away -> faster, capped.
    const d = Math.min(10, Math.max(0, overscrollCells))
    return Math.min(80, 8 + d * 12 + d * d * 2)
  }

  function autoScrollStep(ts: number): void {
    autoScrollFrameId = null
    if (!isSelecting.value) return

    const mainH = Math.max(1, opts.mainH.value)
    const maxScroll = Math.max(0, lineCountRef.value - mainH)

    const localY = lastPointerLocal.y
    const overscrollDown = Math.max(0, localY - (mainH - 1))
    const overscrollUp = Math.max(0, -localY)

    let dir = 0
    let overscroll = 0
    if (overscrollDown > 0 && opts.scrollTop.value < maxScroll) {
      dir = 1
      overscroll = overscrollDown
    } else if (overscrollUp > 0 && opts.scrollTop.value > 0) {
      dir = -1
      overscroll = overscrollUp
    } else {
      stopAutoScroll()
      return
    }

    const dtMs = autoScrollLastTs ? Math.max(0, ts - autoScrollLastTs) : 16
    autoScrollLastTs = ts

    const speed = autoScrollSpeedLinesPerSecond(overscroll)
    autoScrollAccum += dir * speed * (dtMs / 1000)

    const delta =
      autoScrollAccum > 0
        ? Math.floor(autoScrollAccum)
        : Math.ceil(autoScrollAccum)
    if (delta !== 0) {
      autoScrollAccum -= delta
      const next = Math.max(
        0,
        Math.min(maxScroll, opts.scrollTop.value + delta),
      )
      if (next !== opts.scrollTop.value) {
        opts.scrollTop.value = next
        const row = opts.scrollTop.value + lastPointerLocal.y
        setSelectionEnd(row, Math.max(0, lastPointerLocal.x))
      }
    }

    // Keep scrolling while still overscrolling and not at bounds.
    maybeStartOrStopAutoScroll()
  }

  return {
    messageSelectStart,
    messageSelectEnd,
    isSelecting,
    hasPendingMessageSelection,
    hasMessageSelection,
    focusBrowserTextarea,
    getSelectedMessageText,
    getNativeSelectionText,
    clearMessageSelection,
    onMessageClick,
    onMessagePointerDown,
    onMessagePointerMove,
    onMessagePointerUp,
    onMessageContextMenu,
  }
}
