<template>
  <div class="search-bar" @keydown="onContainerKey">
    <input
      ref="searchInputRef"
      v-model="searchQuery"
      type="search"
      placeholder="Date or Event..."
      aria-label="Search dates, holidays and events"
      @keydown="handleSearchKeydown"
    />
    <div v-if="shortcut" class="shortcut-hint">
      {{ shortcut }}
    </div>
    <ul
      v-if="searchQuery.trim() && searchResults.length"
      class="search-dropdown"
      role="listbox"
      :aria-activedescendant="activeResultId"
    >
      <li
        v-for="(r, i) in searchResults"
        :key="r.id"
        :id="'sr-' + r.id"
        :class="{ active: i === searchIndex }"
        role="option"
        @mousedown.prevent="selectResult(i)"
      >
        <span class="title">{{ r.title }}</span
        ><span class="date">{{ r.startDate }}</span>
      </li>
    </ul>
    <div v-else-if="searchQuery.trim() && !searchResults.length" class="search-empty">
      No matches
    </div>
  </div>
</template>

<script setup>
import { ref, watch, nextTick, computed, defineExpose, onUnmounted, onMounted } from 'vue'
import { useCalendarStore } from '@/stores/CalendarStore'
import {
  fromLocalString,
  DEFAULT_TZ,
  monthAbbr,
  getLocalizedMonthName,
  toLocalString,
  getMondayOfISOWeek,
  formatTodayString,
  makeTZDate,
  getISOWeek,
} from '@/utils/date'
import { addDays } from 'date-fns'
import { getDate as getNearestOccurrence } from '@/utils/events'
import { getHolidaysForYear } from '@/utils/holidays'

const emit = defineEmits(['activate', 'preview'])
const props = defineProps({ referenceDate: { type: String, default: null } })
const calendarStore = useCalendarStore()

const searchQuery = ref('')
const searchResults = ref([])
const searchIndex = ref(0)
const searchInputRef = ref(null)
let previewTimer = null

// Note: Android is also Linux. HarmonyOS 5 doesn't include "Linux".
const shortcut = /Android/.test(navigator.userAgent) ? ''
  : /Mac/.test(navigator.userAgent) ? '⌘F'
  : /Windows|Linux/.test(navigator.userAgent) ? 'Ctrl+F'
  : ''

// Accent-insensitive lowercasing
const norm = (s) =>
  s
    .normalize('NFD')
    .replace(/\p{Diacritic}/gu, '')
    .toLowerCase()
let lastQuery = ''
let frozenRefStr = null // reference date frozen at last query change
const YEAR_SCAN_OFFSETS = [-4, -3, -2, -1, 0, 1, 2, 3, 4]
function buildSearchResults(queryChanged = false) {
  const raw = searchQuery.value.trim()
  if (!raw) {
    searchResults.value = []
    searchIndex.value = 0
    lastQuery = raw
    return
  }
  const listAll = raw === '*'
  const search = norm(raw)
  const out = []
  let refStrLive = props.referenceDate || calendarStore.today || calendarStore.now
  if (refStrLive.includes('T')) refStrLive = refStrLive.slice(0, 10)
  if (queryChanged || !frozenRefStr) frozenRefStr = refStrLive
  const refStr = frozenRefStr
  const nowDate = fromLocalString(refStr, DEFAULT_TZ)
  for (const ev of calendarStore.events.values()) {
    const title = '⚜️ ' + (ev.title || '').trim()
    if (!(listAll || norm(title).includes(search))) continue
    let displayStart = ev.startDate
    if (ev.recur) {
      const nearest = getNearestOccurrence(ev, refStr, DEFAULT_TZ)
      if (nearest?.dateStr) displayStart = nearest.dateStr
    }
    out.push({ id: ev.id, title, startDate: displayStart })
  }
  if (calendarStore.config?.holidays?.enabled) {
    try {
      calendarStore._ensureHolidaysInitialized?.()
      const refYear = nowDate.getFullYear()
      const yearWindow = YEAR_SCAN_OFFSETS.map((o) => refYear + o)
      const bestByName = Object.create(null)
      for (const yr of yearWindow) {
        for (const h of getHolidaysForYear(yr) || []) {
          const name = (h.name || '').trim().split(/\s*\/\s*/)[0]
          if (!name) continue
          if (!listAll && !norm(name).includes(search)) continue
          let dateObj
          try {
            dateObj = new Date(h.date)
          } catch {
            dateObj = null
          }
          if (!dateObj || isNaN(dateObj)) continue
          const diff = Math.abs(dateObj - nowDate)
          const key = name.toLowerCase()
          const prev = bestByName[key]
          if (!prev || diff < prev.diff) bestByName[key] = { name, dateObj, diff }
        }
      }
      for (const key in bestByName) {
        const { name, dateObj } = bestByName[key]
        const dateStr = toLocalString(dateObj, DEFAULT_TZ)
        out.push({
          id: '__holiday__' + dateStr + ':' + key,
          title: `✨ ${name}`,
          startDate: dateStr,
          _holiday: true,
          _dupeKey: '__holiday__' + dateStr + ':' + key,
        })
      }
    } catch (e) {
      if (process.env.NODE_ENV !== 'production') console.debug('[Search] holiday search skipped', e)
    }
  }
  if (queryChanged) {
    out.sort((a, b) => (a.startDate < b.startDate ? -1 : a.startDate > b.startDate ? 1 : 0))
  } else if (searchResults.value.length) {
    const order = new Map(searchResults.value.map((r, i) => [r.id, i]))
    out.sort((a, b) => {
      const ai = order.has(a.id) ? order.get(a.id) : 1e9
      const bi = order.has(b.id) ? order.get(b.id) : 1e9
      if (ai !== bi) return ai - bi
      return a.startDate < b.startDate ? -1 : a.startDate > b.startDate ? 1 : 0
    })
  }
  const gotoDateStr = parseGoToDateCandidate(raw, refStr)
  if (gotoDateStr) {
    const dateObj = fromLocalString(gotoDateStr, DEFAULT_TZ)
    out.unshift({
      id: '__goto__' + gotoDateStr,
      title: '📅 ' + formatTodayString(dateObj),
      startDate: gotoDateStr,
      _goto: true,
    })
  }
  searchResults.value = out
  if (searchIndex.value >= out.length) searchIndex.value = 0
  lastQuery = raw
}

watch(searchQuery, (nv, ov) => {
  buildSearchResults(nv.trim() !== lastQuery)
})
watch(
  () => calendarStore.events,
  () => {
    if (searchQuery.value.trim()) buildSearchResults(false)
  },
  { deep: true },
)
watch(
  () => props.referenceDate,
  () => {
    if (searchQuery.value.trim()) buildSearchResults(false)
  },
)

function focusSearch(selectAll = true) {
  nextTick(() => {
    if (searchInputRef.value) {
      searchInputRef.value.focus()
      if (selectAll) searchInputRef.value.select()
    }
  })
}

function navigate(delta) {
  const n = searchResults.value.length
  if (!n) return
  searchIndex.value = (searchIndex.value + delta + n) % n
  // Ensure active item is visible
  const r = searchResults.value[searchIndex.value]
  if (r) {
    const el = document.getElementById('sr-' + r.id)
    if (el) el.scrollIntoView({ block: 'nearest' })
  }
  if (previewTimer) clearTimeout(previewTimer)
  if (r)
    previewTimer = setTimeout(() => {
      if (r === searchResults.value[searchIndex.value]) emit('preview', r)
    }, 200)
}
function selectResult(idx) {
  searchIndex.value = idx
  const r = searchResults.value[searchIndex.value]
  if (r) {
    if (previewTimer) {
      clearTimeout(previewTimer)
      previewTimer = null
    }
    emit('activate', r)
    // Clear query after activation (auto-close handled by parent visibility)
    searchQuery.value = ''
  }
}
function handleSearchKeydown(e) {
  if ((e.ctrlKey || e.metaKey) && !e.altKey && !e.shiftKey && e.key.toLowerCase() === 'f') {
    e.preventDefault()
    e.stopPropagation()
    focusSearch(true)
    return
  }
  if (e.key === 'ArrowDown') {
    e.preventDefault()
    navigate(1)
  } else if (e.key === 'ArrowUp') {
    e.preventDefault()
    navigate(-1)
  } else if (e.key === 'Enter') {
    e.preventDefault()
    selectResult(searchIndex.value)
  } else if (e.key === 'Escape') {
    if (searchQuery.value) {
      searchQuery.value = ''
      e.preventDefault()
    }
  }
}
function onContainerKey(e) {
  /* capture for list navigation if needed */
}

const activeResultId = computed(() => {
  const r = searchResults.value[searchIndex.value]
  return r ? 'sr-' + r.id : null
})

defineExpose({ focusSearch })
onUnmounted(() => {
  if (previewTimer) clearTimeout(previewTimer)
})
// global Ctrl/Cmd+F -> search
let globalFindHandler = null
onMounted(() => {
  globalFindHandler = (e) => {
    if ((e.ctrlKey || e.metaKey) && !e.altKey && !e.shiftKey && e.key.toLowerCase() === 'f') {
      e.preventDefault()
      e.stopPropagation()
      focusSearch(true)
    }
  }
  window.addEventListener('keydown', globalFindHandler, { capture: true })
})
onUnmounted(() => {
  if (globalFindHandler) {
    window.removeEventListener('keydown', globalFindHandler, { capture: true })
    globalFindHandler = null
  }
})

function parseGoToDateCandidate(input, refStr) {
  const s = input.trim()
  if (!s) return null
  const base = refStr ? fromLocalString(refStr, DEFAULT_TZ) : new Date(),
    baseYear = base.getFullYear()
  // now/today -> system date
  if (/^(now|today)$/i.test(s)) {
    const sys = new Date()
    return toLocalString(
      makeTZDate(sys.getFullYear(), sys.getMonth(), sys.getDate(), DEFAULT_TZ),
      DEFAULT_TZ,
    )
  }
  const localized = Array.from({ length: 12 }, (_, i) => getLocalizedMonthName(i))
  const monthFromToken = (tok) => {
    if (!tok) return null
    const tNorm = norm(tok.trim())
    if (/^\d{1,2}$/.test(tok)) {
      const n = +tok
      return n >= 1 && n <= 12 ? n : null
    }
    for (let i = 0; i < 12; i++) {
      if (norm(monthAbbr[i]) === tNorm || norm(monthAbbr[i].slice(0, 3)) === tNorm) return i + 1
    }
    for (let i = 0; i < 12; i++) {
      const full = norm(localized[i])
      if (full === tNorm || full.startsWith(tNorm)) return i + 1
    }
    return null
  }
  // month token -> 15th of nearest year
  const soleMonth = s.match(/^(\p{L}+)[.,;:]?$/u)
  if (soleMonth) {
    const rawMonthTok = soleMonth[1]
    const m = monthFromToken(rawMonthTok)
    if (m) {
      let bestYear = baseYear
      let best = Infinity
      for (const cand of YEAR_SCAN_OFFSETS.map((o) => baseYear + o)) {
        const mid = new Date(cand, m - 1, 15)
        const diff = Math.abs(mid - base)
        if (diff < best) {
          best = diff
          bestYear = cand
        }
      }
      return toLocalString(makeTZDate(bestYear, m - 1, 15, DEFAULT_TZ), DEFAULT_TZ)
    }
  }
  const isoFull = s.match(/^(\d{4})-(\d{2})-(\d{2})$/)
  if (isoFull) {
    const y = +isoFull[1],
      mm = +isoFull[2],
      d = +isoFull[3]
    return toLocalString(makeTZDate(y, mm - 1, d, DEFAULT_TZ), DEFAULT_TZ)
  }
  // wNN -> Monday of nearest ISO week
  const weekOnly = s.match(/^w(\d{1,2})[.,;:]?$/i)
  if (weekOnly) {
    const wk = +weekOnly[1]
    if (wk >= 1 && wk <= 53) {
      const has53Weeks = (year) => getISOWeek(makeTZDate(year, 11, 28, DEFAULT_TZ)) === 53
      let bestYear = baseYear,
        bestDiff = Infinity,
        bestDate = null
      for (const off of YEAR_SCAN_OFFSETS) {
        const cand = baseYear + off
        if (wk === 53 && !has53Weeks(cand)) continue
        const jan4 = makeTZDate(cand, 0, 4, DEFAULT_TZ)
        const target = addDays(jan4, (wk - 1) * 7)
        const monday = getMondayOfISOWeek(target)
        const diff = Math.abs(monday - base)
        if (diff < bestDiff) {
          bestDiff = diff
          bestYear = cand
          bestDate = monday
        }
      }
      if (bestDate) return toLocalString(bestDate, DEFAULT_TZ)
    }
  }
  const isoMonth = s.match(/^(\d{4})[-/](\d{2})$/)
  if (isoMonth) {
    const y = +isoMonth[1],
      mm = +isoMonth[2]
    return toLocalString(makeTZDate(y, mm - 1, 15, DEFAULT_TZ), DEFAULT_TZ)
  }
  // year+week variants
  let isoWeek = s.match(/^(\d{4})[-/]?w(\d{1,2})$/i)
  if (!isoWeek) isoWeek = s.match(/^w(\d{1,2})[-/]?(\d{4})$/i)
  if (!isoWeek) isoWeek = s.match(/^(\d{4})\s+w(\d{1,2})$/i)
  if (!isoWeek) isoWeek = s.match(/^w(\d{1,2})\s+(\d{4})$/i)
  if (isoWeek) {
    const wy = +isoWeek[1]
    const w = +isoWeek[2]
    if (w >= 1 && w <= 53) {
      if (w === 53 && getISOWeek(makeTZDate(wy, 11, 28, DEFAULT_TZ)) !== 53) return null
      const jan4 = makeTZDate(wy, 0, 4, DEFAULT_TZ)
      const target = addDays(jan4, (w - 1) * 7)
      return toLocalString(getMondayOfISOWeek(target), DEFAULT_TZ)
    }
    return null
  }
  let d = null,
    m = null,
    y = null,
    yearExplicit = false
  const dot = s.match(/^(\d{1,2})\.([\p{L}]+|\d{1,2})(?:\.(\d{4}))?\.?$/u)
  if (dot) {
    d = +dot[1]
    m = monthFromToken(dot[2])
    if (dot[3]) {
      y = +dot[3]
      yearExplicit = true
    }
  }
  if (m == null) {
    const usFull = s.match(/^([\p{L}]+|\d{1,2})\/(\d{1,2})\/(\d{4})$/u)
    if (usFull) {
      m = monthFromToken(usFull[1])
      d = +usFull[2]
      y = +usFull[3]
      yearExplicit = true
    } else {
      const usShort = s.match(/^([\p{L}]+|\d{1,2})\/(\d{1,2})$/u)
      if (usShort) {
        m = monthFromToken(usShort[1])
        d = +usShort[2]
      }
    }
  }
  if (m == null) {
    const tokens = s.split(/[ ,]+/).filter(Boolean)
    if (tokens.length >= 2 && tokens.length <= 3) {
      let monthIdx = tokens.findIndex((t) => /\p{L}/u.test(t) && monthFromToken(t) != null)
      if (monthIdx === -1) monthIdx = tokens.findIndex((t) => monthFromToken(t) != null)
      if (monthIdx !== -1) {
        const monthTok = tokens[monthIdx]
        const monthTokIsNum = /^\d{1,2}$/.test(monthTok)
        const hasNonMonthLetter = tokens.some(
          (t, i) => i !== monthIdx && /\p{L}/u.test(t) && monthFromToken(t) == null,
        )
        const otherNumeric = tokens.some((t, i) => i !== monthIdx && /^\d{1,2}$/.test(t))
        if (monthTokIsNum && hasNonMonthLetter && !otherNumeric) {
          monthIdx = -1
        }
      }
      if (monthIdx !== -1) {
        m = monthFromToken(tokens[monthIdx])
        const others = tokens.filter((_, i) => i !== monthIdx)
        let dayExplicit = false
        for (const rawTok of others) {
          const tok = rawTok.replace(/^[.,;:]+|[.,;:]+$/g, '')
          if (!tok) continue
          if (/^\d+$/.test(tok)) {
            const num = +tok
            if (num > 100) {
              y = num
              yearExplicit = true
            } else if (!d) {
              d = num
              dayExplicit = true
            }
          } else if (!y && /^\d{4}[.,;:]?$/.test(tok)) {
            const num = parseInt(tok, 10)
            if (num > 1000) {
              y = num
              yearExplicit = true
            }
          }
        }
        if (!d && !dayExplicit) d = 15
      }
    }
  }
  if (m != null && d != null && !yearExplicit) {
    let bestYear = baseYear,
      bestDiff = Infinity
    for (const cand of YEAR_SCAN_OFFSETS.map((o) => baseYear + o)) {
      const dt = new Date(cand, m - 1, d)
      if (dt.getMonth() !== m - 1) continue
      const diff = Math.abs(dt - base)
      if (diff < bestDiff) {
        bestDiff = diff
        bestYear = cand
      }
    }
    y = bestYear
  }
  if (y != null && m != null && d != null) {
    if (y < 1000 || y > 9999 || m < 1 || m > 12 || d < 1 || d > 31) return null
    return toLocalString(makeTZDate(y, m - 1, d, DEFAULT_TZ), DEFAULT_TZ)
  }
  return null
}
</script>

<style scoped>
.search-bar {
  flex: 1;
  min-width: 0;
  position: relative;
}
.search-bar input {
  width: 100%;
  padding: 0.32rem 0.5rem;
  padding-inline-start: 2.05rem; /* increased space for icon */
  border-radius: 0.45rem;
  border: 1px solid color-mix(in srgb, var(--muted) 35%, transparent);
  background: color-mix(in srgb, var(--panel) 88%, transparent);
  font: inherit;
  line-height: 1.1;
  color: var(--ink);
  outline: none;
  transition:
    border-color 0.15s ease,
    box-shadow 0.15s ease,
    background 0.2s;
}
.search-bar::before {
  content: '🔍';
  position: absolute;
  inset-inline-start: 0.55rem;
  top: 50%;
  transform: translateY(-50%);
  font-size: 0.85rem;
  pointer-events: none;
  opacity: 0.75;
  line-height: 1;
  filter: saturate(0.8);
}
.search-bar input:focus-visible {
  border-color: color-mix(in srgb, var(--accent, #4b7) 70%, transparent);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent, #4b7) 45%, transparent);
  background: color-mix(in srgb, var(--panel) 95%, transparent);
}
.search-bar input::-webkit-search-cancel-button {
  cursor: pointer;
}

.shortcut-hint {
  position: absolute;
  top: 50%;
  inset-inline-end: 0.5rem;
  transform: translateY(-50%);
  pointer-events: none;
  font-size: 0.75rem;
  opacity: 0.6;
  color: var(--muted);
  font-family: ui-monospace, SF Mono, Consolas, Monaco, "Cascadia Mono", "Segoe UI Mono", "Roboto Mono", "Ubuntu Mono", "Source Code Pro", "Fira Mono", "Droid Sans Mono", "Courier New", monospace;
  background: color-mix(in srgb, var(--panel) 85%, transparent);
  padding: 0.15rem 0.3rem;
  border-radius: 0.25rem;
  border: 1px solid color-mix(in srgb, var(--muted) 25%, transparent);
}

.search-bar input:focus + .shortcut-hint,
.search-bar input:not(:placeholder-shown) + .shortcut-hint {
  display: none;
}
.search-dropdown {
  position: absolute;
  top: calc(100% + 0.25rem);
  left: 0;
  right: 0;
  z-index: 1400;
  list-style: none;
  margin: 0;
  padding: 0.2rem;
  background: color-mix(in srgb, var(--panel) 92%, transparent);
  backdrop-filter: blur(0.6em);
  -webkit-backdrop-filter: blur(0.6em);
  border: 1px solid color-mix(in srgb, var(--muted) 35%, transparent);
  border-radius: 0.55rem;
  max-height: 16rem;
  overflow: auto;
  box-shadow: 0 0.5em 1.25em rgba(0, 0, 0, 0.3);
  font-size: 0.8rem;
}
.search-dropdown li {
  display: flex;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 0.35rem 0.5rem;
  cursor: pointer;
  border-radius: 0.4rem;
}
.search-dropdown li.active {
  background: color-mix(in srgb, var(--accent, #4b7) 45%, transparent);
  color: var(--ink, #111);
  font-weight: 600;
}
.search-dropdown li:hover:not(.active) {
  background: color-mix(in srgb, var(--panel) 70%, transparent);
}
.search-dropdown .title {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.search-dropdown .date {
  opacity: 0.6;
  font-family: monospace;
}
.search-empty {
  position: absolute;
  top: calc(100% + 0.25rem);
  left: 0;
  right: 0;
  padding: 0.45rem 0.6rem;
  background: color-mix(in srgb, var(--panel) 92%, transparent);
  backdrop-filter: blur(0.6em);
  -webkit-backdrop-filter: blur(0.6em);
  border: 1px solid color-mix(in srgb, var(--muted) 35%, transparent);
  border-radius: 0.55rem;
  box-shadow: 0 0.5em 1.25em rgba(0, 0, 0, 0.3);
  font-size: 0.7rem;
  opacity: 0.65;
  pointer-events: none;
  text-align: center;
}
</style>
