<script setup lang="ts">
import dayjs from 'dayjs'
import { useDebounce } from '@vueuse/core'
import { useFavoritesStore } from '~/stores/favorites'
import { useHistoryStore } from '~/stores/history'
import { storeToRefs } from 'pinia'
import { useSettingsStore } from '~/stores/settings'
import type { SearchHit } from '~/types'
import { pageHeader, select } from '#build/ui'

interface TypesenseHighlight {
  field?: string
  snippet?: string
  value?: string
}

interface ParagraphDoc {
  id?: string
  document_id: string
  text: string
  raw?: string
  number: number
  locale: string
  type: string
}

interface TypesenseParagraphHit {
  document: ParagraphDoc
  highlights?: TypesenseHighlight[]
  highlight?: Record<string, { snippet?: string, value?: string }>
}

interface DocumentDoc {
  id?: string
  code: string
  locale: string
  type: string
  title: string
  timestamp: number
  date: string
  place?: string
  city?: string
  state?: string
  country?: string
  thumbnail?: string
  files?: {
    youtube?: string
    video?: string
    audio?: string
    booklet?: string
    simple?: string
  }
  slug?: string
  body?: string
  draft?: boolean
  [key: string]: unknown
}

const props = defineProps<{
  document: DocumentDoc | null
  documentLoading: boolean
  paragraphs: TypesenseParagraphHit[]
  paragraphsLoading: boolean
  collection: string
  query?: string
  selectedHit?: TypesenseParagraphHit | null
  selectedMatchingHits?: TypesenseParagraphHit[] | null
  accentColor?: 'green' | 'blue'
}>()

const emits = defineEmits(['close'])

const nuxtApp = useNuxtApp()

const { locale } = useI18n()
const favorites = useFavoritesStore()

const iconColor = computed(() => props.accentColor === 'blue' ? 'text-carpablue' : 'text-carpagreen')
const history = useHistoryStore()
const { showParagraphNumbers } = storeToRefs(useSettingsStore())
const toast = useToast()

function toSearchHit(doc: DocumentDoc): SearchHit {
  return {
    _id: doc.id || doc.code || '',
    id: doc.id,
    title: doc.title || '',
    date: doc.date,
    place: doc.place,
    city: doc.city,
    state: doc.state,
    country: doc.country,
    body: doc.body,
    thumbnail: doc.thumbnail,
    type: doc.type,
    files: doc.files,
    slug: doc.slug,
    ...doc
  }
}

watch(
  () => [props.collection, props.document?.id] as const,
  ([collection, id]) => {
    if (!collection || !id || !props.document) return
    history.visit(collection, toSearchHit(props.document))
  },
  { immediate: true }
)

const isFav = computed(() => {
  if (!props.collection || !props.document?.id) return false
  return favorites.isFavorite(props.collection, props.document.id)
})

function onToggleFavorite() {
  if (!props.collection || !props.document) return
  const wasFav = isFav.value
  favorites.toggle(props.collection, toSearchHit(props.document))
  toast.add({
    title: wasFav ? 'Eliminado de tu lista' : 'Guardado en tu lista',
    description: props.document.title,
    icon: wasFav ? 'i-lucide-bookmark-x' : 'i-lucide-bookmark-check',
    color: wasFav ? 'neutral' : 'primary',
    duration: 1800
  })
}

const matchUrl = computed(() => {
  const doc = props.document
  if (!doc?.type) return null
  const ts = doc.timestamp
    ? doc.timestamp * 1000
    : doc.date ? new Date(doc.date).getTime() : null
  if (!ts || !Number.isFinite(ts)) return null
  const d = dayjs(ts)
  const month = (d.month() + 1).toString().padStart(2, '0')
  const slug = doc.slug && doc.slug !== 'undefined' ? doc.slug : doc.id ?? doc.code
  return `https://www.carpa.com/${locale.value}/${doc.type}/${d.year()}/${month}/${slug}`
})

const fileLinks = computed(() => formatFiles(props.document?.files || {}))

function safeDate(): string {
  const doc = props.document
  if (!doc) return ''
  const ts = doc.timestamp
    ? doc.timestamp
    : doc.date ? Math.floor(new Date(doc.date).getTime() / 1000) : null
  if (!ts) return doc.date || ''
  return formatDate(ts)
}

function safeLocation(): string {
  const doc = props.document
  if (!doc) return ''
  return formatLocation({
    id: doc.id || '',
    date: doc.timestamp ?? 0,
    slug: doc.slug ?? '',
    type: doc.type ?? '',
    place: doc.place ?? '',
    city: doc.city ?? '',
    state: doc.state ?? '',
    country: doc.country ?? '',
    thumbnail: ''
  })
}

// ---- Utilerías de highlight --------------------------------------------------

function highlightedFor(hit: TypesenseParagraphHit, field: string): string | null {
  const fromArr = hit.highlights?.find(h => h.field === field)
  if (fromArr?.snippet) return fromArr.snippet
  if (fromArr?.value) return fromArr.value
  const fromObj = hit.highlight?.[field]
  if (fromObj?.snippet) return fromObj.snippet
  if (fromObj?.value) return fromObj.value
  return null
}

function decodeEntities(s: string): string {
  if (!s || s.indexOf('&') === -1) return s
  if (typeof document === 'undefined') return s
  const tmp = document.createElement('div')
  tmp.innerHTML = s
  return tmp.textContent || s
}

// ---- Snippet parsing & precise mark application ----------------------------

interface SnippetSegment {
  text: string
  isMarked: boolean
}

// Descompone el HTML del snippet en segmentos marcados y no marcados.
function parseSnippetHtml(html: string): SnippetSegment[] {
  const segments: SnippetSegment[] = []
  let lastIdx = 0
  const markRe = /<mark[^>]*>([\s\S]*?)<\/mark>/gi
  let m: RegExpExecArray | null
  while ((m = markRe.exec(html)) !== null) {
    if (m.index > lastIdx) {
      const raw = html.slice(lastIdx, m.index).replace(/<[^>]*>/g, '')
      if (raw) segments.push({ text: decodeEntities(raw), isMarked: false })
    }
    segments.push({ text: decodeEntities(m[1]), isMarked: true })
    lastIdx = m.index + m[0].length
  }
  if (lastIdx < html.length) {
    const raw = html.slice(lastIdx).replace(/<[^>]*>/g, '')
    if (raw) segments.push({ text: decodeEntities(raw), isMarked: false })
  }
  return segments
}

// Recolecta nodos de texto de un párrafo, excluyendo los que ya están dentro de marks.
function collectParaTextNodes(paraEl: HTMLElement): { flat: string, nmap: { node: Text, start: number, end: number }[] } {
  const walker = document.createTreeWalker(paraEl, NodeFilter.SHOW_TEXT, {
    acceptNode(node: Node) {
      let p = node.parentNode
      while (p && p !== paraEl) {
        if (p instanceof HTMLElement && p.matches('mark.search-match')) return NodeFilter.FILTER_REJECT
        p = p.parentNode
      }
      return NodeFilter.FILTER_ACCEPT
    }
  })
  let flat = ''
  const nmap: { node: Text, start: number, end: number }[] = []
  let n: Node | null
  while ((n = walker.nextNode())) {
    const node = n as Text
    const t = node.nodeValue || ''
    nmap.push({ node, start: flat.length, end: flat.length + t.length })
    flat += t
  }
  return { flat, nmap }
}

// Ubica el snippet completo (con contexto) en el párrafo y aplica un <mark> por cada
// segmento marcado del snippet. Retorna los marks creados en orden de documento.
//
// Algoritmo: aplica los marks de DERECHA A IZQUIERDA para que las posiciones
// computadas desde el flat text original sigan siendo válidas en cada iteración,
// ya que las mutaciones DOM a la derecha no desplazan los nodos a la izquierda.
function findAndApplySnippetMarks(paraEl: HTMLElement, snippetHtml: string): HTMLElement[] {
  const segments = parseSnippetHtml(snippetHtml)
  if (!segments.some(s => s.isMarked)) return []

  const fullPlain = segments.map(s => s.text).join('')

  // Eliminar SOLO marcadores de truncación de Typesense (… o ...), NO puntos de oración.
  // Ejemplo correcto: "…texto aquí…" → "texto aquí"
  // Ejemplo incorrecto anterior: "el Programa Divino." → "el Programa Divino" (perdía el punto)
  const stripped = fullPlain
    .replace(/^(?:…|\.\.\.)\s*/, '')
    .replace(/\s*(?:…|\.\.\.)$/, '')
    .trim()
  if (!stripped) return []

  // Ajustar leadingLen para el cálculo de offsets de marks
  const leadingLen = fullPlain.length - fullPlain.replace(/^(?:…|\.\.\.)\s*/, '').length

  // Flat text inicial del párrafo para calcular posiciones
  const { flat: initFlat } = collectParaTextNodes(paraEl)
  if (!initFlat) return []

  // Mapeo posición-normalizada → posición-original
  const { normalized: normFlat, map: normToOrig } = normalizeWithMap(initFlat)
  const normStripped = normalize(stripped)
  const normIdx = normFlat.indexOf(normStripped)
  if (normIdx === -1) return []

  // Calcular posición original de cada segmento marcado dentro del snippet
  const markPositions: { origStart: number, origEnd: number }[] = []
  let plainCursor = leadingLen

  for (const seg of segments) {
    const normOffset = normalize(fullPlain.slice(leadingLen, plainCursor)).length
    if (seg.isMarked && normOffset <= normStripped.length) {
      const normStart = normIdx + normOffset
      const normEnd = normStart + normalize(seg.text).length
      const origStart = normStart < normToOrig.length ? normToOrig[normStart] : initFlat.length
      const origEnd = normEnd < normToOrig.length ? normToOrig[normEnd] : initFlat.length
      if (origStart < origEnd) markPositions.push({ origStart, origEnd })
    }
    plainCursor += seg.text.length
  }

  if (!markPositions.length) return []

  // Aplicar marks de derecha a izquierda
  markPositions.sort((a, b) => b.origStart - a.origStart)
  const createdMarks: HTMLElement[] = []

  for (const { origStart, origEnd } of markPositions) {
    // Re-recolectar nodos: los ya marcados quedan excluidos del flat text.
    // Las posiciones < origStart del último mark aplicado no se ven afectadas.
    const { nmap } = collectParaTextNodes(paraEl)

    for (let i = nmap.length - 1; i >= 0; i--) {
      const info = nmap[i]
      const segStart = Math.max(origStart, info.start) - info.start
      const segEnd = Math.min(origEnd, info.end) - info.start
      if (segStart >= segEnd) continue

      const nodeText = info.node.nodeValue || ''
      const frag = document.createDocumentFragment()
      if (segStart > 0) frag.appendChild(document.createTextNode(nodeText.slice(0, segStart)))
      const mark = document.createElement('mark')
      mark.className = 'search-match match-start'
      mark.textContent = nodeText.slice(segStart, segEnd)
      frag.appendChild(mark)
      createdMarks.push(mark)
      if (segEnd < nodeText.length) frag.appendChild(document.createTextNode(nodeText.slice(segEnd)))
      info.node.parentNode?.replaceChild(frag, info.node)
    }
    paraEl.normalize()
  }

  // Los marks se crearon de derecha a izquierda; revertir para orden de documento
  createdMarks.reverse()
  return createdMarks
}

function normalize(s: string): string {
  //   = non-breaking space (&nbsp;); treat as regular space so DOM text
  // nodes (which keep &nbsp; as  ) match Typesense snippets (regular spaces).
  return s.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase().replace(/ /g, ' ')
}

// ---- Refs de estado ---------------------------------------------------------

const paragraphsContainer = ref<HTMLElement | null>(null)
const scrollContainer = ref<HTMLElement | null>(null)

// 'typesense' = Estado 1 (server-driven), 'local' = Estado 2 (client-driven)
type SearchMode = 'typesense' | 'local'
const searchMode = ref<SearchMode>('typesense')
const matchElements = ref<HTMLElement[]>([])
const currentMatchIdx = ref(0)

// ---- Limpieza de marks del DOM ----------------------------------------------

function clearMatchMarks() {
  const container = paragraphsContainer.value
  if (!container) return
  container.querySelectorAll('mark.search-match').forEach((m) => {
    const parent = m.parentNode
    if (parent) {
      parent.replaceChild(document.createTextNode(m.textContent || ''), m)
      parent.normalize()
    }
  })
  matchElements.value = []
  currentMatchIdx.value = 0
}

// ---- Envolver coincidencias de texto en un párrafo --------------------------
//
// Recorre los nodos de texto dentro de paraEl, encuentra la primera aparición
// de matchText (ignorando diacríticos y mayúsculas) y la envuelve en un <mark>.
// Repite hasta que no haya más apariciones. Devuelve todos los marks creados.

function wrapTextMatchesInParagraph(paraEl: HTMLElement, matchText: string): HTMLElement[] {
  const normMatch = normalize(matchText)
  if (!normMatch) return []

  const marks: HTMLElement[] = []

  while (true) {
    const walker = document.createTreeWalker(
      paraEl,
      NodeFilter.SHOW_TEXT,
      {
        acceptNode(node: Node) {
          let p = node.parentNode
          while (p && p !== paraEl) {
            if (p instanceof HTMLElement && p.matches('mark.search-match')) {
              return NodeFilter.FILTER_REJECT
            }
            p = p.parentNode
          }
          return NodeFilter.FILTER_ACCEPT
        }
      }
    )

    const textNodes: Text[] = []
    let n: Node | null
    while ((n = walker.nextNode())) textNodes.push(n as Text)

    if (!textNodes.length) break

    let flatText = ''
    const nodeMap: { node: Text, start: number, end: number }[] = []
    for (const node of textNodes) {
      const t = node.nodeValue || ''
      nodeMap.push({ node, start: flatText.length, end: flatText.length + t.length })
      flatText += t
    }

    const normFlat = normalize(flatText)
    const startIdx = normFlat.indexOf(normMatch)
    if (startIdx === -1) break

    const endIdx = startIdx + normMatch.length
    let applied = false

    for (let i = nodeMap.length - 1; i >= 0; i--) {
      const info = nodeMap[i]
      const segStart = Math.max(startIdx, info.start) - info.start
      const segEnd = Math.min(endIdx, info.end) - info.start
      if (segStart >= segEnd) continue

      const nodeText = info.node.nodeValue || ''
      const frag = document.createDocumentFragment()
      let cursor = 0

      if (segStart > cursor) frag.appendChild(document.createTextNode(nodeText.slice(cursor, segStart)))

      const mark = document.createElement('mark')
      mark.className = 'search-match match-start'
      mark.textContent = nodeText.slice(segStart, segEnd)
      frag.appendChild(mark)
      marks.push(mark)
      cursor = segEnd

      if (cursor < nodeText.length) frag.appendChild(document.createTextNode(nodeText.slice(cursor)))

      info.node.parentNode?.replaceChild(frag, info.node)
      applied = true
    }

    if (!applied) break

    paraEl.normalize()
  }

  return marks
}

// ---- Estado 1: aplicar highlights de Typesense y scroll inicial -------------

async function applyTypesenseHighlights() {
  await nextTick()
  clearMatchMarks()

  const container = paragraphsContainer.value
  if (!container) return

  searchMode.value = 'typesense'

  const hasMatchingHits = !!(props.selectedMatchingHits?.length)

  // Paso 1: marks del snippet del selectedHit (solo cuando hay hits de Typesense).
  let snippetMarks: HTMLElement[] = []

  if (hasMatchingHits && props.selectedHit) {
    const snippet = highlightedFor(props.selectedHit, 'text')
    const paraNumber = props.selectedHit.document.number
    if (snippet && paraNumber !== undefined) {
      const paraEl = container.querySelector(`[data-paragraph-number="${paraNumber}"]`) as HTMLElement | null
      if (paraEl) snippetMarks = findAndApplySnippetMarks(paraEl, snippet)
    }
  }

  // Paso 2: marks de fondo con la frase EXACTA del query (siempre se aplican).
  // Pre-check de texto plano para saltarse párrafos sin la frase → más rápido.
  const rawPhrase = (props.query || '').replace(/^"+|"+$/g, '').trim()

  if (rawPhrase) {
    const normPhrase = normalize(rawPhrase)
    const allParaEls = Array.from(
      container.querySelectorAll('[data-paragraph-number]')
    ) as HTMLElement[]
    for (const paraEl of allParaEls) {
      if (!normalize(paraEl.textContent || '').includes(normPhrase)) continue
      wrapTextMatchesInParagraph(paraEl, rawPhrase)
    }
  }

  // Paso 3: recolectar todos los marks en orden DOM para navegación secuencial.
  const domMarks = Array.from(
    container.querySelectorAll('mark.search-match.match-start')
  ) as HTMLElement[]
  matchElements.value = domMarks
  currentMatchIdx.value = 0

  // Paso 4: scroll — sin hits (browse mode) volver al inicio; con hits, ir al párrafo correcto.
  if (!hasMatchingHits) {
    if (scrollContainer.value) scrollContainer.value.scrollTop = 0
    return
  }

  let targetMark: HTMLElement | null = snippetMarks[0] ?? null

  if (!targetMark && props.selectedHit) {
    const paraNumber = props.selectedHit.document.number
    if (paraNumber !== undefined) {
      const paraEl = container.querySelector(`[data-paragraph-number="${paraNumber}"]`) as HTMLElement | null
      if (paraEl) {
        const marksInPara = Array.from(
          paraEl.querySelectorAll('mark.search-match.match-start')
        ) as HTMLElement[]
        if (marksInPara.length) {
          targetMark = marksInPara[0]
        } else {
          await nextTick()
          domMarks.forEach(m => m.classList.remove('is-current'))
          paraEl.scrollIntoView({ block: 'start', behavior: 'smooth' })
          return
        }
      }
    }
  }

  if (!targetMark) targetMark = domMarks[0] ?? null

  if (targetMark) {
    const idx = domMarks.indexOf(targetMark)
    currentMatchIdx.value = idx !== -1 ? idx : 0
    await nextTick()
    domMarks.forEach(m => m.classList.remove('is-current'))
    targetMark.classList.add('is-current')
    targetMark.scrollIntoView({ block: 'center', behavior: 'smooth' })
  }
}

// ---- Estado 2: búsqueda local -----------------------------------------------

function applyLocalHighlights(query: string) {
  clearMatchMarks()
  searchMode.value = 'local'

  const el = paragraphsContainer.value
  if (!el || !query) return

  const terms = termsFor(query)
  if (!terms.length) return

  highlightTextNodes(el, terms)

  matchElements.value = Array.from(el.querySelectorAll('mark.search-match.match-start')) as HTMLElement[]
  currentMatchIdx.value = 0

  if (matchElements.value.length) {
    nextTick(() => {
      matchElements.value[0].classList.add('is-current')
    })
  }
}

// ---- Navegación entre coincidencias -----------------------------------------

function navigateToCurrent() {
  matchElements.value.forEach(m => m.classList.remove('is-current'))
  const target = matchElements.value[currentMatchIdx.value]
  if (!target) return
  target.classList.add('is-current')
  target.scrollIntoView({ block: 'center', behavior: 'smooth' })
}

function nextMatch() {
  if (!matchElements.value.length) return
  currentMatchIdx.value = (currentMatchIdx.value + 1) % matchElements.value.length
  navigateToCurrent()
}

function prevMatch() {
  if (!matchElements.value.length) return
  currentMatchIdx.value = (currentMatchIdx.value - 1 + matchElements.value.length) % matchElements.value.length
  navigateToCurrent()
}

function clearLocalQuery() {
  localQuery.value = ''
  // Al limpiar el input local, volver a Estado 1 si hay hits de Typesense
  if (props.selectedMatchingHits?.length) {
    applyTypesenseHighlights()
  } else {
    clearMatchMarks()
    searchMode.value = 'typesense'
  }
}

// ---- Input de búsqueda local ------------------------------------------------
// Arranca vacío siempre: el Estado 1 (Typesense) es el modo inicial.
// Solo al escribir aquí se activa el Estado 2.

const localQuery = ref('')
const debouncedLocalQuery = useDebounce(localQuery, 200)
const showInternalSearch = ref(true)
const internalSearchRef = ref<{ input?: HTMLInputElement } | null>(null)
const metaExpanded = ref(true)

function toggleInternalSearch() {
  showInternalSearch.value = !showInternalSearch.value
  if (showInternalSearch.value) {
    metaExpanded.value = true
    nextTick(() => internalSearchRef.value?.input?.focus())
  } else {
    clearLocalQuery()
  }
}

watch(() => props.document?.id, () => { metaExpanded.value = true; showInternalSearch.value = true })

watch(debouncedLocalQuery, (q) => {
  // Cualquier cambio en el input activa el Estado 2
  applyLocalHighlights(q)
})

function onInputKey(e: KeyboardEvent) {
  if (e.key === 'Enter') {
    e.preventDefault()
    if (e.shiftKey) prevMatch()
    else nextMatch()
  } else if (e.key === 'Escape') {
    clearLocalQuery()
  }
}

// ---- Inicialización y reactividad -------------------------------------------

// Al cambiar de documento, resetear todo el estado
watch(
  () => props.document?.id,
  () => {
    localQuery.value = ''
    matchElements.value = []
    currentMatchIdx.value = 0
    searchMode.value = 'typesense'
  }
)

// Cuando llegan los párrafos, aplicar highlights de Typesense (Estado 1)
watch(
  () => props.paragraphs,
  async (paragraphs) => {
    if (paragraphs.length) {
      // Resetear input local para garantizar Estado 1 al cargar nuevo documento
      localQuery.value = ''
      await applyTypesenseHighlights()
    }
  }
)

onMounted(async () => {
  if (props.paragraphs.length) {
    await applyTypesenseHighlights()
  }
  document.addEventListener('selectionchange', onSelectionChange)
})

onUnmounted(() => {
  document.removeEventListener('selectionchange', onSelectionChange)
  if (selectionTimer) clearTimeout(selectionTimer)
})

// ---- Utilerías de búsqueda local --------------------------------------------

function escapeRegex(s: string) {
  return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}

function normalizeWithMap(text: string): { normalized: string, map: number[] } {
  let normalized = ''
  const map: number[] = []
  for (let i = 0; i < text.length; i++) {
    // Treat non-breaking space ( ) as regular space — same as normalize()
    const char = text.charCodeAt(i) === 0xa0 ? ' ' : text[i]
    const norm = char.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase()
    for (let j = 0; j < norm.length; j++) {
      normalized += norm[j]
      map.push(i)
    }
  }
  return { normalized, map }
}

function termToRegexSource(term: string): string {
  const parts = term.split(/\s+/).filter(Boolean)
  if (parts.length === 0) return ''
  if (parts.length === 1) return escapeRegex(parts[0])
  return parts.map(escapeRegex).join('\\s+')
}

function findMatchesInText(text: string, terms: string[]): Array<{ start: number, end: number }> {
  if (!text || !terms.length) return []
  const sources = terms
    .map(t => ({ term: t, src: termToRegexSource(normalize(t)) }))
    .filter(x => x.src.length > 0)
    .sort((a, b) => b.term.length - a.term.length)
    .map(x => x.src)
  if (!sources.length) return []
  const { normalized, map } = normalizeWithMap(text)
  const re = new RegExp(`(${sources.join('|')})`, 'g')
  const out: Array<{ start: number, end: number }> = []
  let m: RegExpExecArray | null
  while ((m = re.exec(normalized)) !== null) {
    if (m[0].length === 0) { re.lastIndex++; continue }
    const start = map[m.index] ?? text.length
    const endNormIdx = m.index + m[0].length
    const end = endNormIdx < map.length ? map[endNormIdx] : text.length
    out.push({ start, end })
  }
  return out
}

function parseTerms(query: string): string[] {
  if (!query) return []
  const tokens: string[] = []
  const tokenRe = /"([^"]+)"|(\S+)/g
  for (const match of query.matchAll(tokenRe)) {
    const [, phrase, word] = match
    if (phrase) tokens.push(phrase.trim())
    else if (word && word.length > 1) tokens.push(word)
  }
  return tokens.filter(Boolean)
}

function termsFor(query: string): string[] {
  if (!query) return []
  if (query.includes('"')) return parseTerms(query)
  const stripped = query.replace(/^"+|"+$/g, '').trim()
  return stripped ? [stripped] : []
}

function highlightTextNodes(root: HTMLElement, terms: string[]): number {
  if (!terms.length) return 0
  const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null)
  const infos: { node: Text, start: number, end: number }[] = []
  let flat = ''
  let n: Node | null
  while ((n = walker.nextNode())) {
    const node = n as Text
    const text = node.nodeValue || ''
    infos.push({ node, start: flat.length, end: flat.length + text.length })
    flat += text
  }
  if (!infos.length) return 0

  const matches = findMatchesInText(flat, terms)
  if (!matches.length) return 0

  const matchStartByPos = new Set(matches.map(m => m.start))

  for (let i = infos.length - 1; i >= 0; i--) {
    const info = infos[i]
    const nodeText = info.node.nodeValue || ''
    const segments: { start: number, end: number, matchStart: number }[] = []
    for (const match of matches) {
      const segStart = Math.max(match.start, info.start) - info.start
      const segEnd = Math.min(match.end, info.end) - info.start
      if (segStart < segEnd) segments.push({ start: segStart, end: segEnd, matchStart: match.start })
    }
    if (!segments.length) continue
    segments.sort((a, b) => a.start - b.start)
    const frag = document.createDocumentFragment()
    let cursor = 0
    for (const seg of segments) {
      if (seg.start > cursor) frag.appendChild(document.createTextNode(nodeText.slice(cursor, seg.start)))
      const mark = document.createElement('mark')
      mark.className = 'search-match'
      if (matchStartByPos.has(seg.matchStart) && info.start + seg.start === seg.matchStart) {
        mark.classList.add('match-start')
      }
      mark.textContent = nodeText.slice(seg.start, seg.end)
      frag.appendChild(mark)
      cursor = seg.end
    }
    if (cursor < nodeText.length) frag.appendChild(document.createTextNode(nodeText.slice(cursor)))
    info.node.parentNode?.replaceChild(frag, info.node)
  }
  return matches.length
}
const isPopOverOpen = ref(false)
const selectionTooltip = ref<{ x: number; y: number } | null>(null)
let selectionTimer: ReturnType<typeof setTimeout> | null = null

function handleSelection(event: MouseEvent) {
  const selection = window.getSelection()
  if (selection && selection.toString().trim().length > 3) {
    if (selectionTimer) clearTimeout(selectionTimer)
    selectionTimer = setTimeout(() => {
      const x = Math.min(Math.max(event.clientX, 96), window.innerWidth - 96)
      selectionTooltip.value = { x, y: event.clientY }
      isPopOverOpen.value = true
    }, 320)
  } else {
    clearSelectionTooltip()
  }
}

function clearSelectionTooltip() {
  if (selectionTimer) { clearTimeout(selectionTimer); selectionTimer = null }
  selectionTooltip.value = null
  isPopOverOpen.value = false
}

function onSelectionChange() {
  const sel = window.getSelection()
  if (!sel || sel.toString().trim().length === 0) clearSelectionTooltip()
}

const links = computed(() => [])

const items = computed(() => {
  return [
    
  ]
})
</script>

<template>
  <UDashboardPanel id="estudios-ts-detail">
    <UDashboardNavbar :toggle="false">
      <template #leading>
        <UButton
          icon="i-lucide-arrow-left"
          color="neutral"
          variant="ghost"
          class="-ms-1.5"
          @click="emits('close')"
        />
      </template>

      <template #title>
        <span class="truncate font-semibold text-sm min-w-0 block" :title="document?.title">
          {{ documentLoading ? 'Cargando...' : (document?.title || '—') }}
        </span>
      </template>

      <template #right>
        <UTooltip :text="showInternalSearch ? 'Cerrar buscador' : 'Buscar en documento'">
          <UButton
            icon="i-lucide-search"
            :color="showInternalSearch || !!localQuery ? 'primary' : 'neutral'"
            :variant="showInternalSearch || !!localQuery ? 'soft' : 'ghost'"
            :disabled="!paragraphs.length"
            @click="toggleInternalSearch"
          />
        </UTooltip>
        <UTooltip text="Como funciona?">
          <UButton
            icon="ph-student"
            color="neutral"
            variant="ghost"
            :disabled="!document"
            @click="nuxtApp.callHook('tour',12)"
          />
        </UTooltip>
        <UTooltip :text="isFav ? 'Quitar de mi lista' : 'Guardar en mi lista'">
          <UButton
            :icon="isFav ? 'i-lucide-bookmark-check' : 'i-lucide-bookmark-plus'"
            :color="isFav ? 'primary' : 'neutral'"
            variant="ghost"
            :disabled="!document"
            :aria-label="isFav ? 'Quitar de mi lista' : 'Guardar en mi lista'"
            class="favorites_toggle"
            @click="onToggleFavorite"
          />
        </UTooltip>
      </template>
    </UDashboardNavbar>

    <!-- Strip siempre visible para expandir/colapsar metadatos -->
    <button
      class="w-full flex items-center justify-center gap-1.5 py-1 border-b border-default bg-elevated/60 hover:bg-elevated transition-colors text-xs text-muted"
      @click="metaExpanded = !metaExpanded"
    >
      <UIcon :name="metaExpanded ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'" class="size-3.5" />
      <span>{{ metaExpanded ? 'Ocultar detalles' : 'Ver detalles' }}</span>
    </button>

    <!-- Metadata -->
    <div v-show="metaExpanded" class="flex flex-col gap-3 p-4 sm:px-6 border-b border-default shadow-sm z-10">
      <div class="flex flex-wrap items-start justify-between gap-2">
        <div class="flex flex-wrap items-center gap-x-4 gap-y-1 min-w-0">
          <p v-if="document?.draft" class="text-sm text-highlighted flex items-center gap-1.5 shrink-0">
            <UIcon name="ph:file-dashed" class="size-4 text-carpared" />
            {{ $t('search.draft') }}
          </p>
          <p v-if="safeDate()" class="text-sm text-highlighted flex items-center gap-1.5 shrink-0">
            <UIcon name="ph:calendar" :class="['size-4', iconColor]" />
            {{ safeDate() }}
          </p>
          <p v-if="document?.activity" class="text-sm flex items-center gap-1.5 text-muted shrink-0">
            <UIcon name="ph:hash" :class="['size-4 shrink-0', iconColor]" />
            {{ $t('search.publication') }} {{ document.activity }}
          </p>
          <p v-if="safeLocation()" class="text-sm flex items-center gap-1.5 text-muted min-w-0">
            <UIcon name="ph:map-pin" :class="['size-4 shrink-0', iconColor]" />
            <span class="truncate max-w-55">{{ safeLocation() }}</span>
          </p>
        </div>
        <UButton
          v-if="matchUrl"
          :to="matchUrl"
          target="_blank"
          icon="i-lucide-external-link"
          label="Ver en sitio"
          color="primary"
          variant="soft"
          size="xs"
          class="shrink-0"
        />
      </div>

      <!-- file links hidden -->

      <!-- Buscador interno del documento -->
      <div v-if="paragraphs.length" v-show="showInternalSearch || !!localQuery" class="flex items-center gap-2">
        <UInput
          ref="internalSearchRef"
          v-model="localQuery"
          icon="i-lucide-search"
          :placeholder="props.query || 'Buscar en este documento...'"
          class="flex-1 min-w-0"
          :ui="{ trailing: 'pe-1' }"
          @keydown="onInputKey"
        >
          <template #trailing>
            <UButton
              v-if="localQuery"
              icon="i-lucide-x"
              variant="link"
              aria-label="Limpiar"
              @click="clearLocalQuery"
            />
          </template>
        </UInput>
        <template v-if="matchElements.length || searchMode === 'local'">
          <UBadge
            v-if="searchMode === 'typesense' && matchElements.length"
            label="Typesense"
            size="xs"
            variant="subtle"
            color="warning"
            class="shrink-0"
          />
          <span
            class="text-xs tabular-nums whitespace-nowrap shrink-0 px-2 py-1 rounded-md bg-elevated border border-default"
            :class="matchElements.length ? 'text-toned font-medium' : 'text-dimmed'"
          >
            {{ matchElements.length ? `${currentMatchIdx + 1} / ${matchElements.length}` : '0 / 0' }}
          </span>
          <div class="flex items-center gap-1 shrink-0">
            <UTooltip text="Anterior (Shift+Enter)">
              <UButton
                icon="i-lucide-chevron-up"
                :disabled="!matchElements.length"
                aria-label="Anterior"
                color="neutral"
                variant="ghost"
                size="sm"
                @click="prevMatch"
              />
            </UTooltip>
            <UTooltip text="Siguiente (Enter)">
              <UButton
                icon="i-lucide-chevron-down"
                :disabled="!matchElements.length"
                aria-label="Siguiente"
                color="neutral"
                variant="ghost"
                size="sm"
                @click="nextMatch"
              />
            </UTooltip>
          </div>
        </template>
      </div>
    </div>

    <div ref="scrollContainer" class="flex-1 overflow-y-auto relative bg-gray-100">
      <UPage class="max-w-6xl mx-auto py-0 my-0 " @mouseup="handleSelection">
        <UPageBody class="bg-white p-8 rounded-xl shadow-lg">
          <UPageHeader
            :title="document?.title"
            :description="formatSignature(document)"
            class="py-0 pb-2"
          >
            <template #headline>
              <div class="flex w-full justify-between" v-if="paragraphs.length>0">
                <UBadge variant="outline" color="neutral" :class="document?.type=='activities' ? 'bg-carpagreen/20' : 'bg-carpablue/20'">{{ document?.locale.toUpperCase() }}-{{ document?.code }}</UBadge>
                <div class="text-carpared font-semibold" v-if="document?.draft"><UIcon name="ph-file-dashed" class="mr-1" />{{ $t('ui.draft') }}</div>
              </div>
            </template>
          </UPageHeader>
          <div v-if="paragraphsLoading" class="flex items-center justify-center gap-2 py-16 text-sm text-muted">
            <UIcon name="i-lucide-loader-circle" class="size-5 animate-spin" />
            Cargando párrafos...
          </div>
          <div v-else-if="paragraphs.length" ref="paragraphsContainer">
            <div class="">
              <div v-for="(hit, idx) in paragraphs" :key="idx" :data-paragraph-number="hit.document.number">
                <div class="grid grid-cols-1fr items-start gap-2 mb-2" :class="(showParagraphNumbers && 'grid-cols-[20px_1fr]')">
                  <div v-if="showParagraphNumbers" class="w-full select-none cursor-pointer flex justify-end">
                    <UBadge
                      v-if="hit.document.number"
                      :label="`${hit.document.number}`"
                      size="md"
                      variant="link"
                      class="text-gray-300 font-bold"
                      :class="(hit.document.type=='activities'?'hover:text-carpagreen':'hover:text-carpablue')"
                      @click="isPopOverOpen = !isPopOverOpen"
                    />
                  </div>
                  <div class="">
                    <div
                      class="paragraph-html text-sm leading-relaxed text-gray-800 dark:text-gray-200"
                      v-html="hit.document.html || hit.document.text"
                    />
                  </div>
                </div>
              </div>
            </div>
          </div>

          <!-- Sin contenido -->
          <div v-else class="flex flex-col items-center justify-center gap-2 py-16 text-dimmed text-sm">
            <UIcon name="i-lucide-file-x" class="size-10" />
            <p>No hay contenido disponible para este documento.</p>
            <p v-if="matchUrl">
              Puedes
              <ULink :to="matchUrl" target="_blank" class="text-primary">verlo en el sitio</ULink>.
            </p>
          </div>
        </UPageBody>

      </UPage>
    </div>

    <!-- Tooltip de selección -->
    <Teleport to="body">
      <div
        v-if="selectionTooltip"
        class="fixed z-9999 pointer-events-none"
        :style="{ left: selectionTooltip.x + 'px', top: (selectionTooltip.y - 12) + 'px' }"
      >
        <Transition name="tooltip-pop">
          <div
            v-if="isPopOverOpen"
            class="p-2 selection-tooltip flex items-center bg-white rounded-xl shadow-2xl overflow-hidden select-none pointer-events-auto"
            @mousedown.prevent
          >
            <!-- <UButton
              variant="soft"
              class="group flex flex-col items-center gap-1 px-4 py-2.5 text-blue-300 hover:bg-white/10 active:bg-white/20 transition-colors"
              aria-label="Crear nota"
              @click="addNote()"
            >
              <UIcon name="ph-note-pencil" class="size-5" />
              <span class="group-hover:text-black text-[9px] font-semibold leading-none">Nota</span>
            </UButton>
            <div class="w-px self-stretch bg-white/10" /> -->
            <!-- <UButton
              variant="soft"
              class="group flex flex-col items-center gap-1 px-4 py-2.5 text-green-400 hover:bg-white/10 active:bg-white/20 transition-colors"
              aria-label="Resaltar"
              @click="highlightParagraph()"
            >
              <UIcon name="ph-highlighter" class="size-5" />
              <span class="group-hover:text-black text-[9px] font-semibold leading-none">Resaltar</span>
            </UButton> -->
            <div class="w-px self-stretch bg-white/10" />
            <UButton
              variant="soft"
              class="cursor-pointer group flex flex-col items-center gap-1 px-4 py-2.5 text-carpared hover:bg-white/10 active:bg-white/20 transition-colors"
              :aria-label="$t('ui.copy')"
              @click="copyToClipboard(props.document)"
            >
              <UIcon name="ph-clipboard-text" class="size-7" />
              <span class="group-hover:text-black text-xs font-semibold leading-none">{{ $t('ui.copy') }}</span>
            </UButton>
          </div>
        </Transition>
      </div>
    </Teleport>
  </UDashboardPanel>
</template>

<style scoped>
.paragraph-html :deep(p) {
  margin-bottom: 0.5rem;
}

.paragraph-html :deep(p:last-child) {
  margin-bottom: 0;
}

/* Tooltip de selección de texto */
.selection-tooltip {
  transform: translateX(-50%) translateY(-100%);
}

.tooltip-pop-enter-active {
  transition: transform 0.18s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.12s ease;
}
.tooltip-pop-leave-active {
  transition: transform 0.12s ease-in, opacity 0.1s ease;
}
.tooltip-pop-enter-from,
.tooltip-pop-leave-to {
  transform: translateX(-50%) translateY(calc(-100% + 8px));
  opacity: 0;
}
</style>
