<script setup lang="ts">
import type { GeoPoint, PoiCategory, PoiDatasetMeta, PoiSummary } from '@/domain/poi'
import type { PlanResponse } from '@/domain/travel'
import type { PoiMapMarker } from '@/services/map'
import MapFilterHeader from '@/components/map/MapFilterHeader.vue'
import PoiSummaryCard from '@/components/map/PoiSummaryCard.vue'
import PageState from '@/components/poi/PageState.vue'
import { getPoiRepository } from '@/data/poi'
import { isTrustworthyCoordinate } from '@/domain/poi'
import { buildMarkerIdMap, createPoiMarkers } from '@/services/map'
import { getSessionPlanningOrigin, loadPlan } from '@/services/travel-assistant'
import { useLocationStore, useMapStore } from '@/stores'

const MAP_ID = 'guangming-cultural-map'
const mapStore = useMapStore()
const locationStore = useLocationStore()

const pageState = ref<'loading' | 'ready' | 'data_error' | 'dataset_empty'>('loading')
const mapReady = ref(false)
const mapError = ref(false)
const categories = ref<PoiCategory[]>([])
const allPoiSummaries = ref<PoiSummary[]>([])
const datasetMeta = ref<PoiDatasetMeta | null>(null)
const activePlan = shallowRef<PlanResponse | null>(null)
const mapContext = shallowRef<ReturnType<typeof uni.createMapContext> | null>(null)
const markerIdToPoiId = shallowRef(new Map<number, string>())
const lastMarkerTapAt = ref(0)
const navigating = ref(false)
let includePointsRequestVersion = 0
let locationListenerAttached = false
let locationUpdatesStarted = false
let locationUpdatesStarting = false
let locationUpdatesRequested = false
let locationRequestVersion = 0

const activeCategoryCode = computed(() => mapStore.categoryCode)
const selectedPoiId = computed(() => mapStore.selectedPoiId)
const viewport = computed(() => mapStore.viewport)
const locationReady = computed(() => locationStore.status === 'ready' && Boolean(locationStore.snapshot))
const locationButtonText = computed(() => {
  if (locationStore.status === 'locating')
    return '定位中…'
  if (locationReady.value)
    return '回到我的位置'
  if (locationStore.status === 'denied')
    return '开启位置权限'
  return '定位到我'
})
const filteredPois = computed(() => {
  if (!activeCategoryCode.value)
    return allPoiSummaries.value
  return allPoiSummaries.value.filter(poi => poi.categoryCode === activeCategoryCode.value)
})
const selectedPoi = computed(() => {
  if (!selectedPoiId.value)
    return null
  return allPoiSummaries.value.find(poi => poi.id === selectedPoiId.value) ?? null
})
const markers = computed<PoiMapMarker[]>(() => {
  const baseMarkers = createPoiMarkers(
    filteredPois.value,
    categories.value,
    markerIdToPoiId.value,
    selectedPoiId.value,
  )
  if (!activePlan.value)
    return baseMarkers

  const routeOrder = new Map(activePlan.value.itinerary.items.map((item, index) => [item.placeId, index + 1]))
  return baseMarkers.map((marker) => {
    const poiId = markerIdToPoiId.value.get(marker.id)
    const order = poiId ? routeOrder.get(poiId) : undefined
    if (!order)
      return marker
    return {
      ...marker,
      callout: {
        ...marker.callout,
        content: selectedPoiId.value === poiId ? `${order} · ${marker.callout.content}` : `路线 ${order}`,
        color: '#ffffff',
        bgColor: '#167a5b',
        display: 'ALWAYS',
      },
      zIndex: Math.max(marker.zIndex, 8),
    }
  })
})
const mapPoints = computed<Array<{ latitude: number, longitude: number }>>(() => filteredPois.value.map(poi => ({
  latitude: poi.latitude,
  longitude: poi.longitude,
})))
const plannedPois = computed(() => {
  if (!activePlan.value)
    return []
  const repository = getPoiRepository()
  return activePlan.value.itinerary.items.flatMap((item) => {
    const poi = repository.getPoiById(item.placeId)
    return poi ? [poi] : []
  })
})
const plannedOrigin = computed(() => activePlan.value
  ? getSessionPlanningOrigin(activePlan.value.conversationId)
  : null)
const plannedRoutePoints = computed(() => [
  ...(activePlan.value?.itinerary.startsFromCurrentLocation && plannedOrigin.value
    ? [{ latitude: plannedOrigin.value.latitude, longitude: plannedOrigin.value.longitude }]
    : []),
  ...plannedPois.value.map(poi => ({ latitude: poi.latitude, longitude: poi.longitude })),
])
const plannedRoutePolyline = computed(() => plannedRoutePoints.value.length > 1
  ? [{
      points: plannedRoutePoints.value,
      color: '#167a5bcc',
      width: 6,
      dottedLine: false,
      arrowLine: true,
      borderColor: '#ffffffcc',
      borderWidth: 2,
    }]
  : [])

function loadActivePlan() {
  activePlan.value = mapStore.plannedRouteVisible ? loadPlan() : null
}

function includePlannedRoute() {
  const context = mapContext.value
  if (!context || !mapReady.value || plannedRoutePoints.value.length === 0)
    return
  nextTick(() => {
    context.includePoints({
      points: plannedRoutePoints.value,
      padding: [uni.upx2px(70), uni.upx2px(70), uni.upx2px(150), uni.upx2px(70)],
    })
  })
}

function loadDataset() {
  pageState.value = 'loading'
  mapError.value = false

  try {
    const repository = getPoiRepository()
    const nextMeta = repository.getDatasetMeta()
    const nextCategories = repository.getCategories()
    const nextPois = repository.getPoiSummaries()

    datasetMeta.value = nextMeta
    categories.value = nextCategories
    allPoiSummaries.value = nextPois
    markerIdToPoiId.value = buildMarkerIdMap(nextPois.map(poi => poi.id))

    const untouchedDefaultViewport = mapStore.viewport.longitude === 113.935
      && mapStore.viewport.latitude === 22.748
      && mapStore.viewport.scale === 11
    if (untouchedDefaultViewport)
      mapStore.resetViewport(nextMeta.defaultViewport)
    if (mapStore.categoryCode && !nextCategories.some(category => category.code === mapStore.categoryCode))
      mapStore.setCategory(null)
    if (mapStore.selectedPoiId && !nextPois.some(poi => poi.id === mapStore.selectedPoiId))
      mapStore.selectPoi(null)

    pageState.value = nextPois.length === 0 ? 'dataset_empty' : 'ready'
  }
  catch (error) {
    console.error('Failed to load POI dataset', error)
    categories.value = []
    allPoiSummaries.value = []
    markerIdToPoiId.value = new Map()
    pageState.value = 'data_error'
  }
}

function includeFilteredPoints() {
  const requestVersion = ++includePointsRequestVersion
  const context = mapContext.value
  const points = mapPoints.value.map(point => ({ ...point }))
  const bottomPadding = selectedPoi.value ? 360 : 80

  if (!mapReady.value || !context || points.length === 0)
    return

  nextTick(() => {
    if (requestVersion !== includePointsRequestVersion
      || !mapReady.value
      || mapContext.value !== context) {
      return
    }

    if (points.length === 1) {
      mapStore.updateViewport({
        ...points[0],
        scale: 15,
      })
      return
    }

    try {
      context.includePoints({
        points,
        padding: [
          uni.upx2px(48),
          uni.upx2px(48),
          uni.upx2px(bottomPadding),
          uni.upx2px(48),
        ],
        fail: (error) => {
          handleIncludePointsFailure(requestVersion, context, error)
        },
      })
    }
    catch (error) {
      handleIncludePointsFailure(requestVersion, context, error)
    }
  })
}

function handleIncludePointsFailure(
  requestVersion: number,
  context: ReturnType<typeof uni.createMapContext>,
  error: unknown,
) {
  if (requestVersion !== includePointsRequestVersion || mapContext.value !== context)
    return

  console.error('Failed to include POI points', error)
  includePointsRequestVersion += 1
  mapReady.value = false
  mapError.value = true
  mapContext.value = null
}

function selectCategory(categoryCode: string | null) {
  if (mapStore.categoryCode === categoryCode)
    return

  mapStore.setCategory(categoryCode)
  mapStore.hidePlannedRoute()
  activePlan.value = null
  if (selectedPoi.value && categoryCode && selectedPoi.value.categoryCode !== categoryCode)
    mapStore.selectPoi(null)

  includeFilteredPoints()
}

function selectMarker(event: { detail: { markerId: number } }) {
  // 跳转过程中原生地图还在，别再受理点选。
  if (navigating.value)
    return
  lastMarkerTapAt.value = Date.now()
  const poiId = markerIdToPoiId.value.get(Number(event.detail.markerId))
  if (!poiId)
    return

  // A marker tap is more recent than any pending category fit operation.
  includePointsRequestVersion += 1
  mapStore.selectPoi(poiId)
  const poi = allPoiSummaries.value.find(item => item.id === poiId)
  if (poi) {
    mapStore.updateViewport({
      longitude: poi.longitude,
      latitude: poi.latitude,
      scale: Math.max(viewport.value.scale, 13),
    })
  }
}

function clearSelection() {
  if (Date.now() - lastMarkerTapAt.value < 250)
    return
  mapStore.selectPoi(null)
}

/**
 * 尽量直接从 regionchange 事件里取中心点。
 * 微信把中心点放在 `detail.centerLocation`，老基础库才退化到 `detail.longitude`。
 * 两者都拿不到时才回落到异步的 getCenterLocation。
 */
function readEventCenter(detail: Record<string, unknown>): GeoPoint | null {
  const center = detail.centerLocation
  if (center && typeof center === 'object') {
    const { longitude, latitude } = center as Record<string, unknown>
    if (isTrustworthyCoordinate(Number(longitude), Number(latitude)))
      return { longitude: Number(longitude), latitude: Number(latitude) }
  }

  const longitude = Number(detail.longitude)
  const latitude = Number(detail.latitude)
  if (isTrustworthyCoordinate(longitude, latitude))
    return { longitude, latitude }

  return null
}

function updateViewport(event: { type: string, detail: Record<string, unknown> }) {
  const changeType = String(event.detail.type ?? event.type ?? '')
  if (changeType !== 'end')
    return
  // 地图上下文还没建立、或正在跳转详情页时，地图回传的中心点不可信，直接丢弃。
  if (!mapReady.value || navigating.value)
    return

  const eventScale = Number(event.detail.scale)
  const fallbackScale = Number.isFinite(eventScale) ? eventScale : viewport.value.scale

  const center = readEventCenter(event.detail)
  if (center) {
    updateViewportWithScale(center.longitude, center.latitude, fallbackScale)
    return
  }

  mapContext.value?.getCenterLocation({
    success: (location: { longitude: number, latitude: number }) => {
      // 地图 SDK 还没拿到有效中心点时会回传 (0, 0)，写进 viewport 会把地图甩到几内亚湾。
      if (!isTrustworthyCoordinate(location.longitude, location.latitude))
        return
      updateViewportWithScale(location.longitude, location.latitude, fallbackScale)
    },
  })
}

function updateViewportWithScale(longitude: number, latitude: number, fallbackScale: number) {
  const context = mapContext.value as (ReturnType<typeof uni.createMapContext> & {
    getScale?: (options: {
      success: (result: { scale: number }) => void
      fail: () => void
    }) => void
  }) | null

  if (!context?.getScale) {
    mapStore.updateViewport({ longitude, latitude, scale: fallbackScale })
    return
  }

  context.getScale({
    success: (result) => {
      const scale = Number(result.scale)
      mapStore.updateViewport({
        longitude,
        latitude,
        scale: Number.isFinite(scale) ? scale : fallbackScale,
      })
    },
    fail: () => {
      mapStore.updateViewport({ longitude, latitude, scale: fallbackScale })
    },
  })
}

function resetToAll() {
  mapStore.setCategory(null)
  mapStore.selectPoi(null)
  mapStore.hidePlannedRoute()
  activePlan.value = null
  const target = datasetMeta.value?.defaultViewport
  if (target)
    mapStore.resetViewport(target)
  includeFilteredPoints()
}

function isPermissionDenied(error: unknown): boolean {
  if (!error || typeof error !== 'object')
    return false
  const message = String((error as { errMsg?: unknown }).errMsg ?? '')
  return /auth deny|auth denied|authorize:fail|permission/i.test(message)
}

function locationChangeListener(result: UniNamespace.OnLocationChangeCallbackResult) {
  const longitude = Number(result.longitude)
  const latitude = Number(result.latitude)
  const accuracy = Number(result.accuracy ?? result.horizontalAccuracy)
  locationStore.updateLocation(longitude, latitude, accuracy)
}

function attachLocationListener() {
  if (locationListenerAttached)
    return
  uni.onLocationChange(locationChangeListener)
  locationListenerAttached = true
}

function detachLocationListener() {
  if (!locationListenerAttached)
    return
  uni.offLocationChange(locationChangeListener)
  locationListenerAttached = false
}

function startForegroundLocationUpdates() {
  if (locationUpdatesStarted || locationUpdatesStarting || !locationUpdatesRequested)
    return

  locationUpdatesStarting = true
  attachLocationListener()
  uni.startLocationUpdate({
    type: 'gcj02',
    success: () => {
      locationUpdatesStarting = false
      if (!locationUpdatesRequested) {
        uni.stopLocationUpdate()
        detachLocationListener()
        return
      }
      locationUpdatesStarted = true
    },
    fail: (error) => {
      locationUpdatesStarting = false
      detachLocationListener()
      console.error('Failed to start foreground location updates', error)
    },
  })
}

function stopForegroundLocationUpdates() {
  locationUpdatesRequested = false
  locationRequestVersion += 1
  detachLocationListener()
  if (locationUpdatesStarted) {
    uni.stopLocationUpdate()
    locationUpdatesStarted = false
  }
}

function centerOnUserLocation() {
  const snapshot = locationStore.snapshot
  if (!snapshot)
    return
  includePointsRequestVersion += 1
  mapStore.selectPoi(null)
  mapStore.updateViewport({
    longitude: snapshot.longitude,
    latitude: snapshot.latitude,
    scale: 15,
  })
}

function requestUserLocation() {
  if (locationStore.status === 'locating')
    return
  if (locationStore.status === 'denied') {
    uni.openSetting({
      success: (settings) => {
        if (settings.authSetting['scope.userLocation']) {
          locationUpdatesRequested = true
          locationStore.startLocating()
          requestCurrentLocation()
        }
      },
    })
    return
  }
  if (locationReady.value) {
    locationUpdatesRequested = true
    centerOnUserLocation()
    startForegroundLocationUpdates()
    return
  }

  locationUpdatesRequested = true
  locationStore.startLocating()
  requestCurrentLocation()
}

function requestCurrentLocation() {
  const requestVersion = ++locationRequestVersion
  uni.getLocation({
    type: 'gcj02',
    isHighAccuracy: true,
    highAccuracyExpireTime: 5000,
    success: (result) => {
      if (requestVersion !== locationRequestVersion)
        return
      const updated = locationStore.updateLocation(
        Number(result.longitude),
        Number(result.latitude),
        Number(result.accuracy ?? result.horizontalAccuracy),
      )
      if (!updated) {
        locationStore.failLocation('error', '定位结果无效，请稍后重试')
        return
      }
      centerOnUserLocation()
      startForegroundLocationUpdates()
    },
    fail: (error) => {
      if (requestVersion !== locationRequestVersion)
        return
      if (isPermissionDenied(error)) {
        locationStore.failLocation('denied', '位置权限未开启')
        uni.showModal({
          title: '需要位置权限',
          content: '开启位置权限后，可在地图显示实时方位，并按当前位置优化推荐路线。',
          confirmText: '去设置',
          success: result => result.confirm && requestUserLocation(),
        })
        return
      }
      locationStore.failLocation('unavailable', '暂时无法获取位置')
      uni.showToast({ title: '定位失败，请检查系统定位服务', icon: 'none' })
    },
  })
}

function openDetail(poiId: string) {
  if (navigating.value)
    return
  navigating.value = true
  uni.navigateTo({
    url: `/pages-poi/detail?poiId=${encodeURIComponent(poiId)}`,
    complete: () => {
      setTimeout(() => {
        navigating.value = false
      }, 500)
    },
  })
}

function openCheckInRecords() {
  if (navigating.value)
    return
  navigating.value = true
  uni.navigateTo({
    url: '/pages/check-in/records',
    complete: () => {
      setTimeout(() => {
        navigating.value = false
      }, 500)
    },
  })
}

function initializeMap() {
  includePointsRequestVersion += 1
  mapReady.value = false
  mapError.value = false

  nextTick(() => {
    try {
      mapContext.value = uni.createMapContext(MAP_ID)
      mapReady.value = true
      if (activePlan.value)
        includePlannedRoute()
      else
        includeFilteredPoints()
    }
    catch (error) {
      console.error('Failed to initialize map', error)
      mapReady.value = false
      mapError.value = true
    }
  })
}

function handleMapError(event: unknown) {
  console.error('Map component error', event)
  includePointsRequestVersion += 1
  mapReady.value = false
  mapError.value = true
  mapContext.value = null
}

onLoad(() => {
  loadDataset()
})

onReady(() => {
  initializeMap()
})

onShow(() => {
  // 从详情页返回后立刻恢复地图交互，不等 navigateTo 的 500ms 兜底定时器。
  navigating.value = false
  loadActivePlan()
  if (activePlan.value)
    includePlannedRoute()
  if (locationReady.value && locationUpdatesRequested)
    startForegroundLocationUpdates()
})

onHide(stopForegroundLocationUpdates)
onUnload(stopForegroundLocationUpdates)
</script>

<template>
  <view class="map-page">
    <MapFilterHeader
      :categories="categories"
      :active-category-code="activeCategoryCode"
      :result-count="filteredPois.length"
      :coverage-label="datasetMeta?.coverageLabel ?? 'POC 示例数据'"
      :loading="pageState === 'loading'"
      @check-ins="openCheckInRecords"
      @select="selectCategory"
    />

    <view v-if="pageState === 'loading'" class="map-page__state">
      <PageState state="loading" title="正在加载光明区文旅资源…" />
    </view>

    <view v-else-if="pageState === 'data_error'" class="map-page__state">
      <PageState
        state="error"
        title="点位数据加载失败"
        description="暂时无法读取点位信息，请稍后重试"
        action-label="重新加载"
        @action="loadDataset"
      />
    </view>

    <view v-else-if="pageState === 'dataset_empty'" class="map-page__state">
      <PageState state="empty" title="暂无可展示点位" description="本期审核数据中暂时没有已发布点位" />
    </view>

    <template v-else>
      <view v-if="filteredPois.length === 0" class="map-page__empty-strip">
        <text>当前分类暂无点位</text>
        <button @click="resetToAll">查看全部</button>
      </view>

      <view class="map-page__viewport">
        <view v-if="mapError" class="map-page__map-error">
          <PageState
            state="error"
            title="地图服务暂不可用"
            description="请检查网络后重试地图；位置功能仅在您主动点击定位按钮后启用"
            action-label="重试地图"
            @action="initializeMap"
          />
        </view>
        <map
          v-else
          :id="MAP_ID"
          class="map-page__map"
          :longitude="viewport.longitude"
          :latitude="viewport.latitude"
          :scale="viewport.scale"
          :markers="markers"
          :polyline="plannedRoutePolyline"
          :show-location="locationReady"
          :enable-rotate="false"
          :enable-overlooking="false"
          :show-compass="false"
          :enable-scroll="!navigating"
          :enable-zoom="!navigating"
          @markertap="selectMarker"
          @tap="clearSelection"
          @regionchange="updateViewport"
          @error="handleMapError"
        >
          <cover-view class="map-page__reset" @tap="resetToAll">
            回到全域
          </cover-view>
          <cover-view
            class="map-page__locate"
            :class="{ 'map-page__locate--active': locationReady }"
            @tap="requestUserLocation"
          >
            {{ locationButtonText }}
          </cover-view>
          <cover-view v-if="activePlan" class="map-page__route" @tap="includePlannedRoute">
            完整路线 · {{ plannedPois.length }} 站
          </cover-view>
        </map>
      </view>

      <PoiSummaryCard
        v-if="selectedPoi"
        :poi="selectedPoi"
        @close="mapStore.selectPoi(null)"
        @detail="openDetail"
      />
    </template>
  </view>
</template>

<route lang="yaml" type="home">
layout: map
style:
  navigationBarTitleText: 光明文旅地图
  navigationBarBackgroundColor: '#ffffff'
  navigationBarTextStyle: black
  disableScroll: true
</route>

<style scoped lang="scss">
.map-page {
  display: flex;
  flex-direction: column;
  width: 100%;
  height: 100vh;
  overflow: hidden;
  background: #f5f7f6;
}

.map-page__state,
.map-page__map-error {
  display: flex;
  flex: 1;
  align-items: stretch;
  justify-content: stretch;
  min-height: 0;
}

.map-page__empty-strip {
  display: flex;
  flex: none;
  align-items: center;
  justify-content: space-between;
  min-height: 72rpx;
  padding: 0 32rpx;
  font-size: 24rpx;
  color: #934b00;
  background: #fff3e0;
}

.map-page__empty-strip button {
  width: auto;
  padding: 0 24rpx;
  margin: 0;
  font-size: 24rpx;
  font-weight: 600;
  line-height: 72rpx;
  color: #0e6247;
  background: transparent;
  border: 0;
}

.map-page__empty-strip button::after {
  border: 0;
}

.map-page__viewport {
  position: relative;
  flex: 1;
  min-height: 480rpx;
}

.map-page__map {
  width: 100%;
  height: 100%;
}

.map-page__reset {
  position: absolute;
  top: 24rpx;
  right: 24rpx;
  padding: 14rpx 22rpx;
  font-size: 24rpx;
  font-weight: 600;
  line-height: 36rpx;
  color: #0e6247;
  background: #fff;
  border-radius: 28rpx;
  box-shadow: 0 8rpx 24rpx rgb(24 32 29 / 16%);
}

.map-page__locate {
  position: absolute;
  right: 24rpx;
  bottom: 28rpx;
  padding: 14rpx 22rpx;
  font-size: 24rpx;
  font-weight: 600;
  line-height: 36rpx;
  color: #34473d;
  background: #fff;
  border-radius: 28rpx;
  box-shadow: 0 8rpx 24rpx rgb(24 32 29 / 16%);
}

.map-page__locate--active {
  color: #fff;
  background: #167a5b;
}

.map-page__route {
  position: absolute;
  bottom: 28rpx;
  left: 24rpx;
  padding: 14rpx 22rpx;
  font-size: 24rpx;
  font-weight: 700;
  line-height: 36rpx;
  color: #fff;
  background: #167a5b;
  border-radius: 28rpx;
  box-shadow: 0 8rpx 24rpx rgb(24 32 29 / 16%);
}
</style>
