<template>
  <section class="web-search-card" :class="{ 'is-collapsed': collapsedState, 'is-pending': isPendingOnly }">
    <button
      v-if="isPendingOnly"
      type="button"
      class="web-search-capsule"
      @mousedown.stop.prevent
      @click.stop="props.onActivate?.()"
    >
      <span class="web-search-badge">WEB</span>
      <span class="web-search-label">{{ startLabel }}</span>
    </button>

    <template v-else>
      <header class="web-search-card__header">
        <div class="web-search-card__meta">
          <div class="web-search-card__title">{{ titleLabel }}</div>
          <div class="web-search-card__subtitle">{{ subtitleLabel }}</div>
        </div>
        <div class="web-search-card__actions">
          <button
            v-if="isBusy"
            type="button"
            class="web-search-card__text-btn"
            contenteditable="false"
            @mousedown.stop.prevent
            @click.stop="props.onCancel?.()"
          >
            {{ t('cancel') || '取消' }}
          </button>
          <template v-else-if="hasContent">
            <button type="button" class="web-search-card__btn" title="压缩搜索结果" contenteditable="false" @mousedown.stop.prevent @click.stop="handleCompress">
              <!-- 压缩完成/空闲：上下箭头向中间挤压，直观表达"压缩" -->
              <svg v-if="compressState === 'idle' || compressState === 'completed'" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                <polyline points="8 3 12 7 16 3"/>
                <polyline points="8 21 12 17 16 21"/>
                <line x1="12" y1="7" x2="12" y2="17"/>
              </svg>
              <span v-if="compressState === 'queued' || compressState === 'processing'" class="web-search-card__spinner"></span>
              <!-- 错误状态：X形四向箭头，表示"取消/错误" -->
              <svg v-else-if="compressState === 'error'" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                <line x1="12" y1="5" x2="12" y2="19"/>
                <line x1="5" y1="12" x2="19" y2="12"/>
                <polyline points="9 8 12 5 15 8"/>
                <polyline points="9 16 12 19 15 16"/>
              </svg>
            </button>
            <button type="button" class="web-search-card__btn" :title="collapsedState ? '展开结果' : '折叠结果'" contenteditable="false" @mousedown.stop.prevent @click.stop="toggleCollapse">
              <svg v-if="collapsedState" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                <polyline points="9 18 15 12 9 6"/>
              </svg>
              <svg v-else width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                <polyline points="6 9 12 15 18 9"/>
              </svg>
            </button>
          </template>
          <button type="button" class="web-search-card__btn web-search-card__btn--danger" title="删除搜索块" contenteditable="false" @mousedown.stop.prevent @click.stop="props.onDelete?.()">
            <!-- 删除按钮：标准垃圾桶图标（梯形桶身 + 提手 + 两条竖线表示桶壁） -->
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
              <polyline points="3 6 5 3 19 3 21 6"/>
              <path d="M20 6v14c0 .6-.4 1-1 1H5c-.6 0-1-.4-1-1V6"/>
              <path d="M5 6l2 14"/>
              <path d="M17 6l-2 14"/>
              <line x1="9" y1="9" x2="9.01" y2="9"/>
              <line x1="14" y1="9" x2="14.01" y2="9"/>
              <line x1="9" y1="13" x2="9.01" y2="13"/>
              <line x1="14" y1="13" x2="14.01" y2="13"/>
            </svg>
          </button>
        </div>
      </header>

      <div v-if="isBusy || hasError" class="web-search-card__progress">
        <div v-if="isBusy" class="web-search-card__progress-row">
          <span class="web-search-card__spinner"></span>
          <span>{{ progressText || '正在联网搜索' }}</span>
        </div>
        <div v-if="hasError" class="web-search-card__error">{{ errorMessage }}</div>
      </div>

      <div v-show="hasContent && !collapsedState" class="web-search-card__body">
        <div ref="editorRoot" class="web-search-card__editor"></div>
      </div>
    </template>
  </section>
</template>

<script setup>
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { replaceAll } from '@milkdown/kit/utils'
import { Crepe } from '@milkdown/crepe'
import { editorViewCtx } from '@milkdown/kit/core'
import { copilotPlugin, copilotConfigCtx, copilotGhostMark, setCopilotEnabled, clearGhostSuggestion } from '../plugins/copilotPlugin'
import { hiddenTextInputPlugin, hiddenTextNode, hiddenTextRemark, hiddenTextView } from '../plugins/hiddenTextPlugin'
import { fetchSuggestion, submitCompress, pollCompressStatus } from '../utils/api.js'
import { isDocumentVisible, getRecommendedDebounce, getRecommendedSyncInterval } from '../composables/useVisibility.js'

const COPILOT_TOGGLE_EVENT = 'llm-in-text:copilot-toggle'
const props = defineProps({
  stage: { type: String, default: 'idle' },
  progressText: { type: String, default: '' },
  errorMessage: { type: String, default: '' },
  content: { type: String, default: '' },
  createdAt: { type: String, default: '' },
  collapsed: { type: Boolean, default: false },
  t: { type: Function, default: (key) => key },
  onActivate: { type: Function, default: null },
  onCancel: { type: Function, default: null },
  onDelete: { type: Function, default: null },
  onUpdateContent: { type: Function, default: null },
  onUpdateCollapsed: { type: Function, default: null },
  resolveSuggestionRequest: { type: Function, default: null },
})

const editorRoot = ref(null)
const collapsedState = ref(Boolean(props.collapsed))
const currentContent = ref(props.content || '')
const compressState = ref('idle')
let crepe = null
let syncTimer = null
let syncingExternal = false
let compressPoller = null
let copilotToggleHandler = null

const ensureEditor = async () => {
  if (crepe || !editorRoot.value || !hasContent.value) return
  crepe = new Crepe({
    root: editorRoot.value,
    defaultValue: props.content || '',
    features: {
      [Crepe.Feature.Latex]: true,
      [Crepe.Feature.ImageBlock]: true,
      [Crepe.Feature.Table]: true,
      [Crepe.Feature.ListCheck]: true,
    },
    config: {
      showLineNumber: false,
    },
  })

  crepe.editor.config((ctx) => {
    ctx.set(copilotConfigCtx.key, {
      fetchSuggestion: async (prefix, suffix, languageId, signal) => {
        const payload = props.resolveSuggestionRequest
          ? await props.resolveSuggestionRequest({ prefix, suffix, languageId })
          : { prefix, suffix, languageId, blocked: false }
        if (payload?.blocked) return ''
        return fetchSuggestion(payload?.prefix ?? prefix, payload?.suffix ?? suffix, payload?.languageId ?? languageId, signal)
      },
      debounceMs: getRecommendedDebounce(900),
    })
  })

  crepe.editor.use(copilotConfigCtx)
  crepe.editor.use(copilotGhostMark)
  crepe.editor.use(copilotPlugin)
  crepe.editor.use(hiddenTextRemark)
  crepe.editor.use(hiddenTextNode)
  crepe.editor.use(hiddenTextView)
  crepe.editor.use(hiddenTextInputPlugin)
  await crepe.create()

  crepe.on((listener) => {
    listener.updated(() => {
      syncContent()
    })
  })

  crepe.editor.action((ctx) => {
    const view = ctx.get(editorViewCtx)
    const enabled = typeof window !== 'undefined'
      ? window.__LLM_IN_TEXT_COPILOT_ENABLED__ !== false
      : true
    setCopilotEnabled(view, enabled)
    if (!enabled) {
      clearGhostSuggestion(view)
    }
  })

  copilotToggleHandler = (event) => {
    const enabled = Boolean(event?.detail?.enabled)
    crepe?.editor?.action((ctx) => {
      const view = ctx.get(editorViewCtx)
      setCopilotEnabled(view, enabled)
      if (!enabled) {
        clearGhostSuggestion(view)
      }
    })
  }
  window.addEventListener(COPILOT_TOGGLE_EVENT, copilotToggleHandler)
}

const hasContent = computed(() => Boolean((props.content || '').trim()))
const hasError = computed(() => Boolean((props.errorMessage || '').trim()))
const isBusy = computed(() => props.stage !== 'idle' && props.stage !== 'done' && props.stage !== 'error' && props.stage !== 'cancelled')
const isPendingOnly = computed(() => !hasContent.value && !isBusy.value && !hasError.value)
const startLabel = computed(() => props.t('webSearchStart') || '开始联网搜索')
const titleLabel = computed(() => hasContent.value ? (props.t('webSearchResultTitle') || '搜索结果') : (props.t('webSearchTitle') || '联网搜索'))
const progressText = computed(() => props.progressText || props.t('webSearchWorking') || '正在联网搜索')
const subtitleLabel = computed(() => {
  if (hasContent.value) {
    if (!props.createdAt) return props.t('webSearchDone') || '搜索已完成'
    const date = new Date(props.createdAt)
    if (Number.isNaN(date.getTime())) return props.t('webSearchDone') || '搜索已完成'
    return date.toLocaleString('zh-CN', { hour12: false })
  }
  if (hasError.value) return props.t('webSearchError') || '搜索失败'
  if (isBusy.value) return progressText.value
  return props.t('webSearchIdleHint') || '点击后自动生成关键词并抓取网页'
})

const toggleCollapse = () => {
  collapsedState.value = !collapsedState.value
  props.onUpdateCollapsed?.(collapsedState.value)
}

const handleCompress = () => {
  if (compressState.value !== 'idle' || !crepe) return
  const content = crepe.getMarkdown?.() || ''
  if (!String(content || '').trim()) {
    compressState.value = 'error'
    setTimeout(() => { compressState.value = 'idle' }, 2000)
    return
  }

  submitCompress(content, 'txt').then((result) => {
    compressState.value = 'queued'
    if (compressPoller) compressPoller.stop()
    compressPoller = pollCompressStatus(result.task_id, (status, compressedContent) => {
      compressState.value = status
      if (status === 'completed' && compressedContent) {
        crepe.editor.action(replaceAll(compressedContent))
      } else if (status === 'error') {
        setTimeout(() => { compressState.value = 'idle' }, 3000)
      }
    })
  }).catch(() => {
    compressState.value = 'error'
    setTimeout(() => { compressState.value = 'idle' }, 3000)
  })
}

const syncContent = () => {
  if (!crepe) return
  if (!isDocumentVisible()) return
  if (compressState.value !== 'idle') return
  if (syncTimer) clearTimeout(syncTimer)
  syncTimer = setTimeout(async () => {
    if (!crepe || syncingExternal) return
    const markdown = await crepe.getMarkdown()
    currentContent.value = markdown
    props.onUpdateContent?.(markdown)
  }, getRecommendedSyncInterval(120))
}

const syncExternalContent = async (nextValue) => {
  const value = nextValue || ''
  if (!crepe) {
    currentContent.value = value
    return
  }
  if (value === currentContent.value) return
  if (syncTimer) {
    clearTimeout(syncTimer)
    syncTimer = null
  }
  syncingExternal = true
  try {
    crepe.editor.action(replaceAll(value))
    currentContent.value = value
  } finally {
    syncingExternal = false
  }
}

watch(() => props.content, (nextValue) => {
  if (nextValue && !crepe) {
    void ensureEditor()
  }
  void syncExternalContent(nextValue)
})

watch(() => props.collapsed, (nextValue) => {
  collapsedState.value = Boolean(nextValue)
})

onMounted(async () => {
  await ensureEditor()
})

onUnmounted(() => {
  if (syncTimer) {
    clearTimeout(syncTimer)
    syncTimer = null
  }
  if (compressPoller) {
    compressPoller.stop()
    compressPoller = null
  }
  if (copilotToggleHandler) {
    window.removeEventListener(COPILOT_TOGGLE_EVENT, copilotToggleHandler)
    copilotToggleHandler = null
  }
  if (crepe) {
    crepe.editor.action((ctx) => {
      const view = ctx.get(editorViewCtx)
      clearGhostSuggestion(view)
    })
    crepe.destroy()
    crepe = null
  }
})
</script>

<style scoped>
.web-search-card {
  width: 100%;
  margin: 12px 0;
}

.web-search-capsule,
.web-search-card__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  width: 100%;
  min-height: 48px;
  padding: 11px 14px;
  border: 1px solid rgba(14, 116, 144, 0.24);
  background: linear-gradient(135deg, rgba(239, 246, 255, 0.96), rgba(224, 242, 254, 0.94));
  color: #0f172a;
}

.web-search-capsule {
  border-radius: 999px;
  cursor: pointer;
}

.web-search-card__header {
  border-radius: 14px 14px 0 0;
}

.web-search-card:not(.is-pending) {
  border: 1px solid rgba(14, 116, 144, 0.16);
  border-radius: 14px;
  overflow: hidden;
  background: rgba(255, 255, 255, 0.9);
}

:root[data-theme='dark'] .web-search-capsule,
:root[data-theme='dark'] .web-search-card__header {
  background: linear-gradient(135deg, rgba(8, 47, 73, 0.92), rgba(17, 24, 39, 0.96));
  color: #e2e8f0;
  border-color: rgba(34, 211, 238, 0.22);
}

:root[data-theme='dark'] .web-search-card:not(.is-pending) {
  background: rgba(15, 23, 42, 0.88);
  border-color: rgba(34, 211, 238, 0.16);
}

.web-search-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  padding: 4px 10px;
  border-radius: 999px;
  background: linear-gradient(135deg, #0ea5e9, #14b8a6);
  color: #fff;
  font-size: 11px;
  font-weight: 700;
}

.web-search-label,
.web-search-card__title {
  font-size: 14px;
  font-weight: 700;
}

.web-search-card__meta {
  min-width: 0;
}

.web-search-card__subtitle {
  margin-top: 2px;
  font-size: 12px;
  color: rgba(15, 23, 42, 0.72);
}

:root[data-theme='dark'] .web-search-card__subtitle {
  color: rgba(226, 232, 240, 0.78);
}

.web-search-card__actions {
  display: flex;
  align-items: center;
  gap: 6px;
}

.web-search-card__btn,
.web-search-card__text-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border: none;
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.82);
  color: inherit;
  cursor: pointer;
}

.web-search-card__text-btn {
  width: auto;
  padding: 0 10px;
  font-size: 12px;
}

:root[data-theme='dark'] .web-search-card__btn,
:root[data-theme='dark'] .web-search-card__text-btn {
  background: rgba(15, 23, 42, 0.72);
}

.web-search-card__btn--danger {
  color: #dc2626;
}

.web-search-card__progress {
  padding: 14px;
  border-top: 1px solid rgba(14, 116, 144, 0.12);
}

.web-search-card__progress-row {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 13px;
}

.web-search-card__error {
  margin-top: 10px;
  font-size: 13px;
  color: #b91c1c;
}

.web-search-card__body {
  border-top: 1px solid rgba(14, 116, 144, 0.12);
}

.web-search-card__editor {
  min-height: 0;
  height: auto;
  max-height: none;
  overflow: auto;
  overscroll-behavior-y: contain;
  scrollbar-width: thin;
  scrollbar-color: var(--scrollbar-thumb) transparent;
}

.web-search-card__editor :deep(.milkdown),
.web-search-card__editor :deep(.milkdown__main),
.web-search-card__editor :deep(.milkdown__editor) {
  min-height: 0;
  height: auto !important;
}

.web-search-card__editor :deep(.ProseMirror) {
  min-height: 0;
  height: auto !important;
  overflow-x: hidden;
}

.web-search-card__editor :deep(.cm-scroller) {
  overflow-x: hidden;
  overflow-y: auto;
  overscroll-behavior-y: contain;
  scrollbar-width: thin;
  scrollbar-color: var(--scrollbar-thumb) transparent;
}

.web-search-card__editor :deep(.cm-editor) {
  min-height: 0;
  height: 100%;
  overflow: hidden;
}

.web-search-card__spinner {

  width: 14px;
  height: 14px;
  border: 2px solid rgba(14, 116, 144, 0.18);
  border-top-color: #0ea5e9;
  border-radius: 999px;
  animation: web-search-spin 0.8s linear infinite;
}

@keyframes web-search-spin {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}
</style>
