import { computed, ref, watch } from 'vue'
import Fuse from 'fuse.js'
import { pinyin } from 'pinyin-pro'
import { storeToRefs } from 'pinia'

import { useLinksStore } from '@/stores/links'

const RESULT_LIMIT = 12
const PIANO_OPTIONS = {
  toneType: 'none',
  type: 'array',
}

const FUSE_OPTIONS = {
  shouldSort: true,
  includeScore: true,
  includeMatches: true,
  threshold: 0.35,
  ignoreLocation: true,
  distance: 80,
  minMatchCharLength: 2,
  useExtendedSearch: false,
  keys: [
    { name: 'title', weight: 0.6 },
    { name: 'description', weight: 0.3 },
    { name: 'tags', weight: 0.5 },
    { name: 'url', weight: 0.2 },
    { name: 'pinyinTitleFull', weight: 0.4 },
    { name: 'pinyinTitleInitialsCompact', weight: 0.45 },
    { name: 'pinyinTagsFull', weight: 0.2 },
    { name: 'pinyinTagsInitials', weight: 0.25 },
  ],
}

function compressInitials(initials) {
  if (!initials) return ''
  let compressed = ''
  for (const char of initials) {
    if (compressed.endsWith(char)) continue
    compressed += char
  }
  return compressed
}

function stringFromArray(values = []) {
  return Array.isArray(values) ? values.join(' ') : ''
}

function makePinyinVariants(text) {
  if (!text) {
    return {
      full: '',
      initials: '',
      initialsCompact: '',
    }
  }
  const array = pinyin(text, { ...PIANO_OPTIONS, pattern: 'pinyin' })
  const initialsArray = pinyin(text, { ...PIANO_OPTIONS, pattern: 'first' })
  const full = Array.isArray(array) ? array.join(' ') : ''
  const initials = Array.isArray(initialsArray) ? initialsArray.join('') : ''
  const initialsCompact = compressInitials(initials)
  return { full, initials, initialsCompact }
}

function createSearchRecord(link) {
  const titleVariants = makePinyinVariants(link.title)
  const tagsString = stringFromArray(link.tags)
  const tagVariants = makePinyinVariants(tagsString)
  return {
    id: link.id,
    title: link.title,
    description: link.description ?? '',
    tags: tagsString,
    url: link.url,
    visitCount: link.visitCount ?? 0,
    lastVisitTime: link.lastVisitTime ?? null,
    createdAt: link.createdAt,
    updatedAt: link.updatedAt,
    pinyinTitleFull: titleVariants.full,
    pinyinTitleInitials: titleVariants.initials,
    pinyinTitleInitialsCompact: titleVariants.initialsCompact,
    pinyinTagsFull: tagVariants.full,
    pinyinTagsInitials: tagVariants.initialsCompact,
    original: link,
  }
}

function mergeRanges(ranges) {
  if (!ranges || ranges.length === 0) return []
  const sorted = [...ranges]
    .map(([start, end]) => [Math.max(0, start), Math.max(start, end)])
    .sort((a, b) => a[0] - b[0])

  const merged = []
  for (const range of sorted) {
    if (!merged.length) {
      merged.push(range)
      continue
    }
    const last = merged[merged.length - 1]
    if (range[0] <= last[1] + 1) {
      last[1] = Math.max(last[1], range[1])
    } else {
      merged.push(range)
    }
  }
  return merged
}

function aggregateResults(fuse, tokens) {
  if (!fuse || tokens.length === 0) {
    return []
  }

  const resultsMap = new Map()

  tokens.forEach((token) => {
    const tokenLower = token.toLowerCase()
    const tokenMatches = fuse.search(tokenLower)
    const perTokenBest = new Map()

    tokenMatches.forEach((result) => {
      const { item } = result
      const entry = perTokenBest.get(item.id)
      if (!entry || (result.score ?? 0) < (entry.score ?? 0)) {
        perTokenBest.set(item.id, { ...result, token: tokenLower })
      }
    })

    perTokenBest.forEach((result, id) => {
      const existing = resultsMap.get(id) ?? {
        link: result.item.original,
        scoreSum: 0,
        bestScore: Number.POSITIVE_INFINITY,
        hitCount: 0,
        exactTitleMatch: false,
        partialTitleMatch: false,
        tagMatch: false,
        highlights: new Map(),
      }

      const score = result.score ?? 0
      existing.scoreSum += score
      existing.bestScore = Math.min(existing.bestScore, score)
      existing.hitCount += 1

      if (Array.isArray(result.matches)) {
        result.matches.forEach((match) => {
          const { key, indices, value } = match
          if (!key || !indices) return
          const fieldHighlights = existing.highlights.get(key) ?? []
          fieldHighlights.push(...indices)
          existing.highlights.set(key, fieldHighlights)
          if (key === 'title') {
            const [start, end] = indices[0]
            const matched = value.slice(start, end + 1)
            if (matched.toLowerCase() === tokenLower) {
              existing.exactTitleMatch = true
            } else {
              existing.partialTitleMatch = true
            }
          }
          if (key === 'tags') {
            existing.tagMatch = true
          }
        })
      }

      resultsMap.set(id, existing)
    })
  })

  const filtered = Array.from(resultsMap.values()).filter((entry) => entry.hitCount === tokens.length)

  filtered.forEach((entry) => {
    const visitBoost = entry.link.visitCount ?? 0
    const lastVisitBoost = entry.link.lastVisitTime ? entry.link.lastVisitTime.getTime() : 0

    let priority = 5
    if (entry.exactTitleMatch) priority = 1
    else if (entry.partialTitleMatch) priority = 2
    else if (entry.tagMatch) priority = 3
    else if (visitBoost > 0) priority = 4

    entry.ranking = {
      priority,
      score: entry.scoreSum / entry.hitCount,
      visitBoost,
      lastVisitBoost,
    }
  })

  const sorted = filtered
    .sort((a, b) => {
      if (a.ranking.priority !== b.ranking.priority) {
        return a.ranking.priority - b.ranking.priority
      }
      if (a.ranking.score !== b.ranking.score) {
        return a.ranking.score - b.ranking.score
      }
      if (a.ranking.visitBoost !== b.ranking.visitBoost) {
        return b.ranking.visitBoost - a.ranking.visitBoost
      }
      return b.ranking.lastVisitBoost - a.ranking.lastVisitBoost
    })
    .slice(0, RESULT_LIMIT)

  return sorted.map((entry) => {
    const highlights = {}
    entry.highlights.forEach((ranges, key) => {
      highlights[key] = mergeRanges(ranges)
    })
    return {
      link: entry.link,
      ranking: entry.ranking,
      highlights,
    }
  })
}

export function useSearchEngine() {
  const linksStore = useLinksStore()
  const { links, isReady } = storeToRefs(linksStore)

  const fuseRef = ref(null)
  const query = ref('')
  const results = ref([])
  const isSearching = ref(false)

  watch(
    links,
    (items) => {
      const dataset = items.map(createSearchRecord)
      fuseRef.value = new Fuse(dataset, FUSE_OPTIONS)
      if (query.value.trim()) {
        results.value = aggregateResults(fuseRef.value, query.value.trim().split(/\s+/))
      }
    },
    { immediate: true, deep: true }
  )

  function performSearch(input) {
    const trimmed = input.trim()
    if (!trimmed) {
      results.value = []
      return
    }
    const tokens = trimmed.split(/\s+/)
    results.value = aggregateResults(fuseRef.value, tokens)
  }

  let debounceTimer = null

  function search(value) {
    query.value = value
    window.clearTimeout(debounceTimer)
    isSearching.value = true
    debounceTimer = window.setTimeout(() => {
      performSearch(value)
      isSearching.value = false
    }, 320)
  }

  function clear() {
    query.value = ''
    results.value = []
  }

  const hasResults = computed(() => results.value.length > 0)

  return {
    query,
    results,
    hasResults,
    isSearching: computed(() => isSearching.value),
    search,
    clear,
    isIndexReady: computed(() => isReady.value && Boolean(fuseRef.value)),
  }
}

