<template>
  <div class="map-container">
    <div class="atmo-bg"></div>

    <div class="novel-selector">
      <button
        v-for="(novel, key) in NOVELS"
        :key="key"
        :class="['novel-btn', { active: activeNovelId === key }]"
        @click="switchNovel(key)"
      >
        {{ novel.name }}
      </button>
    </div>

    <div class="map-state-pill">
      {{ currentNovel.name }} · {{ currentVolLabel }} · 缩放 {{ mapViewState.zoom.toFixed(1) }}
    </div>

    <div id="map"></div>

    <div class="legend">
      <div class="legend-block legend-factions">
        <h3>势力图例</h3>
        <input
          v-model.trim="factionQuery"
          class="legend-search"
          type="text"
          placeholder="搜索势力/首领/人物"
        >
        <div class="legend-list legend-faction-list">
          <div
            v-for="faction in filteredFactions"
            :key="faction.id"
            class="legend-item"
            @click="showFactionInfo(faction)"
          >
            <div class="legend-dot" :style="{ background: faction.color }"></div>
            <span>{{ faction.name }}（{{ faction.leader }}）</span>
          </div>
          <div v-if="!filteredFactions.length" class="empty-tip">未匹配到势力</div>
        </div>
      </div>

      <div class="legend-block legend-routes">
        <h3>人物路线</h3>
        <input
          v-model.trim="routeQuery"
          class="legend-search"
          type="text"
          placeholder="搜索人物路线"
        >

        <div class="route-toolbar">
          <button class="mini-btn" @click="selectTopRoutes">显示高频路线</button>
          <button class="mini-btn" @click="clearAllRoutes">清空</button>
          <button class="mini-btn" :class="{ active: showRouteOrder }" @click="showRouteOrder = !showRouteOrder">
            {{ showRouteOrder ? '顺序标记:开' : '顺序标记:关' }}
          </button>
        </div>

        <div class="legend-list legend-route-list">
          <div
            v-for="route in filteredRouteDefs"
            :key="route.character"
            class="legend-item route-item"
            @click="toggleRoute(route)"
          >
            <div
              class="legend-line"
              :style="{
                background: route.color,
                opacity: selectedRoutes.includes(route.character) ? 1 : 0.35
              }"
            ></div>
            <span :style="{ opacity: selectedRoutes.includes(route.character) ? 1 : 0.55 }">{{ route.character }}</span>
            <button
              v-if="selectedRoutes.includes(route.character)"
              class="focus-btn"
              :class="{ active: focusedRouteName === route.character }"
              @click.stop="focusRoute(route.character)"
            >
              {{ focusedRouteName === route.character ? '聚焦中' : '聚焦' }}
            </button>
          </div>
          <div v-if="!filteredRouteDefs.length" class="empty-tip">未匹配到路线</div>
        </div>
      </div>

      <div class="route-insight" v-if="focusedRouteSteps.length">
        <div class="insight-header">
          <span>路线细读：{{ focusedRouteName }}</span>
          <button class="mini-btn" @click="clearRouteFocus">退出聚焦</button>
        </div>
        <div class="insight-list">
          <div
            v-for="(step, idx) in focusedRouteSteps"
            :key="`${focusedRouteName}-${idx}`"
            class="insight-item"
            @click="flyToRouteStep(step)"
          >
            <span class="step-idx">{{ idx + 1 }}</span>
            <span class="step-main">{{ step.locationName }}</span>
            <span class="step-sub">卷{{ step.vol || '?' }} 第{{ step.chapter || '?' }}{{ currentNovel.volsLabel }}</span>
          </div>
        </div>
      </div>
    </div>

    <div class="chapter-panel">
      <h3>卷册进度</h3>
      <div class="vol-slider-wrap">
        <input
          type="range"
          class="chapter-slider"
          v-model.number="currentVol"
          :min="1"
          :max="TOTAL_VOLS"
          @input="onVolSliderChange"
        >
        <div class="vol-ticks">
          <span
            v-for="tick in volTicks"
            :key="tick.vol"
            class="tick-label"
            :style="{ left: tick.pct + '%' }"
          >{{ tick.label }}</span>
        </div>
      </div>
      <div class="vol-info">
        当前：{{ currentVolLabel }} | 已加载 {{ loadedCount }} / {{ TOTAL_VOLS }} {{ currentNovel.volsLabel }}
      </div>

      <div class="search-panel">
        <div class="events-title">地图搜索</div>
        <input
          v-model.trim="mapSearchQuery"
          class="map-search-input"
          type="text"
          placeholder="搜地点 / 势力 / 人物"
        >
        <div class="search-results" v-if="mapSearchQuery">
          <div
            v-for="result in globalSearchResults"
            :key="result.key"
            class="search-result-item"
            @click="applySearchResult(result)"
          >
            <span class="search-type">{{ result.type }}</span>
            <span class="search-name">{{ result.name }}</span>
          </div>
          <div v-if="!globalSearchResults.length" class="empty-tip">暂无匹配</div>
        </div>
      </div>

      <div class="events-section" v-if="currentKeyEvents.length">
        <div class="events-title">本卷关键事件</div>
        <div
          v-for="(ev, idx) in currentKeyEvents"
          :key="idx"
          class="event-item"
          @click="flyToEvent(ev)"
        >
          <span class="event-ch">第{{ ev.chapter }}{{ currentNovel.volsLabel }}</span>
          <span class="event-text">{{ ev.event }}</span>
        </div>
      </div>
    </div>

    <div class="info-panel" v-if="selectedFaction">
      <span class="close-btn" @click="selectedFaction = null">&times;</span>
      <h3 :style="{ color: selectedFaction.color }">{{ selectedFaction.name }}</h3>
      <p><b>首领：</b>{{ selectedFaction.leader }}</p>
      <p>{{ selectedFaction.description }}</p>
      <p v-if="selectedFaction.territory && selectedFaction.territory.length">
        <b>据点：</b>{{ getTerritoryNames(selectedFaction.territory).join('、') }}
      </p>
      <p v-if="selectedFaction.key_figures && selectedFaction.key_figures.length">
        <b>关键人物：</b>{{ selectedFaction.key_figures.join('、') }}
      </p>
    </div>

    <div class="loading-bar" v-if="isLoading">
      <div class="loading-inner" :style="{ width: (loadedCount / TOTAL_VOLS * 100) + '%' }"></div>
      <span class="loading-text">加载数据中… {{ loadedCount }}/{{ TOTAL_VOLS }} {{ currentNovel.volsLabel }}</span>
    </div>

    <div class="chat-toggle-btn" @click="toggleChat" v-if="!isChatOpen" title="打开智能助手" aria-label="打开智能助手">
      智能问答
    </div>

    <div class="chat-panel" v-show="isChatOpen">
      <div class="chat-header">
        <h3>知识问答</h3>
        <span class="close-btn" @click="toggleChat">&times;</span>
      </div>

      <div class="chat-context-panel">
        <label>
          <input type="checkbox" v-model="includeMapContext">
          把当前地图上下文一起发给助手
        </label>
        <div class="context-preview" v-if="includeMapContext">
          {{ mapContextSummary }}
        </div>
      </div>

      <div class="chat-messages" ref="chatMessagesRef">
        <div v-for="(msg, idx) in chatMessages" :key="idx" :class="['chat-bubble', msg.role]">
          {{ msg.content }}
        </div>
        <div v-if="isChatLoading" class="chat-bubble assistant loading">思索中...</div>
      </div>
      <div class="chat-input-area">
        <input
          type="text"
          v-model="chatInput"
          @keyup.enter="sendChatMessage"
          :placeholder="currentNovel.chatPlaceholder"
        >
        <button @click="sendChatMessage" :disabled="isChatLoading">发送</button>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, onMounted, watch, nextTick } from 'vue'
import L from 'leaflet'
import 'leaflet/dist/leaflet.css'
import { NOVELS } from './novel-config.js'

// ── 常量 ────────────────────────────────────────────────────
const KOUZHONG_NAME = '寇仲'
const KOUZHONG_COLOR = '#ff4500'
const XUZILING_NAME = '徐子陵'
const XUZILING_COLOR = '#ffa07a'

// ── 响应式状态 ──────────────────────────────────────────────
const activeNovelId = ref('dtslz')
const currentNovel = computed(() => NOVELS[activeNovelId.value])
const TOTAL_VOLS = computed(() => currentNovel.value.totalVols)

const allData = ref([])
const currentVol = ref(1)
const selectedRoutes = ref([])
const focusedRouteName = ref('')
const showRouteOrder = ref(true)
const selectedFaction = ref(null)
const isLoading = ref(true)
const loadedCount = ref(0)
const mapViewState = ref({ center: [33.0, 113.0], zoom: 6 })
const lastInteraction = ref('')

const factionQuery = ref('')
const routeQuery = ref('')
const mapSearchQuery = ref('')

// ── 问答系统状态 ────────────────────────────────────────────
const isChatOpen = ref(false)
const chatInput = ref('')
const isChatLoading = ref(false)
const includeMapContext = ref(true)
const chatMessagesRef = ref(null)
const chatMessages = ref([
  { role: 'assistant', content: '您好！我是知识助理。我可以结合你当前浏览的地图状态回答问题。' }
])

// ── 小说切换逻辑 ──────────────────────────────────────────
async function switchNovel(novelId) {
  if (activeNovelId.value === novelId) return

  activeNovelId.value = novelId
  currentVol.value = 1
  selectedRoutes.value = []
  focusedRouteName.value = ''
  selectedFaction.value = null
  factionQuery.value = ''
  routeQuery.value = ''
  mapSearchQuery.value = ''

  chatMessages.value = [
    { role: 'assistant', content: `您好！我是${currentNovel.value.name}知识助理。你可以问人物行踪、势力分布、章节事件。` }
  ]

  await loadAllData()

  if (map) {
    if (novelId === 'ldj') {
      map.setView([33.0, 113.0], 5)
    } else if (novelId === 'tlbb') {
      map.setView([33.0, 105.0], 5)
    } else {
      map.setView([33.0, 113.0], 6)
    }
  }

  drawAll()
}

// ── Leaflet 图层管理 ────────────────────────────────────────
let map = null
let layerGroups = {
  territories: null,
  locations: null,
  routes: null,
  events: null
}

// ── 计算属性 ───────────────────────────────────────────────
const currentVolLabel = computed(() => {
  const d = allData.value[currentVol.value - 1]
  return (d && d.volume) ? d.volume : `第${currentVol.value}${currentNovel.value.volsLabel}`
})

const volTicks = computed(() => {
  const maxVol = TOTAL_VOLS.value
  const points = new Set([1, maxVol])
  for (let i = 1; i <= 4; i++) {
    points.add(Math.max(1, Math.min(maxVol, Math.round((maxVol - 1) * (i / 5)) + 1)))
  }

  return [...points].sort((a, b) => a - b).map((vol) => ({
    vol,
    label: `${vol}${currentNovel.value.volsLabel}`,
    pct: maxVol === 1 ? 0 : ((vol - 1) / (maxVol - 1) * 100).toFixed(2)
  }))
})

const mergedLocations = computed(() => {
  const locMap = new Map()
  for (let i = 0; i < currentVol.value; i++) {
    const d = allData.value[i]
    if (!d) continue
    for (const loc of (d.locations || [])) {
      locMap.set(loc.id, loc)
    }
  }
  return Array.from(locMap.values())
})

const mergedFactions = computed(() => {
  const facMap = new Map()
  for (let i = 0; i < currentVol.value; i++) {
    const d = allData.value[i]
    if (!d) continue
    for (const f of (d.factions || [])) {
      facMap.set(f.id, f)
    }
  }
  return Array.from(facMap.values())
})

const mergedRoutes = computed(() => {
  const charMap = new Map()

  for (let i = 0; i < currentVol.value; i++) {
    const d = allData.value[i]
    if (!d) continue
    const vol = i + 1

    if (activeNovelId.value === 'dtslz' && vol <= 20) {
      let combinedRoute = []
      for (const r of (d.character_routes || [])) {
        if (r.character === KOUZHONG_NAME || r.character === XUZILING_NAME) {
          combinedRoute = combinedRoute.concat(r.route || [])
          continue
        }
        if (!charMap.has(r.character)) {
          charMap.set(r.character, { character: r.character, color: r.color, route: [] })
        }
        const entry = charMap.get(r.character)
        entry.color = r.color
        for (const pt of (r.route || [])) entry.route.push({ ...pt, vol })
      }

      if (combinedRoute.length) {
        const comboName = `${KOUZHONG_NAME} & ${XUZILING_NAME}`
        if (!charMap.has(comboName)) {
          charMap.set(comboName, { character: comboName, color: KOUZHONG_COLOR, route: [] })
        }
        const combo = charMap.get(comboName)
        combo.color = combo.color || XUZILING_COLOR
        for (const pt of combinedRoute) combo.route.push({ ...pt, vol })
      }
    } else {
      for (const r of (d.character_routes || [])) {
        if (!charMap.has(r.character)) {
          charMap.set(r.character, { character: r.character, color: r.color, route: [] })
        }
        const entry = charMap.get(r.character)
        entry.color = r.color
        for (const pt of (r.route || [])) entry.route.push({ ...pt, vol })
      }
    }
  }

  return Array.from(charMap.values()).sort((a, b) => b.route.length - a.route.length)
})

const visibleRouteDefs = computed(() => mergedRoutes.value.map(r => ({
  character: r.character,
  color: r.color,
  points: r.route.length
})))

const filteredFactions = computed(() => {
  const q = factionQuery.value.toLowerCase()
  if (!q) return mergedFactions.value
  return mergedFactions.value.filter((f) => {
    const figures = Array.isArray(f.key_figures) ? f.key_figures.join(' ') : ''
    return (`${f.name} ${f.leader || ''} ${figures}`).toLowerCase().includes(q)
  })
})

const filteredRouteDefs = computed(() => {
  const q = routeQuery.value.toLowerCase()
  if (!q) return visibleRouteDefs.value
  return visibleRouteDefs.value.filter((r) => r.character.toLowerCase().includes(q))
})

const currentKeyEvents = computed(() => {
  const d = allData.value[currentVol.value - 1]
  return d ? (d.key_events || []) : []
})

const focusedRouteData = computed(() =>
  mergedRoutes.value.find((r) => r.character === focusedRouteName.value) || null
)

const focusedRouteSteps = computed(() => {
  if (!focusedRouteData.value) return []
  const locMap = new Map(mergedLocations.value.map((l) => [l.id, l]))

  return (focusedRouteData.value.route || [])
    .map((pt, idx) => {
      const coords = getPointCoords(pt, mergedLocations.value)
      if (!coords) return null
      const loc = pt.location ? locMap.get(pt.location) : null
      return {
        idx,
        lat: coords.lat,
        lng: coords.lng,
        chapter: pt.chapter,
        vol: pt.vol,
        event: pt.event,
        locationName: loc?.name || pt.location || `坐标点 ${idx + 1}`
      }
    })
    .filter(Boolean)
})

const globalSearchResults = computed(() => {
  const q = mapSearchQuery.value.trim().toLowerCase()
  if (!q) return []

  const locationHits = mergedLocations.value
    .filter((l) => `${l.name} ${l.id}`.toLowerCase().includes(q))
    .slice(0, 6)
    .map((l) => ({ key: `loc-${l.id}`, type: '地点', name: l.name, payload: l }))

  const factionHits = mergedFactions.value
    .filter((f) => `${f.name} ${f.leader || ''}`.toLowerCase().includes(q))
    .slice(0, 5)
    .map((f) => ({ key: `fac-${f.id}`, type: '势力', name: f.name, payload: f }))

  const routeHits = visibleRouteDefs.value
    .filter((r) => r.character.toLowerCase().includes(q))
    .slice(0, 5)
    .map((r) => ({ key: `route-${r.character}`, type: '人物', name: r.character, payload: r }))

  return [...locationHits, ...factionHits, ...routeHits].slice(0, 12)
})

const mapContextSummary = computed(() => {
  const center = mapViewState.value.center
  const selected = selectedRoutes.value.length ? selectedRoutes.value.slice(0, 5).join('、') : '无'
  const focused = focusedRouteName.value || '无'
  const faction = selectedFaction.value?.name || '无'
  const eventTitles = currentKeyEvents.value.slice(0, 3).map((e) => e.event).join('；') || '无'

  return [
    `小说=${currentNovel.value.name}`,
    `进度=${currentVolLabel.value}`,
    `地图中心=(${center[0].toFixed(2)}, ${center[1].toFixed(2)})`,
    `缩放=${mapViewState.value.zoom.toFixed(1)}`,
    `当前选中路线=${selected}`,
    `当前聚焦人物=${focused}`,
    `当前势力面板=${faction}`,
    `本卷关键事件=${eventTitles}`,
    `最近地图操作=${lastInteraction.value || '无'}`
  ].join(' | ')
})

// ── 生命周期 ────────────────────────────────────────────────
onMounted(async () => {
  initMap()
  await loadAllData()
  selectTopRoutes()
  drawAll()
})

watch(currentVol, () => {
  if (focusedRouteName.value && !visibleRouteDefs.value.find((r) => r.character === focusedRouteName.value)) {
    focusedRouteName.value = ''
  }
  drawAll()
})

watch(activeNovelId, () => {
  focusedRouteName.value = ''
})

// ── 数据加载 ────────────────────────────────────────────────
async function loadAllData() {
  isLoading.value = true
  loadedCount.value = 0
  allData.value = new Array(TOTAL_VOLS.value).fill(null)

  const promises = []
  for (let i = 1; i <= TOTAL_VOLS.value; i++) {
    const volStr = String(i).padStart(2, '0')
    promises.push(
      fetch(`/novel-data/${activeNovelId.value}/data/vol${volStr}.json`)
        .then((r) => {
          if (!r.ok) throw new Error(`HTTP ${r.status}`)
          return r.json()
        })
        .then((data) => {
          allData.value[i - 1] = data
          loadedCount.value++
        })
        .catch((err) => {
          console.warn(`vol${volStr}.json 加载失败`, err)
          loadedCount.value++
        })
    )
  }

  await Promise.all(promises)
  isLoading.value = false
  allData.value = [...allData.value]
}

// ── 地图初始化 ──────────────────────────────────────────────
function initMap() {
  map = L.map('map', {
    center: [33.0, 113.0],
    zoom: 6,
    zoomControl: true,
    attributionControl: false
  })

  const TIANDITU_KEY = '5316253e2d2d21b707996f1f5748839d'
  L.tileLayer(
    `https://t{s}.tianditu.gov.cn/DataServer?T=vec_w&X={x}&Y={y}&L={z}&tk=${TIANDITU_KEY}`,
    { subdomains: '01234567', maxZoom: 18 }
  ).addTo(map)

  L.tileLayer(
    `https://t{s}.tianditu.gov.cn/DataServer?T=cva_w&X={x}&Y={y}&L={z}&tk=${TIANDITU_KEY}`,
    { subdomains: '01234567', maxZoom: 18 }
  ).addTo(map)

  layerGroups.territories = L.layerGroup().addTo(map)
  layerGroups.locations = L.layerGroup().addTo(map)
  layerGroups.routes = L.layerGroup().addTo(map)
  layerGroups.events = L.layerGroup().addTo(map)

  map.on('moveend zoomend', () => {
    const center = map.getCenter()
    mapViewState.value = {
      center: [center.lat, center.lng],
      zoom: map.getZoom()
    }
  })
}

// ── 全量重绘 ────────────────────────────────────────────────
function drawAll() {
  if (!map) return
  clearLayerGroups()
  drawTerritories()
  drawLocations()
  drawRoutes()
}

function clearLayerGroups() {
  Object.values(layerGroups).forEach((lg) => lg && lg.clearLayers())
}

// ── 绘制势力领地 ────────────────────────────────────────────
function drawTerritories() {
  const locs = mergedLocations.value
  mergedFactions.value.forEach((f) => {
    if (!f.territory || !f.territory.length) return

    f.territory.forEach((tid) => {
      const loc = locs.find((l) => l.id === tid)
      if (!loc) return

      const circle = L.circle([loc.lat, loc.lng], {
        radius: 50000,
        color: f.color,
        fillColor: f.color,
        fillOpacity: 0.1,
        weight: 2,
        opacity: 0.45,
        dashArray: '6 4'
      })
      layerGroups.territories.addLayer(circle)

      const label = L.marker([loc.lat + 0.35, loc.lng], {
        icon: L.divIcon({
          className: '',
          html: `<div style="color:${f.color};font-size:11px;font-weight:bold;text-shadow:0 0 3px #fff,0 0 6px #fff;white-space:nowrap;text-align:center">${f.name}</div>`,
          iconAnchor: [30, 8]
        })
      })
      layerGroups.territories.addLayer(label)
    })
  })
}

// ── 绘制地点标记 ────────────────────────────────────────────
function drawLocations() {
  const locs = mergedLocations.value
  const facs = mergedFactions.value

  locs.forEach((loc) => {
    let markerColor = '#919191'
    for (const f of facs) {
      if (f.territory && f.territory.includes(loc.id)) {
        markerColor = f.color
        break
      }
    }

    const size = loc.type === 'city' ? 10 : loc.type === 'landmark' ? 8 : 6
    const icon = L.divIcon({
      className: '',
      html: `<div style="width:${size}px;height:${size}px;background:${markerColor};border:2px solid #fff;border-radius:50%;box-shadow:0 0 8px ${markerColor}"></div>`,
      iconSize: [size, size],
      iconAnchor: [size / 2, size / 2]
    })

    const marker = L.marker([loc.lat, loc.lng], { icon })
      .bindPopup(`<b>${loc.name}</b><br>${loc.description || ''}`)

    marker.on('click', () => {
      lastInteraction.value = `查看地点 ${loc.name}`
    })

    layerGroups.locations.addLayer(marker)
  })
}

// ── 绘制人物路线 ────────────────────────────────────────────
function drawRoutes() {
  const locs = mergedLocations.value
  const focus = focusedRouteName.value
  const hasFocus = Boolean(focus)

  mergedRoutes.value.forEach((route) => {
    if (!selectedRoutes.value.includes(route.character)) return

    const points = route.route || []
    const resolved = points
      .map((p) => {
        const coords = getPointCoords(p, locs)
        return coords ? { ...coords, chapter: p.chapter, vol: p.vol, event: p.event } : null
      })
      .filter(Boolean)

    if (resolved.length < 2) return

    const isFocused = focus === route.character
    const lineOpacity = hasFocus ? (isFocused ? 0.95 : 0.15) : 0.8
    const lineWeight = hasFocus ? (isFocused ? 4 : 2) : 3

    for (let i = 0; i < resolved.length - 1; i++) {
      const p1 = resolved[i]
      const p2 = resolved[i + 1]
      const line = L.polyline([[p1.lat, p1.lng], [p2.lat, p2.lng]], {
        color: route.color,
        weight: lineWeight,
        opacity: lineOpacity
      })
      layerGroups.routes.addLayer(line)

      if (!hasFocus || isFocused) {
        const midLat = (p1.lat + p2.lat) / 2
        const midLng = (p1.lng + p2.lng) / 2
        const angle = Math.atan2(p2.lat - p1.lat, p2.lng - p1.lng) * (180 / Math.PI)
        const arrow = L.marker([midLat, midLng], {
          icon: L.divIcon({
            className: '',
            html: `<div style="color:${route.color};font-size:13px;font-weight:bold;transform:rotate(${angle}deg);text-shadow:0 0 3px rgba(0,0,0,0.8)">➤</div>`,
            iconAnchor: [6, 6]
          })
        })
        layerGroups.routes.addLayer(arrow)
      }
    }

    resolved.forEach((p, idx) => {
      const isStart = idx === 0
      const isEnd = idx === resolved.length - 1

      const dot = L.circleMarker([p.lat, p.lng], {
        radius: isFocused ? (isStart || isEnd ? 7 : 6) : (isStart || isEnd ? 6 : 4),
        color: route.color,
        fillColor: isEnd ? '#fff' : route.color,
        fillOpacity: hasFocus ? (isFocused ? 0.95 : 0.2) : 0.9,
        weight: isFocused ? 2 : 1
      }).bindPopup(`<b>${route.character}</b><br>卷${p.vol || '?'} 第${p.chapter || '?'}${currentNovel.value.volsLabel}<br>${p.event || ''}`)
      layerGroups.routes.addLayer(dot)

      if (isFocused && showRouteOrder.value) {
        const tag = isStart ? '起' : (isEnd ? '终' : `${idx + 1}`)
        const tagMarker = L.marker([p.lat, p.lng], {
          icon: L.divIcon({
            className: 'route-order-tag',
            html: `<div class="order-badge" style="border-color:${route.color};color:${route.color}">${tag}</div>`,
            iconSize: [24, 24],
            iconAnchor: [12, 12]
          })
        })
        layerGroups.routes.addLayer(tagMarker)
      }
    })
  })
}

// ── 工具函数 ────────────────────────────────────────────────
function getPointCoords(point, locs) {
  if (point.lat && point.lng) {
    return { lat: point.lat, lng: point.lng }
  }
  if (point.location) {
    const loc = locs.find((l) => l.id === point.location)
    if (loc) return { lat: loc.lat, lng: loc.lng }
  }
  return null
}

function toggleRoute(route) {
  const idx = selectedRoutes.value.indexOf(route.character)
  if (idx > -1) {
    selectedRoutes.value.splice(idx, 1)
    if (focusedRouteName.value === route.character) {
      focusedRouteName.value = ''
    }
  } else {
    selectedRoutes.value.push(route.character)
    if (!focusedRouteName.value) {
      focusedRouteName.value = route.character
    }
  }
  drawRoutesOnly()
}

function focusRoute(character) {
  focusedRouteName.value = focusedRouteName.value === character ? '' : character
  if (!selectedRoutes.value.includes(character)) {
    selectedRoutes.value.push(character)
  }
  drawRoutesOnly()
}

function clearRouteFocus() {
  focusedRouteName.value = ''
  drawRoutesOnly()
}

function clearAllRoutes() {
  selectedRoutes.value = []
  focusedRouteName.value = ''
  drawRoutesOnly()
}

function selectTopRoutes() {
  const top = mergedRoutes.value.slice(0, 4).map((r) => r.character)
  selectedRoutes.value = top
  focusedRouteName.value = top[0] || ''
  drawRoutesOnly()
}

function drawRoutesOnly() {
  if (!layerGroups.routes) return
  layerGroups.routes.clearLayers()
  drawRoutes()
}

function showFactionInfo(faction) {
  selectedFaction.value = faction
  if (Array.isArray(faction.territory) && faction.territory.length) {
    const loc = mergedLocations.value.find((l) => l.id === faction.territory[0])
    if (loc && map) {
      map.flyTo([loc.lat, loc.lng], 7, { duration: 1.1 })
    }
  }
}

function getTerritoryNames(territoryIds) {
  return territoryIds.map((tid) => {
    const loc = mergedLocations.value.find((l) => l.id === tid)
    return loc ? loc.name : tid
  })
}

function onVolSliderChange() {
  lastInteraction.value = `切换到 ${currentVolLabel.value}`
}

function flyToEvent(ev) {
  const locs = mergedLocations.value
  let coords = null
  if (ev.lat && ev.lng) {
    coords = [ev.lat, ev.lng]
  } else if (ev.location) {
    const loc = locs.find((l) => l.id === ev.location)
    if (loc) coords = [loc.lat, loc.lng]
  }
  if (coords && map) {
    lastInteraction.value = `查看事件 ${ev.event}`
    map.flyTo(coords, 8, { duration: 1.2 })
  }
}

function flyToRouteStep(step) {
  if (!map) return
  lastInteraction.value = `查看路线节点 ${step.locationName}`
  map.flyTo([step.lat, step.lng], 8, { duration: 1 })
}

function applySearchResult(result) {
  if (result.type === '地点' && result.payload) {
    const loc = result.payload
    if (map) map.flyTo([loc.lat, loc.lng], 8, { duration: 1 })
    lastInteraction.value = `搜索到地点 ${loc.name}`
    return
  }

  if (result.type === '势力' && result.payload) {
    showFactionInfo(result.payload)
    lastInteraction.value = `搜索到势力 ${result.payload.name}`
    return
  }

  if (result.type === '人物' && result.payload) {
    const route = result.payload
    if (!selectedRoutes.value.includes(route.character)) {
      selectedRoutes.value.push(route.character)
    }
    focusedRouteName.value = route.character
    drawRoutesOnly()
    lastInteraction.value = `搜索到人物 ${route.character}`
  }
}

// ── 智能问答逻辑 ────────────────────────────────────────────
function toggleChat() {
  isChatOpen.value = !isChatOpen.value
  if (isChatOpen.value) {
    scrollToChatBottom()
  }
}

async function sendChatMessage() {
  const text = chatInput.value.trim()
  if (!text || isChatLoading.value) return

  chatMessages.value.push({ role: 'user', content: text })
  chatInput.value = ''
  isChatLoading.value = true
  scrollToChatBottom()

  const question = includeMapContext.value
    ? `【地图上下文】\n${mapContextSummary.value}\n\n【用户问题】\n${text}`
    : text

  try {
    const res = await fetch('http://StoryMapAI.api.digitalmars.com.cn/api/chat', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ question })
    })

    if (!res.ok) throw new Error(`HTTP ${res.status}`)
    const data = await res.json()
    chatMessages.value.push({ role: 'assistant', content: data.answer || '助手暂时没有返回结果。' })
  } catch (err) {
    console.error('Chat error:', err)
    chatMessages.value.push({ role: 'assistant', content: '抱歉，连接知识库失败，请稍后再试。' })
  } finally {
    isChatLoading.value = false
    scrollToChatBottom()
  }
}

function scrollToChatBottom() {
  nextTick(() => {
    if (chatMessagesRef.value) {
      chatMessagesRef.value.scrollTop = chatMessagesRef.value.scrollHeight
    }
  })
}
</script>

<style scoped>
:root {
  font-family: 'Noto Serif SC', 'Source Han Serif SC', serif;
}

.map-container {
  position: relative;
  width: 100%;
  height: 100vh;
  overflow: hidden;
  color: #f0ead9;
}

.atmo-bg {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(circle at 12% 15%, rgba(220, 188, 121, 0.25), transparent 38%),
    radial-gradient(circle at 88% 72%, rgba(121, 173, 220, 0.15), transparent 36%),
    linear-gradient(120deg, rgba(18, 23, 34, 0.12), rgba(38, 25, 16, 0.1));
  z-index: 0;
  pointer-events: none;
}

#map {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: 0;
}

.novel-selector,
.legend,
.chapter-panel,
.info-panel,
.chat-panel,
.map-state-pill,
.chat-toggle-btn {
  position: absolute;
  z-index: 1000;
  background: linear-gradient(160deg, rgba(14, 19, 30, 0.92), rgba(36, 27, 21, 0.88));
  border: 1px solid rgba(201, 169, 110, 0.55);
  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.45);
  backdrop-filter: blur(4px);
}

.novel-selector {
  top: 16px;
  left: 16px;
  display: flex;
  gap: 8px;
  padding: 8px;
  border-radius: 10px;
}

.novel-btn {
  background: transparent;
  color: #d6d0bf;
  border: 1px solid rgba(201, 169, 110, 0.35);
  padding: 8px 14px;
  border-radius: 6px;
  cursor: pointer;
  transition: all 0.2s ease;
}

.novel-btn:hover {
  border-color: rgba(201, 169, 110, 0.75);
  transform: translateY(-1px);
}

.novel-btn.active {
  background: #c9a96e;
  color: #1b140d;
  border-color: #c9a96e;
  font-weight: 700;
}

.map-state-pill {
  top: 72px;
  left: 16px;
  padding: 8px 12px;
  border-radius: 999px;
  font-size: 12px;
}

.legend {
  bottom: 14px;
  left: 12px;
  width: 320px;
  max-height: calc(100vh - 160px);
  border-radius: 12px;
  padding: 12px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  overflow: hidden;
}

.legend-block {
  min-height: 0;
  display: flex;
  flex-direction: column;
}

.legend-factions {
  flex: 1 1 auto;
}

.legend-routes {
  flex: 1 1 auto;
}

.legend h3 {
  color: #e6c27a;
  margin: 0 0 8px;
  font-size: 14px;
  letter-spacing: 0.5px;
}

.legend-search {
  width: 100%;
  box-sizing: border-box;
  border: 1px solid rgba(201, 169, 110, 0.35);
  background: rgba(8, 11, 18, 0.55);
  color: #f6f2e7;
  border-radius: 6px;
  padding: 6px 8px;
  margin-bottom: 8px;
}

.legend-list {
  min-height: 0;
  overflow-y: auto;
}

.legend-item {
  display: flex;
  align-items: center;
  margin: 4px 0;
  cursor: pointer;
  gap: 8px;
  padding: 2px 2px;
}

.legend-item:hover {
  opacity: 0.8;
}

.legend-dot {
  width: 11px;
  height: 11px;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, 0.35);
  flex-shrink: 0;
}

.legend-line {
  width: 22px;
  height: 3px;
  border-radius: 2px;
  flex-shrink: 0;
}

.route-toolbar {
  display: flex;
  gap: 6px;
  margin-bottom: 6px;
  flex-wrap: wrap;
}

.mini-btn {
  border: 1px solid rgba(201, 169, 110, 0.55);
  background: rgba(13, 17, 28, 0.75);
  color: #e2d4b3;
  border-radius: 6px;
  padding: 4px 6px;
  font-size: 11px;
  cursor: pointer;
}

.mini-btn.active {
  background: rgba(201, 169, 110, 0.25);
}

.route-item {
  justify-content: space-between;
}

.focus-btn {
  border: 1px solid rgba(255, 255, 255, 0.3);
  background: transparent;
  color: #d8d2c4;
  border-radius: 999px;
  font-size: 11px;
  padding: 1px 8px;
  cursor: pointer;
}

.focus-btn.active {
  border-color: #e5c279;
  color: #e5c279;
}

.route-insight {
  border-top: 1px solid rgba(201, 169, 110, 0.3);
  padding-top: 8px;
}

.insight-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-size: 12px;
  color: #e8cb8f;
  margin-bottom: 6px;
}

.insight-list {
  max-height: 140px;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.insight-item {
  display: grid;
  grid-template-columns: 22px 1fr;
  grid-template-rows: auto auto;
  gap: 1px 6px;
  border: 1px solid rgba(201, 169, 110, 0.2);
  background: rgba(9, 12, 20, 0.55);
  border-radius: 6px;
  padding: 4px;
  cursor: pointer;
}

.insight-item:hover {
  border-color: rgba(201, 169, 110, 0.6);
}

.step-idx {
  grid-row: 1 / span 2;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  margin-top: 2px;
  border-radius: 50%;
  background: rgba(201, 169, 110, 0.2);
  color: #eecf91;
  font-size: 11px;
}

.step-main {
  font-size: 12px;
}

.step-sub {
  font-size: 11px;
  color: #a8a093;
}

.chapter-panel {
  right: 12px;
  bottom: 14px;
  width: 360px;
  max-height: calc(100vh - 140px);
  overflow-y: auto;
  border-radius: 12px;
  padding: 12px;
}

.chapter-panel h3 {
  margin: 0 0 8px;
  color: #e6c27a;
}

.vol-slider-wrap {
  position: relative;
  padding-bottom: 22px;
}

.chapter-slider {
  width: 100%;
  margin: 8px 0 0;
  accent-color: #c9a96e;
}

.vol-ticks {
  position: relative;
  height: 18px;
  margin-top: 2px;
}

.tick-label {
  position: absolute;
  transform: translateX(-50%);
  font-size: 11px;
  color: #8f8a7e;
  white-space: nowrap;
}

.vol-info {
  font-size: 12px;
  color: #c8bfad;
  margin-bottom: 8px;
}

.search-panel {
  border-top: 1px solid rgba(201, 169, 110, 0.2);
  border-bottom: 1px solid rgba(201, 169, 110, 0.2);
  padding: 8px 0;
}

.map-search-input {
  width: 100%;
  box-sizing: border-box;
  border: 1px solid rgba(201, 169, 110, 0.35);
  background: rgba(8, 11, 18, 0.55);
  color: #f6f2e7;
  border-radius: 6px;
  padding: 6px 8px;
}

.search-results {
  margin-top: 6px;
  max-height: 145px;
  overflow-y: auto;
}

.search-result-item {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 3px;
  border-radius: 5px;
  cursor: pointer;
}

.search-result-item:hover {
  background: rgba(201, 169, 110, 0.13);
}

.search-type {
  font-size: 10px;
  color: #e8cb8f;
  border: 1px solid rgba(201, 169, 110, 0.4);
  border-radius: 999px;
  padding: 1px 6px;
}

.search-name {
  font-size: 12px;
}

.events-section {
  padding-top: 8px;
}

.events-title {
  font-size: 12px;
  color: #e8cb8f;
  margin-bottom: 5px;
}

.event-item {
  display: flex;
  gap: 6px;
  margin: 3px 0;
  cursor: pointer;
  padding: 3px 4px;
  border-radius: 4px;
  font-size: 12px;
}

.event-item:hover {
  background: rgba(201, 169, 110, 0.13);
}

.event-ch {
  color: #d9ba7a;
  flex-shrink: 0;
}

.event-text {
  color: #d7d3cc;
}

.info-panel {
  top: 124px;
  right: 12px;
  width: 320px;
  max-height: 40vh;
  overflow-y: auto;
  border-radius: 10px;
  padding: 10px 12px;
}

.info-panel h3 {
  margin: 2px 0 6px;
}

.info-panel p {
  margin: 4px 0;
  font-size: 13px;
  line-height: 1.5;
}

.close-btn {
  position: absolute;
  top: 8px;
  right: 10px;
  cursor: pointer;
  font-size: 18px;
  color: #b7b1a4;
}

.close-btn:hover {
  color: #fff;
}

.loading-bar {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 4px;
  z-index: 2000;
  overflow: hidden;
  background: rgba(11, 11, 11, 0.8);
}

.loading-inner {
  height: 100%;
  background: linear-gradient(90deg, #8f6f3f, #e7ca8e);
  transition: width 0.3s ease;
}

.loading-text {
  position: absolute;
  top: 8px;
  left: 50%;
  transform: translateX(-50%);
  font-size: 12px;
  color: #f0d8a7;
  background: rgba(13, 17, 28, 0.85);
  padding: 3px 10px;
  border-radius: 4px;
}

.chat-toggle-btn {
  top: 16px;
  right: 16px;
  bottom: auto;
  border-radius: 8px;
  padding: 8px 7px;
  color: #f2e3be;
  cursor: pointer;
  font-size: 12px;
  writing-mode: vertical-rl;
  line-height: 1.15;
}

.chat-toggle-btn:hover {
  transform: translateY(-1px);
}

.chat-panel {
  top: 16px;
  right: 16px;
  width: 350px;
  height: 380px;
  border-radius: 12px;
  display: flex;
  flex-direction: column;
}

.chat-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px 12px;
  border-bottom: 1px solid rgba(201, 169, 110, 0.3);
}

.chat-header h3 {
  margin: 0;
  color: #f2d192;
  font-size: 15px;
}

.chat-context-panel {
  border-bottom: 1px solid rgba(201, 169, 110, 0.2);
  padding: 8px 10px;
  font-size: 12px;
  color: #d9d3c5;
}

.context-preview {
  margin-top: 6px;
  max-height: 70px;
  overflow-y: auto;
  background: rgba(9, 12, 18, 0.55);
  border: 1px solid rgba(201, 169, 110, 0.22);
  border-radius: 6px;
  padding: 6px;
  font-size: 11px;
  line-height: 1.4;
}

.chat-messages {
  flex: 1;
  overflow-y: auto;
  padding: 10px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.chat-bubble {
  max-width: 85%;
  padding: 8px 10px;
  border-radius: 8px;
  font-size: 13px;
  line-height: 1.45;
}

.chat-bubble.user {
  align-self: flex-end;
  background: #cfb276;
  color: #17120c;
}

.chat-bubble.assistant {
  align-self: flex-start;
  background: rgba(236, 229, 213, 0.08);
  border: 1px solid rgba(236, 229, 213, 0.15);
  color: #ece8de;
}

.chat-bubble.loading {
  color: #aaa08d;
  font-style: italic;
}

.chat-input-area {
  display: flex;
  gap: 8px;
  padding: 10px;
  border-top: 1px solid rgba(201, 169, 110, 0.2);
}

.chat-input-area input {
  flex: 1;
  background: rgba(0, 0, 0, 0.25);
  border: 1px solid rgba(201, 169, 110, 0.35);
  color: #fff;
  border-radius: 5px;
  padding: 7px 9px;
  outline: none;
}

.chat-input-area button {
  border: none;
  background: #cfb276;
  color: #1a140f;
  border-radius: 5px;
  padding: 0 12px;
  cursor: pointer;
  font-weight: 700;
}

.chat-input-area button:disabled {
  background: #5a5550;
  color: #9e978e;
  cursor: not-allowed;
}

.empty-tip {
  font-size: 12px;
  color: #9c9588;
  padding: 3px 2px;
}

:deep(.order-badge) {
  width: 22px;
  height: 22px;
  border-radius: 50%;
  border: 2px solid;
  background: rgba(255, 255, 255, 0.9);
  font-size: 11px;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 0 8px rgba(0, 0, 0, 0.35);
}

@media (max-width: 1024px) {
  .legend {
    width: 290px;
  }

  .chapter-panel {
    width: 320px;
  }
}

@media (max-width: 768px) {
  .map-state-pill {
    display: none;
  }

  .novel-selector {
    top: 10px;
    left: 10px;
    right: 74px;
    flex-wrap: wrap;
  }

  .legend {
    left: 8px;
    bottom: 8px;
    width: calc(100vw - 16px);
    max-height: 36vh;
  }

  .chapter-panel {
    right: 8px;
    bottom: calc(36vh + 16px);
    width: calc(100vw - 16px);
    max-height: 30vh;
  }

  .info-panel {
    top: 130px;
    right: 8px;
    width: calc(100vw - 16px);
  }

  .chat-panel {
    top: 64px;
    right: 8px;
    width: calc(100vw - 16px);
    height: 58vh;
  }

  .chat-toggle-btn {
    top: 10px;
    right: 10px;
  }
}
</style>
