<script setup lang="ts">
import dayjs from 'dayjs'
import { useDebounce } from '@vueuse/core'
import type { SearchHit } from '~/types'

const props = defineProps<{
  activity: SearchHit
  collection?: 'activities' | 'conferences'
  query?: string
}>()

const emits = defineEmits(['close'])

const { locale } = useI18n()

// ---- Match URL & metadata ----------------------------------------------

function getTimestamp(i: SearchHit): number | null {
  const d = i.date ?? i.isodate
  if (d == null) return null
  const ts = typeof d === 'string' ? new Date(d).getTime() : (d as number) * 1000
  return Number.isFinite(ts) ? ts : null
}

const matchUrl = computed(() => {
  const i = props.activity
  if (!i?.type) return null
  const ts = getTimestamp(i)
  if (ts == null) return null
  const d = dayjs(ts)
  const month = (d.month() + 1).toString().padStart(2, '0')
  const slug = i.slug && i.slug !== 'undefined' ? i.slug : i.id ?? i._id
  return `https://www.carpa.com/${locale.value}/${i.type}/${d.year()}/${month}/${slug}`
})

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

function safeDate(i: SearchHit) {
  const ts = getTimestamp(i)
  return ts == null ? '' : formatDate(ts / 1000)
}

// ---- Find-in-document --------------------------------------------------

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

const localQuery = ref(props.query || '')
const debouncedLocalQuery = useDebounce(localQuery, 200)

const currentIdx = ref(0)
const totalMatches = ref(0)

// Reset local query and counters when the user opens a different activity.
watch(
  () => props.activity?._id,
  () => {
    localQuery.value = props.query || ''
    currentIdx.value = 0
    totalMatches.value = 0
  }
)

// Keep local query in sync if the parent search changes while the same
// activity stays open.
watch(() => props.query, (q) => {
  if (q !== undefined && q !== localQuery.value) localQuery.value = q || ''
})

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

function normalize(s: string): string {
  return s.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase()
}

function normalizeWithMap(text: string): { normalized: string, map: number[] } {
  let normalized = ''
  const map: number[] = []
  for (let i = 0; i < text.length; i++) {
    const norm = text[i].normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase()
    for (let j = 0; j < norm.length; j++) {
      normalized += norm[j]
      map.push(i)
    }
  }
  return { normalized, map }
}

function findMatchesInText(text: string, terms: string[]): Array<{ start: number, end: number }> {
  if (!text || !terms.length) return []
  const sources = terms.map(t => termToRegexSource(normalize(t))).filter(s => s.length > 0)
  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
}

/** In the detail view, the entire query is treated as ONE exact phrase — no
 *  word splitting. Surrounding double quotes are tolerated and stripped. */
function parseTerms(query: string): string[] {
  const trimmed = (query || '').trim().replace(/^"+|"+$/g, '').trim()
  return trimmed.length > 0 ? [trimmed] : []
}

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+')
}

/** Walk text nodes, build the FLAT text content of `root`, find phrase
 *  positions in that flat text, then mark whichever portion of each match
 *  falls inside each text node. Cross-node phrases (split by HTML tags) get
 *  highlighted as several adjacent <mark>s.
 *  Returns the number of distinct matches (not <mark> elements) so the
 *  navigation counter ("3 / N") reflects unique phrase occurrences. */
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

  // Tag the first <mark> of each logical match so the navigator can step
  // between distinct occurrences (not between fragments of one phrase).
  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'
      // First fragment of a logical match gets a marker class so we can find
      // distinct occurrences for the up/down navigation buttons.
      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
}

function getMarks(): HTMLElement[] {
  const el = bodyContainer.value
  if (!el) return []
  return Array.from(el.querySelectorAll('mark.search-match')) as HTMLElement[]
}

/** Distinct match starts — for cross-node phrases, only the FIRST <mark>
 *  fragment of each phrase carries the .match-start class. */
function getMatchStarts(): HTMLElement[] {
  const el = bodyContainer.value
  if (!el) return []
  return Array.from(el.querySelectorAll('mark.search-match.match-start')) as HTMLElement[]
}

function applyCurrent(scroll = true) {
  const allMarks = getMarks()
  allMarks.forEach(m => m.classList.remove('is-current'))
  const starts = getMatchStarts()
  if (!starts.length) return
  const idx = Math.min(Math.max(currentIdx.value, 0), starts.length - 1)
  const target = starts[idx]
  if (!target) return
  target.classList.remove('is-current')
  // eslint-disable-next-line @typescript-eslint/no-unused-expressions
  void target.offsetWidth
  target.classList.add('is-current')
  if (scroll) target.scrollIntoView({ block: 'center', behavior: 'smooth' })
}

function nextMatch() {
  if (!totalMatches.value) return
  currentIdx.value = (currentIdx.value + 1) % totalMatches.value
  applyCurrent()
}

function prevMatch() {
  if (!totalMatches.value) return
  currentIdx.value = (currentIdx.value - 1 + totalMatches.value) % totalMatches.value
  applyCurrent()
}

function clearLocalQuery() {
  localQuery.value = ''
}

async function renderBody() {
  await nextTick()
  const el = bodyContainer.value
  if (!el) return

  const raw = props.activity?.body
  el.innerHTML = raw ? ((fixLink(raw) as string) || '') : ''

  if (scrollContainer.value) scrollContainer.value.scrollTop = 0

  applyHighlights(false)
}

function applyHighlights(preserveScrollIfSameQuery = false) {
  const el = bodyContainer.value
  if (!el) return

  // Wipe any previous <mark> wrappers by replacing them with text nodes.
  const previous = el.querySelectorAll('mark.search-match')
  previous.forEach((m) => {
    const text = document.createTextNode(m.textContent || '')
    m.parentNode?.replaceChild(text, m)
  })
  // Merge adjacent text nodes so the next regex pass works cleanly.
  el.normalize()

  const terms = parseTerms(debouncedLocalQuery.value || '')

  const count = terms.length ? highlightTextNodes(el, terms) : 0
  totalMatches.value = count
  currentIdx.value = 0

  if (count > 0) {
    nextTick(() => applyCurrent(!preserveScrollIfSameQuery))
  }
}

// Re-render entire body when the activity changes.
watch(() => props.activity?._id, renderBody, { immediate: true })

// Re-apply highlights when the local search query changes.
watch(debouncedLocalQuery, () => {
  // Don't auto-scroll on every keystroke; jump to first match only on activity change.
  applyHighlights(true)
})

onMounted(renderBody)

// Keyboard helpers when the input is focused.
function onInputKey(e: KeyboardEvent) {
  if (e.key === 'Enter') {
    e.preventDefault()
    if (e.shiftKey) prevMatch()
    else nextMatch()
  } else if (e.key === 'Escape') {
    clearLocalQuery()
  }
}
</script>

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

      <template #right>
        <UButton
          v-if="matchUrl"
          :to="matchUrl"
          target="_blank"
          icon="i-lucide-external-link"
          label="Ver en sitio"
          color="primary"
          variant="solid"
          size="sm"
        />
      </template>
    </UDashboardNavbar>

    <div class="flex flex-col sm:flex-row justify-between gap-2 p-4 sm:px-6 border-b border-default">
      <div class="min-w-0">
        <p class="font-semibold text-sm text-highlighted">
          {{ safeDate(activity) }}
        </p>
        <p class="text-muted text-sm truncate">
          {{ formatLocation(activity) }}
        </p>
      </div>

      <ul v-if="fileLinks.length" class="flex flex-wrap gap-3 text-xs text-muted">
        <li
          v-for="(f, idx) in fileLinks"
          :key="idx"
          class="flex items-center"
        >
          <ULink
            :to="f.to"
            :target="f.target"
            class="flex items-center gap-1 hover:text-primary"
          >
            <UIcon :name="f.icon!" class="size-4" />
            <span>{{ f.label }}</span>
          </ULink>
        </li>
      </ul>
    </div>

    <div ref="scrollContainer" class="flex-1 overflow-y-auto relative">
      <!-- Find-in-document toolbar: sticky at the top of the scroll area so
           the user can always reach it while reading. -->
      <div
        v-if="activity?.body"
        class="sticky top-0 z-10 bg-default/95 backdrop-blur-sm border-b border-default px-4 sm:px-6 py-2 flex items-center gap-2"
      >
        <div class="relative flex-1 min-w-0 max-w-md">
          <UInput
            v-model="localQuery"
            icon="i-lucide-search"
            placeholder="Buscar frase exacta en este documento..."
            size="sm"
            class="w-full"
            :ui="{ trailing: 'pe-1' }"
            @keydown="onInputKey"
          >
            <template #trailing>
              <UButton
                v-if="localQuery"
                icon="i-lucide-x"
                color="neutral"
                variant="link"
                size="xs"
                aria-label="Limpiar"
                @click="clearLocalQuery"
              />
            </template>
          </UInput>
        </div>

        <span
          class="text-xs tabular-nums whitespace-nowrap min-w-[3.5rem] text-center"
          :class="totalMatches ? 'text-toned font-medium' : 'text-dimmed'"
        >
          <template v-if="localQuery">
            {{ totalMatches ? `${currentIdx + 1} / ${totalMatches}` : '0 / 0' }}
          </template>
        </span>

        <UTooltip text="Anterior (Shift+Enter)">
          <UButton
            icon="i-lucide-chevron-up"
            color="neutral"
            variant="ghost"
            size="sm"
            :disabled="!totalMatches"
            aria-label="Coincidencia anterior"
            @click="prevMatch"
          />
        </UTooltip>
        <UTooltip text="Siguiente (Enter)">
          <UButton
            icon="i-lucide-chevron-down"
            color="neutral"
            variant="ghost"
            size="sm"
            :disabled="!totalMatches"
            aria-label="Coincidencia siguiente"
            @click="nextMatch"
          />
        </UTooltip>
      </div>

      <article
        v-if="activity?.body"
        ref="bodyContainer"
        class="prose prose-sm max-w-none dark:prose-invert p-4 sm:p-6"
      />
      <p v-else class="p-4 sm:p-6 text-sm text-muted">
        No hay contenido disponible para esta coincidencia.
        <span v-if="matchUrl">
          Puedes
          <ULink :to="matchUrl" target="_blank" class="text-primary">verla en el sitio</ULink>.
        </span>
      </p>
    </div>
  </UDashboardPanel>
</template>
