<script setup>
/**
 * @typedef {{ name: string, slug: string }} SearchTagHit
 * @typedef {{ slug: string, title: string, excerpt: string }} SearchPostHit
 */

const open = defineModel({ type: Boolean, default: false })

const searchInputRef = ref(null)
const query = ref('')
const debouncedQuery = ref('')
const isComposing = ref(false)

/** @type {ReturnType<typeof setTimeout> | null} */
let debounceTimer = null

/**
 * 입력값을 디바운스로 검색어에 반영한다.
 * @param {string} value - 현재 입력값
 * @returns {void}
 */
const scheduleDebouncedQuery = (value) => {
  clearTimeout(debounceTimer)
  debounceTimer = setTimeout(() => {
    debouncedQuery.value = value
  }, 200)
}

watch(query, (value) => {
  scheduleDebouncedQuery(value)
})

const fetchKey = computed(() => `public-search:${debouncedQuery.value}`)

const { data, pending } = await useFetch('/api/search', {
  key: fetchKey,
  query: { q: debouncedQuery },
  watch: [debouncedQuery],
  default: () => ({ tags: /** @type {SearchTagHit[]} */ ([]), posts: /** @type {SearchPostHit[]} */ ([]) })
})

/**
 * 정규식 특수 문자 이스케이프
 * @param {string} value - 원문
 * @returns {string} 이스케이프된 문자열
 */
const escapeRegExp = (value) => value.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&')

/**
 * 검색어 일치 구간을 나누어 하이라이트 표시에 사용한다.
 * @param {string} text - 표시할 문자열
 * @param {string} needle - 검색어
 * @returns {Array<{ text: string, hit: boolean }>} 구간 목록
 */
const highlightParts = (text, needle) => {
  const base = String(text ?? '')
  const q = String(needle ?? '').trim()
  if (!q) {
    return [{ text: base, hit: false }]
  }
  const parts = base.split(new RegExp(`(${escapeRegExp(q)})`, 'gi'))
  return parts
    .filter((part) => part !== '')
    .map((part, index) => ({
      text: part,
      hit: index % 2 === 1
    }))
}

/**
 * 목록용 요약 문자열을 한 줄로 자른다.
 * @param {string} text - 요약 원문
 * @param {number} max - 최대 글자 수
 * @returns {string} 잘린 문자열
 */
const clipExcerpt = (text, max = 140) => {
  const line = String(text ?? '').replace(/\s+/g, ' ').trim()
  if (line.length <= max) {
    return line
  }
  return `${line.slice(0, max)}…`
}

/**
 * 배경 클릭 시 모달을 닫는다.
 * @returns {void}
 */
const onBackdropPointerDown = () => {
  open.value = false
}

/**
 * 검색어를 비운다(입력 포커스 유지).
 * @returns {void}
 */
const clearQuery = () => {
  query.value = ''
  debouncedQuery.value = ''
  nextTick(() => {
    const el = searchInputRef.value
    if (el instanceof HTMLInputElement) {
      el.focus()
    }
  })
}

/**
 * 한글/일본어 등 IME 조합 시작 처리
 * @returns {void}
 */
const onCompositionStart = () => {
  isComposing.value = true
}

/**
 * IME 조합 중에도 입력창 value를 기반으로 검색어를 갱신한다.
 * 일부 환경에서는 v-model 업데이트가 조합 종료까지 지연될 수 있다.
 * @param {CompositionEvent} event - 조합 업데이트 이벤트
 * @returns {void}
 */
const onCompositionUpdate = (event) => {
  const target = event.target
  if (!(target instanceof HTMLInputElement)) {
    return
  }
  const value = target.value
  query.value = value
  scheduleDebouncedQuery(value)
}

/**
 * 한글/일본어 등 IME 조합 종료 처리(종료 시점에만 검색 갱신)
 * @returns {void}
 */
const onCompositionEnd = () => {
  isComposing.value = false
  clearTimeout(debounceTimer)
  debouncedQuery.value = query.value
}

/**
 * 모달 열림·닫힘에 따라 스크롤 잠금과 입력을 동기화한다.
 * @returns {void}
 */
watch(open, (isOpen) => {
  if (typeof document === 'undefined') {
    return
  }
  document.documentElement.classList.toggle('site-search-open', Boolean(isOpen))
  if (isOpen) {
    nextTick(() => {
      const el = searchInputRef.value
      if (el instanceof HTMLInputElement) {
        el.focus()
      }
    })
    return
  }
  query.value = ''
  debouncedQuery.value = ''
})

onBeforeUnmount(() => {
  if (typeof document !== 'undefined') {
    document.documentElement.classList.remove('site-search-open')
  }
  clearTimeout(debounceTimer)
})
</script>

<template>
  <Teleport to="body">
    <div
      v-show="open"
      class="site-search-modal fixed inset-0 z-[60] flex justify-center bg-black/35 px-3 pt-14 pb-8 backdrop-blur-sm sm:px-4 sm:pt-20"
      role="dialog"
      aria-modal="true"
      aria-label="사이트 검색"
      @pointerdown.self="onBackdropPointerDown"
    >
      <div
        class="site-search-modal__panel site-search-modal__panel--animate flex h-[min(78vh,640px)] w-full max-w-[95vw] flex-col overflow-hidden rounded-lg border border-[var(--site-line)] bg-[var(--site-bg)] text-[var(--site-text)] shadow-[0_20px_50px_rgba(0,0,0,0.18)] sm:max-w-lg"
        @pointerdown.stop
      >
        <div class="site-search-modal__header flex shrink-0 items-center gap-2 border-b border-[var(--site-line)] bg-[var(--site-bg)] px-3 py-3 sm:gap-3 sm:px-5 sm:py-4">
          <button
            type="button"
            class="site-search-modal__icon flex h-9 w-9 shrink-0 items-center justify-center rounded-md text-[var(--site-text)] transition-colors hover:bg-[var(--site-panel)]"
            :aria-label="query.trim() ? '검색어 지우기' : '검색'"
            :disabled="!query.trim()"
            @click="query.trim() ? clearQuery() : null"
          >
            <svg v-if="!query.trim()" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true">
              <path d="M23.38 21.62l-6.53-6.53a9.15 9.15 0 0 0 1.9-5.59 9.27 9.27 0 1 0-3.66 7.36l6.53 6.53a1.26 1.26 0 0 0 1.76 0 1.25 1.25 0 0 0 0-1.77ZM2.75 9.5A6.75 6.75 0 1 1 9.5 16.25 6.76 6.76 0 0 1 2.75 9.5Z" />
            </svg>
            <svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
              <path d="M18 6 6 18" />
              <path d="m6 6 12 12" />
            </svg>
          </button>
          <input
            ref="searchInputRef"
            v-model="query"
            type="text"
            inputmode="search"
            enterkeyhint="search"
            autocomplete="off"
            class="site-search-modal__input min-w-0 flex-1 bg-transparent py-2 text-lg outline-none placeholder:text-[var(--site-soft)] focus-visible:ring-0 sm:text-[1.35rem]"
            placeholder="글 제목, 본문, 태그 검색"
            @compositionstart="onCompositionStart"
            @compositionupdate="onCompositionUpdate"
            @compositionend="onCompositionEnd"
          />
          <button type="button" class="site-search-modal__cancel shrink-0 rounded-md px-2 py-1 text-sm text-[var(--site-soft)] hover:text-[var(--site-text)] sm:hidden" @click="open = false">
            취소
          </button>
        </div>

        <div class="site-search-modal__body min-h-0 flex-1 overflow-y-auto overscroll-contain px-3 py-3 sm:px-5 sm:py-4">
          <p v-if="!query.trim()" class="site-search-modal__hint text-sm text-[var(--site-soft)]">
            검색어를 입력하면 태그와 게시물이 섹션별로 표시됩니다.
          </p>

          <template v-else>
            <div v-if="pending" class="text-sm text-[var(--site-soft)]">
              검색 중…
            </div>
            <template v-else>
              <section v-if="(data?.tags?.length ?? 0) > 0" class="site-search-modal__section mb-6">
                <h2 class="site-search-modal__section-title mb-2 text-[11px] font-semibold uppercase tracking-wide text-[var(--site-soft)]">
                  Tags
                </h2>
                <ul class="space-y-0.5">
                  <li v-for="tag in data.tags" :key="tag.slug">
                    <NuxtLink
                      class="site-search-modal__tag-link flex items-baseline gap-1 rounded-md px-2 py-2 text-[15px] transition-colors hover:bg-[var(--site-panel)]"
                      :to="`/tag/${tag.slug}/`"
                      @click="open = false"
                    >
                      <span class="text-[var(--site-soft)]">#</span>
                      <span class="font-medium text-[var(--site-text)]">{{ tag.name }}</span>
                    </NuxtLink>
                  </li>
                </ul>
              </section>

              <section v-if="(data?.posts?.length ?? 0) > 0" class="site-search-modal__section">
                <h2 class="site-search-modal__section-title mb-2 text-[11px] font-semibold uppercase tracking-wide text-[var(--site-soft)]">
                  Posts
                </h2>
                <ul class="space-y-1">
                  <li v-for="post in data.posts" :key="post.slug">
                    <NuxtLink
                      class="site-search-modal__post-link block rounded-md px-2 py-2 transition-colors hover:bg-[var(--site-panel)]"
                      :to="`/post/${post.slug}/`"
                      @click="open = false"
                    >
                      <div class="text-[15px] font-semibold leading-snug text-[var(--site-text)]">
                        <template v-for="(seg, i) in highlightParts(post.title, query)" :key="`t-${post.slug}-${i}`">
                          <mark v-if="seg.hit" class="bg-transparent font-semibold text-[var(--site-text)]">{{ seg.text }}</mark>
                          <span v-else>{{ seg.text }}</span>
                        </template>
                      </div>
                      <div v-if="clipExcerpt(post.excerpt)" class="mt-0.5 line-clamp-2 text-sm leading-relaxed text-[var(--site-soft)]">
                        <template v-for="(seg, i) in highlightParts(clipExcerpt(post.excerpt), query)" :key="`e-${post.slug}-${i}`">
                          <mark v-if="seg.hit" class="bg-transparent font-semibold text-[var(--site-muted)]">{{ seg.text }}</mark>
                          <span v-else>{{ seg.text }}</span>
                        </template>
                      </div>
                    </NuxtLink>
                  </li>
                </ul>
              </section>

              <p v-if="!pending && (data?.tags?.length ?? 0) === 0 && (data?.posts?.length ?? 0) === 0" class="text-sm text-[var(--site-soft)]">
                일치하는 결과가 없습니다.
              </p>
            </template>
          </template>
        </div>
      </div>
    </div>
  </Teleport>
</template>
