import type {
  Interest,
  ItineraryItem,
  ItinerarySource,
  LocalPlanAdjustment,
  Pace,
  PlanningOrigin,
  PlanningRecommendation,
  PlanningRequest,
  PlanResponse,
  Theme,
  TravelPreferences,
} from './types'
import type { PoiRepository, PoiResolved } from '@/domain/poi'
import { classifyDurationFit } from './duration-gap'
import { normalizeTravelPreferences, TravelValidationError } from './preferences'

const THEME_TAG_WEIGHTS: Record<Theme, Readonly<Record<string, number>>> = {
  亲子: { family_friendly: 34, science_learning: 14, indoor_venue: 6 },
  情侣: { photo_spot: 28, nature_view: 18, pastoral_scenery: 12 },
  朋友: { outdoor_leisure: 18, photo_spot: 16, culture_experience: 10 },
  银发: { walking: 18, city_park: 14, culture_experience: 12, indoor_venue: 8 },
  研学: { science_learning: 34, culture_experience: 20, nature_view: 8 },
}

const INTEREST_TAG_WEIGHTS: Record<Interest, Readonly<Record<string, number>>> = {
  自然风光: { nature_view: 36, pastoral_scenery: 28, city_park: 18, outdoor_leisure: 12 },
  文化场馆: { culture_experience: 36, indoor_venue: 22 },
  生态科普: { science_learning: 38, nature_view: 16, pastoral_scenery: 12 },
  美食: {},
  摄影: { photo_spot: 42, nature_view: 12, pastoral_scenery: 12 },
}

const CATEGORY_ACTIVITY: Readonly<Record<string, string>> = {
  cultural_venue: '参观场馆并体验公共文化空间',
  urban_park: '漫步公园，按现场开放区域游览',
  pastoral_leisure: '体验田园景观与休闲活动',
  heritage_landmark: '探访人文地标并了解本地文化',
  outdoor_greenway: '沿绿道轻量步行并欣赏沿途景观',
}

const CATEGORY_BASE_MINUTES: Readonly<Record<string, number>> = {
  cultural_venue: 75,
  urban_park: 65,
  pastoral_leisure: 90,
  heritage_landmark: 55,
  outdoor_greenway: 85,
}

const PACE_FACTOR: Record<Pace, number> = {
  relaxed: 1.15,
  moderate: 1,
  compact: 0.8,
}

function includesNegativeMention(text: string, term: string): boolean {
  const escaped = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
  return new RegExp(`(?:不要|不想|避免|无需|别|不去|不安排).{0,3}${escaped}`).test(text)
}

function includesPositiveMention(text: string, terms: readonly string[]): boolean {
  return terms.some(term => text.includes(term) && !includesNegativeMention(text, term))
}

function preferenceSignals(preferences: TravelPreferences): {
  indoorOnly: boolean
  outdoorOnly: boolean
  avoidClimbing: boolean
} {
  const text = preferences.extraRequirements.replace(/\s+/g, '')
  const rejectsIndoor = includesNegativeMention(text, '室内')
  const rejectsOutdoor = includesNegativeMention(text, '户外')
  return {
    indoorOnly: rejectsOutdoor || includesPositiveMention(text, ['室内', '雨天', '避雨']),
    outdoorOnly: rejectsIndoor || includesPositiveMention(text, ['户外', '室外']),
    avoidClimbing: ['不爬山', '不要爬山', '不登山', '体力不好', '婴儿车'].some(term => text.includes(term)),
  }
}

function tagScore(poi: PoiResolved, weights: Readonly<Record<string, number>>): number {
  return poi.tagCodes.reduce((total, tagCode) => total + (weights[tagCode] ?? 0), 0)
}

function scorePoi(poi: PoiResolved, preferences: TravelPreferences): number {
  let score = poi.recommendationIndex * 10
  preferences.themes.forEach((theme) => {
    score += tagScore(poi, THEME_TAG_WEIGHTS[theme])
  })
  preferences.interests.forEach((interest) => {
    score += tagScore(poi, INTEREST_TAG_WEIGHTS[interest])
  })

  if (preferences.interests.includes('文化场馆') && poi.categoryCode === 'cultural_venue')
    score += 30
  if (preferences.interests.includes('自然风光') && ['urban_park', 'pastoral_leisure', 'outdoor_greenway'].includes(poi.categoryCode))
    score += 16
  if (preferences.children > 0 && poi.tagCodes.includes('family_friendly'))
    score += 24
  if (preferences.themes.includes('银发') && poi.categoryCode === 'outdoor_greenway')
    score -= 18

  return score
}

function isHighIntensity(poi: PoiResolved): boolean {
  return poi.categoryCode === 'outdoor_greenway' || /大顶岭|森林/.test(poi.name)
}

function getCandidates(preferences: TravelPreferences, repository: PoiRepository): PoiResolved[] {
  const signals = preferenceSignals(preferences)
  const allPois = repository.getPublishedPois()
  const candidates = allPois.filter((poi) => {
    if (signals.indoorOnly && !poi.tagCodes.includes('indoor_venue'))
      return false
    if (signals.outdoorOnly && poi.tagCodes.includes('indoor_venue'))
      return false
    if (signals.avoidClimbing && isHighIntensity(poi))
      return false
    return true
  })
  return candidates
}

function roundToFive(value: number): number {
  return Math.max(5, Math.round(value / 5) * 5)
}

function activityMinutes(poi: PoiResolved, pace: Pace): number {
  const base = CATEGORY_BASE_MINUTES[poi.categoryCode] ?? 60
  return roundToFive(base * PACE_FACTOR[pace])
}

function distanceKm(from: { longitude: number, latitude: number }, to: { longitude: number, latitude: number }): number {
  const toRadians = (degrees: number) => degrees * Math.PI / 180
  const deltaLatitude = toRadians(to.latitude - from.latitude)
  const deltaLongitude = toRadians(to.longitude - from.longitude)
  const fromLatitude = toRadians(from.latitude)
  const toLatitude = toRadians(to.latitude)
  const a = Math.sin(deltaLatitude / 2) ** 2
    + Math.cos(fromLatitude) * Math.cos(toLatitude) * Math.sin(deltaLongitude / 2) ** 2
  return 6371 * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
}

function normalizePlanningOrigin(origin?: PlanningOrigin): PlanningOrigin | null {
  if (!origin
    || origin.coordinateSystem !== 'GCJ02'
    || !Number.isFinite(origin.longitude)
    || !Number.isFinite(origin.latitude)
    || origin.longitude < -180
    || origin.longitude > 180
    || origin.latitude < -90
    || origin.latitude > 90) {
    return null
  }
  return { ...origin }
}

function orderPoisFromOrigin(pois: PoiResolved[], origin: PlanningOrigin | null): PoiResolved[] {
  if (!origin || pois.length < 2)
    return pois

  const remaining = [...pois]
  const ordered: PoiResolved[] = []
  let cursor: { longitude: number, latitude: number } = origin
  while (remaining.length > 0) {
    remaining.sort((left, right) => distanceKm(cursor, left) - distanceKm(cursor, right)
      || right.recommendationIndex - left.recommendationIndex
      || left.id.localeCompare(right.id, 'en'))
    const next = remaining.shift()
    if (!next)
      break
    ordered.push(next)
    cursor = next
  }
  return ordered
}

function orderCustomPois(
  pois: PoiResolved[],
  preferences: TravelPreferences,
  origin: PlanningOrigin | null,
): PoiResolved[] {
  if (pois.length < 2)
    return [...pois]

  const remaining = [...pois]
  const first = origin
    ? orderPoisFromOrigin(remaining, origin)[0]
    : [...remaining].sort((left, right) => scorePoi(right, preferences) - scorePoi(left, preferences)
      || left.id.localeCompare(right.id, 'en'))[0]
  const ordered = [first]
  remaining.splice(remaining.findIndex(poi => poi.id === first.id), 1)
  let cursor = first
  while (remaining.length > 0) {
    remaining.sort((left, right) => distanceKm(cursor, left) - distanceKm(cursor, right)
      || scorePoi(right, preferences) - scorePoi(left, preferences)
      || left.id.localeCompare(right.id, 'en'))
    const next = remaining.shift()!
    ordered.push(next)
    cursor = next
  }
  return ordered
}

function transferMinutes(from: { longitude: number, latitude: number }, to: PoiResolved, preferences: TravelPreferences): number {
  const distance = distanceKm(from, to)
  const minutes = preferences.transport === 'walking'
    ? distance / 4.2 * 60
    : preferences.transport === 'driving'
      ? distance / 28 * 60 + 5
      : distance / 18 * 60 + 8
  return Math.min(90, roundToFive(minutes))
}

function rankedCandidates(
  candidates: PoiResolved[],
  preferences: TravelPreferences,
  selected: PoiResolved[],
  cursor: PlanningOrigin | PoiResolved | null,
): PoiResolved[] {
  return [...candidates].sort((left, right) => {
    const adjustedScore = (poi: PoiResolved) => {
      const categoryCount = selected.filter(item => item.categoryCode === poi.categoryCode).length
      const distancePenalty = cursor ? Math.min(36, distanceKm(cursor, poi) * 2) : 0
      return scorePoi(poi, preferences) - categoryCount * 14 - distancePenalty
    }
    return adjustedScore(right) - adjustedScore(left) || left.id.localeCompare(right.id, 'en')
  })
}

function canCompleteMinimumRoute(
  candidates: readonly PoiResolved[],
  preferences: TravelPreferences,
  cursor: PlanningOrigin | PoiResolved | null,
  remainingMinutes: number,
  stopsNeeded: number,
): boolean {
  if (stopsNeeded <= 0)
    return true

  return candidates.some((poi, index) => {
    const requiredMinutes = (cursor ? transferMinutes(cursor, poi, preferences) : 0)
      + activityMinutes(poi, preferences.pace)
    if (requiredMinutes > remainingMinutes)
      return false
    return canCompleteMinimumRoute(
      [...candidates.slice(0, index), ...candidates.slice(index + 1)],
      preferences,
      poi,
      remainingMinutes - requiredMinutes,
      stopsNeeded - 1,
    )
  })
}

function selectQuickPois(
  preferences: TravelPreferences,
  repository: PoiRepository,
  requestedMinutes: number,
  origin: PlanningOrigin | null,
): PoiResolved[] {
  const remaining = getCandidates(preferences, repository)
  const selected: PoiResolved[] = []
  let usedMinutes = 0
  let cursor: PlanningOrigin | PoiResolved | null = origin
  const minimumTargetCount = Math.min(remaining.length, requestedMinutes >= 240 ? 3 : 1)

  while (remaining.length > 0 && selected.length < 8) {
    const ranked = rankedCandidates(remaining, preferences, selected, cursor)
    const fitting = ranked.filter((poi) => {
      const transfer = cursor ? transferMinutes(cursor, poi, preferences) : 0
      return usedMinutes + transfer + activityMinutes(poi, preferences.pace) <= requestedMinutes
    })
    const next = fitting.find((poi) => {
      const transfer = cursor ? transferMinutes(cursor, poi, preferences) : 0
      const requiredMinutes = transfer + activityMinutes(poi, preferences.pace)
      const stopsNeeded = minimumTargetCount - selected.length - 1
      if (stopsNeeded <= 0)
        return true
      return canCompleteMinimumRoute(
        remaining.filter(candidate => candidate.id !== poi.id),
        preferences,
        poi,
        requestedMinutes - usedMinutes - requiredMinutes,
        stopsNeeded,
      )
    }) ?? fitting[0]
    if (!next)
      break
    const transfer = cursor ? transferMinutes(cursor, next, preferences) : 0
    usedMinutes += transfer + activityMinutes(next, preferences.pace)
    selected.push(next)
    remaining.splice(remaining.findIndex(poi => poi.id === next.id), 1)
    cursor = next
  }

  if (selected.length === 0 && remaining.length > 0) {
    throw new TravelValidationError(
      '当前起点、交通方式与所选时长无法容纳首个点位',
      'quick_route_unreachable',
    )
  }

  return selected
}

function selectCustomPois(request: PlanningRequest, repository: PoiRepository): PoiResolved[] {
  const selected = request.selectedPoiIds.map(poiId => repository.getPoiById(poiId))
  if (selected.some(poi => !poi))
    throw new TravelValidationError('自选点位包含当前数据中不存在的 POI')
  return orderCustomPois(
    selected as PoiResolved[],
    request.preferences,
    normalizePlanningOrigin(request.origin),
  )
}

function normalizeRecommendations(
  recommendations: readonly PlanningRecommendation[],
  request: PlanningRequest,
  repository: PoiRepository,
): PlanningRecommendation[] {
  if (!Array.isArray(recommendations) || recommendations.length < 1 || recommendations.length > 8)
    throw new TravelValidationError('AI 推荐点位数量必须为 1 至 8 个')

  const seenIds = new Set<string>()
  const normalized = recommendations.map((recommendation) => {
    const poiId = typeof recommendation?.poiId === 'string' ? recommendation.poiId.trim() : ''
    const reason = typeof recommendation?.reason === 'string' ? recommendation.reason.trim() : ''
    if (!poiId || !repository.getPoiById(poiId))
      throw new TravelValidationError(`AI 推荐包含无效点位 ID：${poiId || '空值'}`)
    if (seenIds.has(poiId))
      throw new TravelValidationError(`AI 推荐包含重复点位：${poiId}`)
    if (!reason || reason.length > 300)
      throw new TravelValidationError('AI 推荐理由格式无效')
    seenIds.add(poiId)
    return { poiId, reason }
  })

  if (request.mode === 'custom') {
    const selectedIds = new Set(request.selectedPoiIds)
    if (normalized.length !== selectedIds.size || normalized.some(item => !selectedIds.has(item.poiId)))
      throw new TravelValidationError('AI 自选规划未完整保留用户选择的点位')
  }
  return normalized
}

function selectRecommendedPois(
  recommendations: readonly PlanningRecommendation[],
  request: PlanningRequest,
  repository: PoiRepository,
  origin: PlanningOrigin | null,
): PoiResolved[] {
  const recommendedPois = recommendations.map(item => repository.getPoiById(item.poiId)!)
  if (request.mode === 'custom') {
    // The AI suggests an order; local nearest-neighbour ordering owns precise location use.
    return origin
      ? orderCustomPois(recommendedPois, request.preferences, origin)
      : recommendedPois
  }

  const remaining = origin
    ? orderPoisFromOrigin(recommendedPois, origin)
    : [...recommendedPois]
  const selected: PoiResolved[] = []
  let usedMinutes = 0
  let cursor: PlanningOrigin | PoiResolved | null = origin
  while (remaining.length > 0 && selected.length < 8) {
    const next = remaining.shift()!
    const transfer = cursor ? transferMinutes(cursor, next, request.preferences) : 0
    const visit = activityMinutes(next, request.preferences.pace)
    if (usedMinutes + transfer + visit > request.durationMinutes)
      continue
    selected.push(next)
    usedMinutes += transfer + visit
    cursor = next
  }
  if (selected.length === 0 && recommendedPois.length > 0)
    selected.push(remaining[0] ?? recommendedPois[0])
  return selected
}

function formatClock(totalMinutes: number): string {
  const hours = Math.floor(totalMinutes / 60)
  const minutes = totalMinutes % 60
  return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`
}

function buildReason(poi: PoiResolved, preferences: TravelPreferences): string {
  const matchingTags = poi.tags
    .filter(tag => preferences.themes.some(theme => THEME_TAG_WEIGHTS[theme][tag.code])
      || preferences.interests.some(interest => INTEREST_TAG_WEIGHTS[interest][tag.code]))
    .slice(0, 2)
    .map(tag => tag.name)
  if (matchingTags.length > 0)
    return `契合你的${matchingTags.join('、')}偏好，推荐指数 ${poi.recommendationIndex}/5。`
  return `结合当前行程节奏与点位推荐指数 ${poi.recommendationIndex}/5 选入。`
}

function buildTips(poi: PoiResolved): string[] {
  const tips = [poi.openingHoursText]
  if (!poi.tagCodes.includes('indoor_venue'))
    tips.push('户外体验受天气和现场开放情况影响，请提前确认。')
  if (poi.address)
    tips.push(`参考地址：${poi.address}`)
  return tips.slice(0, 3)
}

function buildItems(
  pois: PoiResolved[],
  preferences: TravelPreferences,
  origin: PlanningOrigin | null,
  recommendationReasons: ReadonlyMap<string, string> = new Map(),
): ItineraryItem[] {
  let cursor = 9 * 60
  return pois.map((poi, index) => {
    const transfer = index === 0
      ? origin ? transferMinutes(origin, poi, preferences) : null
      : transferMinutes(pois[index - 1], poi, preferences)
    if (transfer !== null)
      cursor += transfer
    const startTime = formatClock(cursor)
    cursor += activityMinutes(poi, preferences.pace)
    return {
      startTime,
      endTime: formatClock(cursor),
      placeId: poi.id,
      placeName: poi.name,
      activity: CATEGORY_ACTIVITY[poi.categoryCode] ?? '按现场开放情况体验点位内容',
      reason: index === 0 && origin
        ? `优先安排距你当前位置较近的首站；${recommendationReasons.get(poi.id) ?? buildReason(poi, preferences)}`
        : recommendationReasons.get(poi.id) ?? buildReason(poi, preferences),
      transferFromPreviousMinutes: transfer,
      tips: buildTips(poi),
    }
  })
}

function isPlanningRequest(value: TravelPreferences | PlanningRequest): value is PlanningRequest {
  return typeof value === 'object' && value !== null && 'mode' in value && 'preferences' in value
}

export function normalizePlanningRequest(
  input: PlanningRequest,
  repository: PoiRepository,
): PlanningRequest {
  if (!input || (input.mode !== 'quick' && input.mode !== 'custom'))
    throw new TravelValidationError('规划模式无效')
  if (!Number.isInteger(input.durationMinutes)
    || input.durationMinutes < 120
    || input.durationMinutes > 600
    || input.durationMinutes % 30 !== 0) {
    throw new TravelValidationError('规划时长必须为 120 至 600 分钟，并以 30 分钟为步进')
  }
  if (!Array.isArray(input.selectedPoiIds))
    throw new TravelValidationError('自选点位格式无效')
  const selectedPoiIds = input.selectedPoiIds.map(poiId => typeof poiId === 'string' ? poiId.trim() : '')
  if (input.mode === 'quick' && selectedPoiIds.length > 0)
    throw new TravelValidationError('快速规划不能预选点位')
  if (input.mode === 'custom' && (selectedPoiIds.length < 2 || selectedPoiIds.length > 8))
    throw new TravelValidationError('自选规划必须选择 2 至 8 个点位')
  if (new Set(selectedPoiIds).size !== selectedPoiIds.length)
    throw new TravelValidationError('自选点位不能重复')
  if (input.mode === 'custom') {
    selectedPoiIds.forEach((poiId) => {
      if (!poiId || !repository.getPoiById(poiId))
        throw new TravelValidationError(`自选点位 ID 无效：${poiId || '空值'}`)
    })
  }
  return {
    mode: input.mode,
    preferences: normalizeTravelPreferences(input.preferences),
    durationMinutes: input.durationMinutes,
    selectedPoiIds,
    ...(normalizePlanningOrigin(input.origin) ? { origin: normalizePlanningOrigin(input.origin)! } : {}),
  }
}

function legacyPlanningRequest(
  preferences: TravelPreferences,
  planningOrigin?: PlanningOrigin,
): PlanningRequest {
  return {
    mode: 'quick',
    preferences,
    durationMinutes: preferences.duration === 'half_day' ? 240 : 480,
    selectedPoiIds: [],
    ...(planningOrigin ? { origin: planningOrigin } : {}),
  }
}

function buildSources(pois: PoiResolved[]): ItinerarySource[] {
  return pois.map(poi => ({
    placeId: poi.id,
    sourceName: poi.dataSource,
    sourceUrl: poi.sourceReference ?? '',
    updatedAt: poi.updatedAt,
  }))
}

function stableHash(value: string): string {
  let hash = 0x811C9DC5
  for (let index = 0; index < value.length; index += 1) {
    hash ^= value.charCodeAt(index)
    hash = Math.imul(hash, 0x01000193)
  }
  return (hash >>> 0).toString(36)
}

function conversationId(request: PlanningRequest, datasetVersion: string): string {
  return `local-poc-${stableHash(JSON.stringify({
    datasetVersion,
    preferences: request.preferences,
    mode: request.mode,
    durationMinutes: request.durationMinutes,
    selectedPoiIds: request.selectedPoiIds,
    usesOrigin: Boolean(request.origin),
  }))}`
}

function budgetText(preferences: TravelPreferences): string {
  const labels = { economy: '经济型', standard: '标准型', quality: '品质型' } as const
  return `${labels[preferences.budgetLevel]}偏好；POC 未接入实时票价和交通计费，请以各点位及导航平台当日信息为准。`
}

export function createLocalPlan(
  input: TravelPreferences | PlanningRequest,
  repository: PoiRepository,
  existingConversationId?: string,
  planningOrigin?: PlanningOrigin,
): PlanResponse {
  const request = normalizePlanningRequest(
    isPlanningRequest(input)
      ? input
      : legacyPlanningRequest(normalizeTravelPreferences(input), planningOrigin),
    repository,
  )
  const preferences = request.preferences
  const origin = normalizePlanningOrigin(request.origin)
  const pois = request.mode === 'custom'
    ? selectCustomPois(request, repository)
    : selectQuickPois(preferences, repository, request.durationMinutes, origin)
  if (pois.length === 0)
    throw new Error('当前 POI 数据中没有可用于规划的地点')
  const items = buildItems(pois, preferences, origin)
  const lastEnd = items[items.length - 1].endTime.split(':').map(Number)
  const totalMinutes = lastEnd[0] * 60 + lastEnd[1] - 9 * 60
  const durationLabel = preferences.duration === 'half_day' ? '半日' : '一日'

  return {
    conversationId: existingConversationId ?? conversationId(request, repository.getDatasetMeta().datasetVersion),
    assistantMessage: origin
      ? '已结合你的当前位置，使用本机 POI 数据生成确定性 POC 行程。'
      : '已使用本机 POI 数据生成确定性 POC 行程。',
    itinerary: {
      title: `光明区${durationLabel}探索路线`,
      summary: `根据你的主题、兴趣和节奏，从当前已发布的光明区 POI 中选出 ${items.length} 个点位${origin ? '，并从距当前位置较近的点位开始编排' : ''}。`,
      totalMinutes,
      planningMode: request.mode,
      requestedMinutes: request.durationMinutes,
      durationFitStatus: classifyDurationFit(request.durationMinutes, totalMinutes),
      startsFromCurrentLocation: Boolean(origin),
      estimatedCostText: budgetText(preferences),
      items,
      notes: [
        ...(origin ? ['已使用本次前台定位优化首站和游览顺序；精确坐标不会写入行程缓存。'] : []),
        '本地 POC 未接入实时路况、票价和预约库存，点位间交通时间仅按直线距离估算。',
        repository.getDatasetMeta().disclaimer,
      ],
      sources: buildSources(pois),
    },
    source: 'local_poc',
  }
}

export function createRecommendedPlan(
  input: PlanningRequest,
  recommendations: readonly PlanningRecommendation[],
  repository: PoiRepository,
  assistantMessage: string,
  source: 'remote_mock' | 'remote_ai',
  existingConversationId?: string,
): PlanResponse {
  const request = normalizePlanningRequest(input, repository)
  const normalizedRecommendations = normalizeRecommendations(recommendations, request, repository)
  const origin = normalizePlanningOrigin(request.origin)
  const pois = selectRecommendedPois(normalizedRecommendations, request, repository, origin)
  if (pois.length === 0)
    throw new TravelValidationError('AI 推荐点位无法在所选时长内完成，请延长时长或调整偏好')

  const recommendationReasons = new Map(normalizedRecommendations.map(item => [item.poiId, item.reason]))
  const items = buildItems(pois, request.preferences, origin, recommendationReasons)
  const lastEnd = items[items.length - 1].endTime.split(':').map(Number)
  const totalMinutes = lastEnd[0] * 60 + lastEnd[1] - 9 * 60
  const durationLabel = request.preferences.duration === 'half_day' ? '半日' : '一日'
  const isRealAI = source === 'remote_ai'

  return {
    conversationId: existingConversationId ?? `remote-ai-${stableHash(JSON.stringify({
      datasetVersion: repository.getDatasetMeta().datasetVersion,
      request: {
        mode: request.mode,
        preferences: request.preferences,
        durationMinutes: request.durationMinutes,
        selectedPoiIds: request.selectedPoiIds,
        usesOrigin: Boolean(origin),
      },
      recommendedPoiIds: normalizedRecommendations.map(item => item.poiId),
    }))}`,
    assistantMessage: assistantMessage.trim() || (isRealAI
      ? '在线 AI 已从审核点位中给出推荐。'
      : '远端演示服务已从审核点位中给出推荐。'),
    itinerary: {
      title: `光明区${durationLabel}${isRealAI ? ' AI' : '智能'}推荐路线`,
      summary: `${isRealAI ? '在线 AI' : '远端演示服务'}从审核 POI 白名单中推荐了 ${items.length} 个点位，本机已结合交通、时长${origin ? '和当前位置' : ''}完成安全排程。`,
      totalMinutes,
      planningMode: request.mode,
      requestedMinutes: request.durationMinutes,
      durationFitStatus: classifyDurationFit(request.durationMinutes, totalMinutes),
      startsFromCurrentLocation: Boolean(origin),
      estimatedCostText: budgetText(request.preferences),
      items,
      notes: [
        ...(origin ? ['已使用本次前台定位优化首站和游览顺序；精确坐标不会写入行程缓存，也不会发送给 AI。'] : []),
        `${isRealAI ? 'AI' : '远端演示服务'}仅从已审核 POI 白名单中推荐点位；路线、交通时间与时长适配由本机核算。`,
        '当前未接入实时路况、票价和预约库存，点位间交通时间仅按直线距离估算。',
        repository.getDatasetMeta().disclaimer,
      ],
      sources: buildSources(pois),
    },
    source,
  }
}

function appendRequirement(current: string, message: string): string {
  const segments = current.split('；').map(item => item.trim()).filter(Boolean)
  if (!segments.includes(message))
    segments.push(message)
  return segments.join('；').slice(0, 500)
}

function addUnique<T>(values: T[], value: T): void {
  if (!values.includes(value))
    values.push(value)
}

function adjustedPreferences(input: TravelPreferences, message: string): TravelPreferences {
  const preferences = normalizeTravelPreferences(input)
  const normalizedMessage = message.trim()
  const result: TravelPreferences = {
    ...preferences,
    themes: [...preferences.themes],
    interests: [...preferences.interests],
    childAges: [...preferences.childAges],
    extraRequirements: appendRequirement(preferences.extraRequirements, normalizedMessage),
  }

  if (/慢一点|轻松|别太赶|少安排|少一点/.test(normalizedMessage))
    result.pace = 'relaxed'
  else if (/紧凑|多安排|多看|丰富一点/.test(normalizedMessage))
    result.pace = 'compact'
  if (/半日|半天/.test(normalizedMessage))
    result.duration = 'half_day'
  else if (/一日|一天|全天/.test(normalizedMessage))
    result.duration = 'full_day'

  if (includesPositiveMention(normalizedMessage, ['亲子', '儿童', '孩子']))
    addUnique(result.themes, '亲子')
  if (includesPositiveMention(normalizedMessage, ['研学']))
    addUnique(result.themes, '研学')
  if (includesPositiveMention(normalizedMessage, ['自然', '公园', '户外']))
    addUnique(result.interests, '自然风光')
  if (includesPositiveMention(normalizedMessage, ['文化', '场馆', '室内']))
    addUnique(result.interests, '文化场馆')
  if (includesPositiveMention(normalizedMessage, ['科普']))
    addUnique(result.interests, '生态科普')
  if (includesPositiveMention(normalizedMessage, ['摄影', '拍照']))
    addUnique(result.interests, '摄影')

  if (includesPositiveMention(normalizedMessage, ['步行', '走路']))
    result.transport = 'walking'
  else if (includesPositiveMention(normalizedMessage, ['自驾', '开车']))
    result.transport = 'driving'
  else if (includesPositiveMention(normalizedMessage, ['公交', '公共交通', '地铁']))
    result.transport = 'public_transport'

  return normalizeTravelPreferences(result)
}

export function adjustLocalPlan(
  currentPlan: PlanResponse,
  currentPreferences: TravelPreferences,
  message: string,
  repository: PoiRepository,
  planningOrigin?: PlanningOrigin,
  currentRequest?: PlanningRequest,
): LocalPlanAdjustment {
  const normalizedMessage = message.trim()
  if (!normalizedMessage)
    throw new TravelValidationError('请填写需要调整的内容')
  if (normalizedMessage.length > 500)
    throw new TravelValidationError('调整内容不能超过 500 字')

  const preferences = adjustedPreferences(currentPreferences, normalizedMessage)
  const requestedMinutes = /半日|半天/.test(normalizedMessage)
    ? 240
    : /一日|一天|全天/.test(normalizedMessage)
      ? 480
      : currentPlan.itinerary.requestedMinutes
  const request = normalizePlanningRequest(currentRequest ?? {
    mode: currentPlan.itinerary.planningMode,
    preferences,
    durationMinutes: requestedMinutes,
    selectedPoiIds: currentPlan.itinerary.planningMode === 'custom'
      ? currentPlan.itinerary.items.map(item => item.placeId)
      : [],
    ...(planningOrigin ? { origin: planningOrigin } : {}),
  }, repository)
  request.preferences = preferences
  request.durationMinutes = requestedMinutes
  const plan = createLocalPlan(request, repository, currentPlan.conversationId)
  plan.assistantMessage = '已按你的补充要求重新编排本地 POC 行程。'
  plan.changeSummary = `已根据“${normalizedMessage}”更新相关安排。`
  return { preferences, plan, request }
}
