'use client'

import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import type { RefObject, ReactNode } from 'react'
import { AnimatePresence, motion } from 'framer-motion'
import { cn } from '@/lib/utils'
import { Tooltip } from '../basic/tooltip'

// ============================================================================
// 对话锚点导航条（Conversation Anchor Nav）
// ----------------------------------------------------------------------------
// 固定在长对话流容器右侧居中，为每条用户消息（或任何可定位节点）渲染一枚短横杠
// 或「打孔」圆点。hover 时横杠加长 / 圆点放大并展示摘要 tooltip，点击触发 onSelect。
// 当前可视区域对应的锚点自动高亮，数量不足 minItems 或 hidden=true 时自动隐藏。
// ============================================================================

/** 单个锚点项 */
export interface ConversationAnchorItem {
  /** 稳定 id，用于定位 DOM 节点与 React key */
  id: string
  /** 在 Tooltip 中展示的摘要文案（纯文本）。为空时回落到 labels.scrollTo。 */
  summary?: string
}

/** i18n 文案 */
export interface ConversationAnchorLabels {
  /** nav 的 aria-label */
  ariaLabel?: string
  /** Tooltip 兜底文案（无摘要时） */
  scrollTo?: string
}

/** 样式变体 */
export type ConversationAnchorVariant = 'bar' | 'punch'

export interface ConversationAnchorNavProps {
  /** 滚动容器 ref，用于检测当前可视锚点；受控场景可传 { current: null } */
  scrollContainerRef: RefObject<HTMLElement | null>
  /** 锚点列表，顺序即视觉顺序 */
  items: ConversationAnchorItem[]
  /** 点击锚点时回调，参数为在 items 中的索引与对应 item */
  onSelect: (index: number, item: ConversationAnchorItem) => void
  /**
   * 当前高亮索引（受控）。
   * 省略时组件内部会订阅 scrollContainerRef 的滚动并通过 IntersectionObserver 自动推导。
   */
  activeIndex?: number
  /**
   * 锚点视觉样式：
   * - 'bar' （默认）：短横杠，hover 时加长
   * - 'punch'：圆形"打孔"点，适合羊皮纸 / 拟物风主题
   */
  variant?: ConversationAnchorVariant
  /** 少于该数量时隐藏整个 nav，默认 3 */
  minItems?: number
  /** 外部强制隐藏（例如 compact 布局），默认 false */
  hidden?: boolean
  /** 自定义 Tooltip / aria 文案 */
  labels?: ConversationAnchorLabels
  /** 额外 className（会与默认定位 className 合并） */
  className?: string
  /**
   * 自定义锚点元素解析。默认通过
   * `scrollContainerRef.current.querySelector('[data-spark-anchor="<id>"]')`
   * 查找；需要业务在消息容器上添加该属性。
   */
  getAnchorElement?: (
    item: ConversationAnchorItem,
    index: number,
    container: HTMLElement,
  ) => HTMLElement | null
  /** 渲染 slot：自定义每个锚点的视觉表达，返回 null 时回落到内置样式 */
  renderAnchor?: (ctx: {
    item: ConversationAnchorItem
    index: number
    isActive: boolean
    variant: ConversationAnchorVariant
  }) => ReactNode
}

const DEFAULT_LABELS: Required<ConversationAnchorLabels> = {
  ariaLabel: 'Conversation anchors',
  scrollTo: 'Scroll to message',
}

export const ConversationAnchorNav = memo(function ConversationAnchorNav({
  scrollContainerRef,
  items,
  onSelect,
  activeIndex: controlledActiveIndex,
  variant = 'bar',
  minItems = 3,
  hidden = false,
  labels,
  className,
  getAnchorElement,
  renderAnchor,
}: ConversationAnchorNavProps) {
  const mergedLabels = { ...DEFAULT_LABELS, ...labels }

  const detectedActiveIndex = useVisibleAnchorIndex({
    scrollContainerRef,
    items,
    getAnchorElement,
    enabled: controlledActiveIndex === undefined && !hidden,
  })

  const activeIndex =
    controlledActiveIndex !== undefined ? controlledActiveIndex : detectedActiveIndex

  const handleClick = useCallback(
    (index: number) => {
      const item = items[index]
      if (item) onSelect(index, item)
    },
    [items, onSelect],
  )

  const shouldShow = !hidden && items.length >= minItems

  return (
    <AnimatePresence mode="wait">
      {shouldShow && (
        <motion.nav
          key="spark-conversation-anchor-nav"
          initial={{ opacity: 0, x: 8 }}
          animate={{
            opacity: 1,
            x: 0,
            transition: { type: 'spring', stiffness: 500, damping: 30, mass: 0.6 },
          }}
          exit={{
            opacity: 0,
            x: 8,
            transition: { duration: 0.15, ease: [0.32, 0.72, 0, 1] },
          }}
          className={cn(
            'absolute right-3 top-1/2 -translate-y-1/2 z-[40]',
            'flex flex-col items-end gap-1.5 max-h-[80%] overflow-y-auto',
            'scrollbar-hide',
            className,
          )}
          aria-label={mergedLabels.ariaLabel}
        >
          {items.map((item, index) => (
            <AnchorDot
              key={item.id}
              item={item}
              index={index}
              isActive={index === activeIndex}
              variant={variant}
              tooltipFallback={mergedLabels.scrollTo}
              onClick={handleClick}
              renderAnchor={renderAnchor}
            />
          ))}
        </motion.nav>
      )}
    </AnimatePresence>
  )
})

ConversationAnchorNav.displayName = 'ConversationAnchorNav'

// ----------------------------------------------------------------------------
// 单个锚点
// ----------------------------------------------------------------------------

interface AnchorDotProps {
  item: ConversationAnchorItem
  index: number
  isActive: boolean
  variant: ConversationAnchorVariant
  tooltipFallback: string
  onClick: (index: number) => void
  renderAnchor?: ConversationAnchorNavProps['renderAnchor']
}

const AnchorDot = memo(
  function AnchorDot({
    item,
    index,
    isActive,
    variant,
    tooltipFallback,
    onClick,
    renderAnchor,
  }: AnchorDotProps) {
    const handleClick = useCallback(() => onClick(index), [onClick, index])

    const custom = renderAnchor
      ? renderAnchor({ item, index, isActive, variant })
      : null

    return (
      <Tooltip
        content={item.summary || tooltipFallback}
        side="left"
        contentClassName="max-w-xs"
      >
        <button
          type="button"
          onClick={handleClick}
          className={cn(
            // 使用命名 group（group/spark-anchor）避免被外层 `.group` 祖先的 hover 污染
            'group/spark-anchor flex cursor-pointer border-0 bg-transparent outline-none',
            'focus-visible:ring-2 focus-visible:ring-[var(--color-link)] focus-visible:rounded-full',
            variant === 'punch'
              ? 'h-7 w-7 shrink-0 items-center justify-center p-0'
              : 'items-center justify-end p-1',
          )}
          aria-label={`${tooltipFallback} ${index + 1}`}
          aria-current={isActive ? 'true' : undefined}
          data-spark-anchor-nav-item={item.id}
        >
          {custom ?? (
            <span
              className={cn(
                'block shrink-0 origin-center',
                variant === 'punch'
                  ? cn(
                      'rounded-full transition-transform duration-300 ease-out',
                      isActive
                        ? 'h-2 w-2 bg-[var(--color-link)] group-hover/spark-anchor:scale-125'
                        : cn(
                            'h-1.5 w-1.5 border border-solid border-border-tertiary bg-transparent',
                            'group-hover/spark-anchor:scale-150 group-hover/spark-anchor:border-[var(--color-link)]',
                          ),
                    )
                  : cn(
                      'rounded-full transition-all duration-300 ease-out h-0.5',
                      isActive
                        ? 'w-4 bg-text-quaternary'
                        : 'w-2.5 bg-border-tertiary',
                      'group-hover/spark-anchor:w-6 group-hover/spark-anchor:bg-text-quaternary',
                    ),
              )}
            />
          )}
        </button>
      </Tooltip>
    )
  },
  (prev, next) =>
    prev.isActive === next.isActive &&
    prev.variant === next.variant &&
    prev.index === next.index &&
    prev.item.id === next.item.id &&
    prev.item.summary === next.item.summary &&
    prev.tooltipFallback === next.tooltipFallback &&
    prev.onClick === next.onClick &&
    prev.renderAnchor === next.renderAnchor,
)

// ----------------------------------------------------------------------------
// Hook: 基于 IntersectionObserver 推导当前可视锚点索引
// ----------------------------------------------------------------------------

export interface UseVisibleAnchorIndexOptions {
  scrollContainerRef: RefObject<HTMLElement | null>
  items: ConversationAnchorItem[]
  /** 与 ConversationAnchorNav 同名 prop，默认按 `[data-spark-anchor="<id>"]` 查找 */
  getAnchorElement?: ConversationAnchorNavProps['getAnchorElement']
  /** 为 false 时不订阅滚动，返回 0；用于受控场景 */
  enabled?: boolean
}

/**
 * 订阅 scroll 容器内每个锚点元素的可见性，返回"最靠近顶部且仍可见"的锚点索引。
 * - 容器必须可滚动（通常 overflow-y: auto），且锚点元素是其后代
 * - 每个锚点需要能通过 `getAnchorElement` 解析到 HTMLElement
 */
export function useVisibleAnchorIndex({
  scrollContainerRef,
  items,
  getAnchorElement,
  enabled = true,
}: UseVisibleAnchorIndexOptions): number {
  const [activeIndex, setActiveIndex] = useState(0)

  // 稳定化 ids 作为 effect 依赖，避免每次 items 引用变化都重建 observer
  const idsKey = useMemo(() => items.map((i) => i.id).join('|'), [items])

  const getAnchorRef = useRef(getAnchorElement)
  useEffect(() => {
    getAnchorRef.current = getAnchorElement
  }, [getAnchorElement])

  useEffect(() => {
    if (!enabled) return
    const container = scrollContainerRef.current
    if (!container || items.length === 0) return
    if (typeof IntersectionObserver === 'undefined') return

    const resolve = (item: ConversationAnchorItem, index: number) => {
      const fn = getAnchorRef.current
      if (fn) return fn(item, index, container)
      return container.querySelector<HTMLElement>(
        `[data-spark-anchor="${cssEscape(item.id)}"]`,
      )
    }

    const anchors: Array<{ index: number; el: HTMLElement }> = []
    items.forEach((item, index) => {
      const el = resolve(item, index)
      if (el) anchors.push({ index, el })
    })
    if (anchors.length === 0) return

    const ratioByEl = new WeakMap<HTMLElement, number>()

    const recompute = () => {
      let bestIndex = anchors[0]?.index ?? 0
      let bestRatio = -1
      for (const { index, el } of anchors) {
        const r = ratioByEl.get(el) ?? 0
        if (r > bestRatio) {
          bestRatio = r
          bestIndex = index
        }
      }
      setActiveIndex((prev) => (prev === bestIndex ? prev : bestIndex))
    }

    const observer = new IntersectionObserver(
      (entries) => {
        for (const e of entries) {
          ratioByEl.set(e.target as HTMLElement, e.intersectionRatio)
        }
        recompute()
      },
      { root: container, threshold: [0, 0.25, 0.5, 0.75, 1] },
    )

    anchors.forEach((a) => observer.observe(a.el))

    return () => observer.disconnect()
    // idsKey 稳定覆盖 items 内容变化；scrollContainerRef 本身稳定
  }, [enabled, scrollContainerRef, items, idsKey])

  return activeIndex
}

function cssEscape(value: string): string {
  if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') {
    return CSS.escape(value)
  }
  return value.replace(/["\\]/g, '\\$&')
}
