import { type LonLatPoint } from '../../utils/projection'
import { DEFAULT_GAME_AUDIO_CONFIG } from '../audio/audioConfig'
import { type GameControl, type GameDefinition } from '../core/gameDefinition'
import { type GameEvent } from '../core/gameEvent'
import { type GameEffect, type GameResult } from '../core/gameResult'
import { type GameSessionState } from '../core/gameSessionState'
import { type GamePresentationState } from '../presentation/presentationState'
import { type HudPresentationState } from '../presentation/hudPresentationState'
import { type MapPresentationState } from '../presentation/mapPresentationState'
import { type RulePlugin } from './rulePlugin'

type ScoreOModeState = {
  phase: 'start' | 'controls' | 'finish' | 'done'
  focusedControlId: string | null
  guidanceControlId: string | null
}

function getApproxDistanceMeters(a: LonLatPoint, b: LonLatPoint): number {
  const avgLatRad = ((a.lat + b.lat) / 2) * Math.PI / 180
  const dx = (b.lon - a.lon) * 111320 * Math.cos(avgLatRad)
  const dy = (b.lat - a.lat) * 110540
  return Math.sqrt(dx * dx + dy * dy)
}

function getStartControl(definition: GameDefinition): GameControl | null {
  return definition.controls.find((control) => control.kind === 'start') || null
}

function getFinishControl(definition: GameDefinition): GameControl | null {
  return definition.controls.find((control) => control.kind === 'finish') || null
}

function getScoreControls(definition: GameDefinition): GameControl[] {
  return definition.controls.filter((control) => control.kind === 'control')
}

function getControlScore(control: GameControl): number {
  return control.kind === 'control' && typeof control.score === 'number' ? control.score : 0
}

function getRemainingScoreControls(definition: GameDefinition, state: GameSessionState): GameControl[] {
  return getScoreControls(definition).filter((control) => !state.completedControlIds.includes(control.id))
}

function getModeState(state: GameSessionState): ScoreOModeState {
  const rawModeState = state.modeState as Partial<ScoreOModeState> | null
  return {
    phase: rawModeState && rawModeState.phase ? rawModeState.phase : 'start',
    focusedControlId: rawModeState && typeof rawModeState.focusedControlId === 'string' ? rawModeState.focusedControlId : null,
    guidanceControlId: rawModeState && typeof rawModeState.guidanceControlId === 'string' ? rawModeState.guidanceControlId : null,
  }
}

function withModeState(state: GameSessionState, modeState: ScoreOModeState): GameSessionState {
  return {
    ...state,
    modeState,
  }
}

function hasCompletedEnoughControlsForFinish(definition: GameDefinition, state: GameSessionState): boolean {
  const completedScoreControls = getScoreControls(definition)
    .filter((control) => state.completedControlIds.includes(control.id))
    .length
  return completedScoreControls >= definition.minCompletedControlsBeforeFinish
}

function canFocusFinish(definition: GameDefinition, state: GameSessionState): boolean {
  const startControl = getStartControl(definition)
  const finishControl = getFinishControl(definition)
  const completedStart = !!startControl && state.completedControlIds.includes(startControl.id)
  const completedFinish = !!finishControl && state.completedControlIds.includes(finishControl.id)
  return completedStart && !completedFinish && hasCompletedEnoughControlsForFinish(definition, state)
}

function isFinishPunchAvailable(
  definition: GameDefinition,
  state: GameSessionState,
  _modeState: ScoreOModeState,
): boolean {
  return canFocusFinish(definition, state)
}

function getNearestRemainingControl(
  definition: GameDefinition,
  state: GameSessionState,
  referencePoint?: LonLatPoint | null,
): GameControl | null {
  const remainingControls = getRemainingScoreControls(definition, state)
  if (!remainingControls.length) {
    return getFinishControl(definition)
  }

  if (!referencePoint) {
    return remainingControls[0]
  }

  let nearestControl = remainingControls[0]
  let nearestDistance = getApproxDistanceMeters(referencePoint, nearestControl.point)
  for (let index = 1; index < remainingControls.length; index += 1) {
    const control = remainingControls[index]
    const distance = getApproxDistanceMeters(referencePoint, control.point)
    if (distance < nearestDistance) {
      nearestControl = control
      nearestDistance = distance
    }
  }
  return nearestControl
}

function getNearestGuidanceTarget(
  definition: GameDefinition,
  state: GameSessionState,
  modeState: ScoreOModeState,
  referencePoint: LonLatPoint,
): GameControl | null {
  const candidates = getRemainingScoreControls(definition, state).slice()
  if (isFinishPunchAvailable(definition, state, modeState)) {
    const finishControl = getFinishControl(definition)
    if (finishControl && !state.completedControlIds.includes(finishControl.id)) {
      candidates.push(finishControl)
    }
  }

  if (!candidates.length) {
    return null
  }

  let nearestControl = candidates[0]
  let nearestDistance = getApproxDistanceMeters(referencePoint, nearestControl.point)
  for (let index = 1; index < candidates.length; index += 1) {
    const control = candidates[index]
    const distance = getApproxDistanceMeters(referencePoint, control.point)
    if (distance < nearestDistance) {
      nearestControl = control
      nearestDistance = distance
    }
  }

  return nearestControl
}

function getFocusedTarget(
  definition: GameDefinition,
  state: GameSessionState,
  remainingControls?: GameControl[],
): GameControl | null {
  const modeState = getModeState(state)
  if (!modeState.focusedControlId) {
    return null
  }

  const controls = remainingControls || getRemainingScoreControls(definition, state)
  for (const control of controls) {
    if (control.id === modeState.focusedControlId) {
      return control
    }
  }

  const finishControl = getFinishControl(definition)
  if (finishControl && canFocusFinish(definition, state) && finishControl.id === modeState.focusedControlId) {
    return finishControl
  }

  return null
}

function resolveInteractiveTarget(
  definition: GameDefinition,
  state: GameSessionState,
  modeState: ScoreOModeState,
  primaryTarget: GameControl | null,
  focusedTarget: GameControl | null,
): GameControl | null {
  if (modeState.phase === 'start') {
    return primaryTarget
  }

  if (modeState.phase === 'finish') {
    return primaryTarget
  }

  if (definition.requiresFocusSelection) {
    return focusedTarget
  }

  if (focusedTarget) {
    return focusedTarget
  }

  if (modeState.phase === 'controls' && isFinishPunchAvailable(definition, state, modeState)) {
    return getFinishControl(definition)
  }

  return primaryTarget
}

function getNearestInRangeControl(
  controls: GameControl[],
  referencePoint: LonLatPoint,
  radiusMeters: number,
): GameControl | null {
  let nearest: GameControl | null = null
  let nearestDistance = Number.POSITIVE_INFINITY

  for (const control of controls) {
    const distance = getApproxDistanceMeters(control.point, referencePoint)
    if (distance > radiusMeters) {
      continue
    }

    if (!nearest || distance < nearestDistance) {
      nearest = control
      nearestDistance = distance
    }
  }

  return nearest
}

function getGuidanceState(definition: GameDefinition, distanceMeters: number): GameSessionState['guidanceState'] {
  const audioConfig = definition.audioConfig || DEFAULT_GAME_AUDIO_CONFIG
  const readyDistanceMeters = Math.max(definition.punchRadiusMeters, audioConfig.readyDistanceMeters)
  const approachDistanceMeters = Math.max(readyDistanceMeters, audioConfig.approachDistanceMeters)
  const distantDistanceMeters = Math.max(approachDistanceMeters, audioConfig.distantDistanceMeters)

  if (distanceMeters <= readyDistanceMeters) {
    return 'ready'
  }

  if (distanceMeters <= approachDistanceMeters) {
    return 'approaching'
  }

  if (distanceMeters <= distantDistanceMeters) {
    return 'distant'
  }

  return 'searching'
}

function getGuidanceEffects(
  previousState: GameSessionState['guidanceState'],
  nextState: GameSessionState['guidanceState'],
  controlId: string | null,
): GameEffect[] {
  if (previousState === nextState) {
    return []
  }

  return [{ type: 'guidance_state_changed', guidanceState: nextState, controlId }]
}

function getDisplayTargetLabel(control: GameControl | null): string {
  if (!control) {
    return '目标点'
  }
  if (control.kind === 'start') {
    return '开始点'
  }
  if (control.kind === 'finish') {
    return '终点'
  }
  return '目标点'
}

function buildPunchHintText(
  definition: GameDefinition,
  state: GameSessionState,
  primaryTarget: GameControl | null,
  focusedTarget: GameControl | null,
): string {
  if (state.status === 'idle') {
    return '点击开始后先打开始点'
  }

  if (state.status === 'finished') {
    return '本局已完成'
  }

  const modeState = getModeState(state)
  if (modeState.phase === 'controls' || modeState.phase === 'finish') {
    if (modeState.phase === 'controls' && definition.requiresFocusSelection && !focusedTarget) {
      return '点击地图选中一个目标点'
    }

    const displayTarget = resolveInteractiveTarget(definition, state, modeState, primaryTarget, focusedTarget)
    const targetLabel = getDisplayTargetLabel(displayTarget)
    if (displayTarget && state.inRangeControlId === displayTarget.id) {
      return definition.punchPolicy === 'enter'
        ? `${targetLabel}内，自动打点中`
        : `${targetLabel}内，可点击打点`
    }

    return definition.punchPolicy === 'enter'
      ? `进入${targetLabel}自动打点`
      : `进入${targetLabel}后点击打点`
  }

  const targetLabel = getDisplayTargetLabel(primaryTarget)
  if (state.inRangeControlId && primaryTarget && state.inRangeControlId === primaryTarget.id) {
    return definition.punchPolicy === 'enter'
      ? `${targetLabel}内，自动打点中`
      : `${targetLabel}内，可点击打点`
  }

  return definition.punchPolicy === 'enter'
    ? `进入${targetLabel}自动打点`
    : `进入${targetLabel}后点击打点`
}

function buildTargetSummaryText(
  definition: GameDefinition,
  state: GameSessionState,
  primaryTarget: GameControl | null,
  focusedTarget: GameControl | null,
): string {
  if (state.status === 'idle') {
    return primaryTarget ? `${primaryTarget.label} / 先打开始点` : '先打开始点'
  }

  if (state.status === 'finished') {
    return '本局已完成'
  }

  const modeState = getModeState(state)
  if (modeState.phase === 'start') {
    return primaryTarget ? `${primaryTarget.label} / 先打开始点` : '先打开始点'
  }

  if (modeState.phase === 'finish') {
    return primaryTarget ? `${primaryTarget.label} / 可随时结束` : '可前往终点结束'
  }

  if (focusedTarget && focusedTarget.kind === 'control') {
    return `${focusedTarget.label} / ${getControlScore(focusedTarget)} 分目标`
  }

  if (focusedTarget && focusedTarget.kind === 'finish') {
    return `${focusedTarget.label} / 结束比赛`
  }

  if (definition.requiresFocusSelection) {
    return '请选择目标点'
  }

  if (primaryTarget && primaryTarget.kind === 'control') {
    return `${primaryTarget.label} / ${getControlScore(primaryTarget)} 分目标`
  }

  return primaryTarget ? primaryTarget.label : '自由打点'
}

function buildCompletedEffect(control: GameControl, punchPolicy: GameDefinition['punchPolicy']): GameEffect {
  const allowAutoPopup = punchPolicy === 'enter'
    ? false
    : (control.displayContent ? control.displayContent.autoPopup : true)
  const autoOpenQuiz = control.kind === 'control'
    && !!control.displayContent
    && control.displayContent.ctas.some((item) => item.type === 'quiz')
  if (control.kind === 'start') {
    return {
      type: 'control_completed',
      controlId: control.id,
      controlKind: 'start',
      sequence: null,
      label: control.label,
      displayTitle: control.displayContent ? control.displayContent.title : '比赛开始',
      displayBody: control.displayContent ? control.displayContent.body : '已完成开始点打卡，开始自由打点。',
      displayAutoPopup: allowAutoPopup,
      displayOnce: control.displayContent ? control.displayContent.once : false,
      displayPriority: control.displayContent ? control.displayContent.priority : 1,
      autoOpenQuiz: false,
    }
  }

  if (control.kind === 'finish') {
    return {
      type: 'control_completed',
      controlId: control.id,
      controlKind: 'finish',
      sequence: null,
      label: control.label,
      displayTitle: control.displayContent ? control.displayContent.title : '比赛结束',
      displayBody: control.displayContent ? control.displayContent.body : '已完成终点打卡，本局结束。',
      displayAutoPopup: allowAutoPopup,
      displayOnce: control.displayContent ? control.displayContent.once : false,
      displayPriority: control.displayContent ? control.displayContent.priority : 2,
      autoOpenQuiz: false,
    }
  }

  const sequenceText = typeof control.sequence === 'number' ? String(control.sequence) : control.label
  return {
    type: 'control_completed',
    controlId: control.id,
    controlKind: 'control',
    sequence: control.sequence,
    label: control.label,
    displayTitle: control.displayContent ? control.displayContent.title : `收集 ${sequenceText}`,
    displayBody: control.displayContent ? control.displayContent.body : control.label,
    displayAutoPopup: allowAutoPopup,
    displayOnce: control.displayContent ? control.displayContent.once : false,
    displayPriority: control.displayContent ? control.displayContent.priority : 1,
    autoOpenQuiz,
  }
}

function resolvePunchableControl(
  definition: GameDefinition,
  state: GameSessionState,
  modeState: ScoreOModeState,
  focusedTarget: GameControl | null,
): GameControl | null {
  if (!state.inRangeControlId) {
    return null
  }

  const inRangeControl = definition.controls.find((control) => control.id === state.inRangeControlId) || null
  if (!inRangeControl) {
    return null
  }

  if (modeState.phase === 'start') {
    return inRangeControl.kind === 'start' ? inRangeControl : null
  }

  if (modeState.phase === 'finish') {
    return inRangeControl.kind === 'finish' ? inRangeControl : null
  }

  if (modeState.phase === 'controls') {
    if (inRangeControl.kind === 'finish' && isFinishPunchAvailable(definition, state, modeState)) {
      return inRangeControl
    }

    if (definition.requiresFocusSelection) {
      return focusedTarget && inRangeControl.id === focusedTarget.id ? inRangeControl : null
    }

    if (inRangeControl.kind === 'control') {
      return inRangeControl
    }
  }

  return null
}

function buildPresentation(definition: GameDefinition, state: GameSessionState): GamePresentationState {
  const modeState = getModeState(state)
  const running = state.status === 'running'
  const startControl = getStartControl(definition)
  const finishControl = getFinishControl(definition)
  const completedStart = !!startControl && state.completedControlIds.includes(startControl.id)
  const completedFinish = !!finishControl && state.completedControlIds.includes(finishControl.id)
  const remainingControls = getRemainingScoreControls(definition, state)
  const scoreControls = getScoreControls(definition)
  const primaryTarget = definition.controls.find((control) => control.id === state.currentTargetControlId) || null
  const focusedTarget = getFocusedTarget(definition, state, remainingControls)
  const canSelectFinish = running && completedStart && !completedFinish && !!finishControl
  const activeControlIds = running && modeState.phase === 'controls'
    ? remainingControls.map((control) => control.id)
    : []
  const activeControlSequences = running && modeState.phase === 'controls'
    ? remainingControls
      .filter((control) => typeof control.sequence === 'number')
      .map((control) => control.sequence as number)
    : []
  const completedControls = scoreControls.filter((control) => state.completedControlIds.includes(control.id))
  const completedControlSequences = completedControls
    .filter((control) => typeof control.sequence === 'number')
    .map((control) => control.sequence as number)
  const revealFullCourse = completedStart
  const punchableControl = resolvePunchableControl(definition, state, modeState, focusedTarget)
  const guidanceControl = modeState.guidanceControlId
    ? definition.controls.find((control) => control.id === modeState.guidanceControlId) || null
    : null
  const punchButtonEnabled = running
    && definition.punchPolicy === 'enter-confirm'
    && !!punchableControl
  const hudTargetControlId = modeState.phase === 'finish'
    ? (primaryTarget ? primaryTarget.id : null)
    : focusedTarget
      ? focusedTarget.id
      : modeState.phase === 'controls' && isFinishPunchAvailable(definition, state, modeState)
        ? (getFinishControl(definition) ? getFinishControl(definition)!.id : null)
        : definition.requiresFocusSelection
          ? null
          : primaryTarget
            ? primaryTarget.id
            : null
  const highlightedControlId = focusedTarget
    ? focusedTarget.id
    : punchableControl
      ? punchableControl.id
      : guidanceControl
        ? guidanceControl.id
        : null

  const showMultiTargetLabels = completedStart && modeState.phase !== 'start'
  const mapPresentation: MapPresentationState = {
    controlVisualMode: showMultiTargetLabels ? 'multi-target' : 'single-target',
    showCourseLegs: false,
    guidanceLegAnimationEnabled: false,
    focusableControlIds: canSelectFinish
      ? [...activeControlIds, finishControl!.id]
      : activeControlIds.slice(),
    focusedControlId: focusedTarget ? focusedTarget.id : null,
    focusedControlSequences: focusedTarget && focusedTarget.kind === 'control' && typeof focusedTarget.sequence === 'number'
      ? [focusedTarget.sequence]
      : [],
    activeControlIds,
    activeControlSequences,
    activeStart: running && modeState.phase === 'start',
    completedStart,
    activeFinish: running && (modeState.phase === 'finish' || (modeState.phase === 'controls' && isFinishPunchAvailable(definition, state, modeState))),
    focusedFinish: !!focusedTarget && focusedTarget.kind === 'finish',
    completedFinish,
    revealFullCourse,
    activeLegIndices: [],
    completedLegIndices: [],
    completedControlIds: completedControls.map((control) => control.id),
    completedControlSequences,
    skippedControlIds: [],
    skippedControlSequences: [],
  }

  const hudPresentation: HudPresentationState = {
    actionTagText: modeState.phase === 'start'
      ? '目标'
      : modeState.phase === 'finish'
        ? '终点'
      : focusedTarget && focusedTarget.kind === 'finish'
        ? '终点'
        : focusedTarget
          ? '目标'
          : '自由',
    distanceTagText: modeState.phase === 'start'
      ? '点距'
      : modeState.phase === 'finish'
        ? '终点距'
      : focusedTarget && focusedTarget.kind === 'finish'
        ? '终点距'
        : focusedTarget
          ? '选中点距'
          : '目标距',
    targetSummaryText: buildTargetSummaryText(definition, state, primaryTarget, focusedTarget),
    hudTargetControlId,
    progressText: `已收集 ${completedControls.length}/${scoreControls.length}`,
    punchableControlId: punchableControl ? punchableControl.id : null,
    punchButtonEnabled,
    punchButtonText: modeState.phase === 'start'
      ? '开始打卡'
      : (punchableControl && punchableControl.kind === 'finish')
        ? '结束打卡'
      : modeState.phase === 'finish'
        ? '结束打卡'
      : focusedTarget && focusedTarget.kind === 'finish'
        ? '结束打卡'
        : '打点',
    punchHintText: buildPunchHintText(definition, state, primaryTarget, focusedTarget),
  }

  return {
    map: mapPresentation,
    hud: hudPresentation,
    targeting: {
      punchableControlId: punchableControl ? punchableControl.id : null,
      guidanceControlId: guidanceControl ? guidanceControl.id : null,
      hudControlId: hudTargetControlId,
      highlightedControlId,
    },
  }
}

function applyCompletion(
  definition: GameDefinition,
  state: GameSessionState,
  control: GameControl,
  at: number,
  referencePoint: LonLatPoint | null,
): GameResult {
  const completedControlIds = state.completedControlIds.includes(control.id)
    ? state.completedControlIds
    : [...state.completedControlIds, control.id]
  const previousModeState = getModeState(state)
  const nextStateDraft: GameSessionState = {
    ...state,
    endReason: control.kind === 'finish' ? 'completed' : state.endReason,
    startedAt: control.kind === 'start' && state.startedAt === null ? at : state.startedAt,
    endedAt: control.kind === 'finish' ? at : state.endedAt,
    completedControlIds,
    currentTargetControlId: null,
    inRangeControlId: null,
    score: getScoreControls(definition)
      .filter((item) => completedControlIds.includes(item.id))
      .reduce((sum, item) => sum + getControlScore(item), 0),
    status: control.kind === 'finish' ? 'finished' : state.status,
    guidanceState: 'searching',
  }

  const remainingControls = getRemainingScoreControls(definition, nextStateDraft)
  let phase: ScoreOModeState['phase']
  if (control.kind === 'finish') {
    phase = 'done'
  } else if (control.kind === 'start') {
    phase = remainingControls.length ? 'controls' : 'finish'
  } else {
    phase = remainingControls.length ? 'controls' : 'finish'
  }

  const nextPrimaryTarget = phase === 'controls'
    ? getNearestRemainingControl(definition, nextStateDraft, referencePoint)
    : phase === 'finish'
      ? getFinishControl(definition)
      : null
  const nextModeState: ScoreOModeState = {
    phase,
    focusedControlId: control.id === previousModeState.focusedControlId ? null : previousModeState.focusedControlId,
    guidanceControlId: nextPrimaryTarget ? nextPrimaryTarget.id : null,
  }
  const nextState = withModeState({
    ...nextStateDraft,
    currentTargetControlId: nextPrimaryTarget ? nextPrimaryTarget.id : null,
  }, nextModeState)

  const effects: GameEffect[] = [buildCompletedEffect(control, definition.punchPolicy)]
  if (control.kind === 'finish') {
    effects.push({ type: 'session_finished' })
  }

  return {
    nextState,
    presentation: buildPresentation(definition, nextState),
    effects,
  }
}

export class ScoreORule implements RulePlugin {
  get mode(): 'score-o' {
    return 'score-o'
  }

  initialize(definition: GameDefinition): GameSessionState {
    const startControl = getStartControl(definition)
    return {
      status: 'idle',
      endReason: null,
      startedAt: null,
      endedAt: null,
      completedControlIds: [],
      skippedControlIds: [],
      currentTargetControlId: startControl ? startControl.id : null,
      inRangeControlId: null,
      score: 0,
      guidanceState: 'searching',
      modeState: {
        phase: 'start',
        focusedControlId: null,
        guidanceControlId: startControl ? startControl.id : null,
      },
    }
  }

  buildPresentation(definition: GameDefinition, state: GameSessionState): GamePresentationState {
    return buildPresentation(definition, state)
  }

  reduce(definition: GameDefinition, state: GameSessionState, event: GameEvent): GameResult {
    if (event.type === 'session_started') {
      const startControl = getStartControl(definition)
      const nextState = withModeState({
        ...state,
        status: 'running',
        endReason: null,
        startedAt: null,
        endedAt: null,
        currentTargetControlId: startControl ? startControl.id : null,
        inRangeControlId: null,
        guidanceState: 'searching',
      }, {
        phase: 'start',
        focusedControlId: null,
        guidanceControlId: startControl ? startControl.id : null,
      })
      return {
        nextState,
        presentation: buildPresentation(definition, nextState),
        effects: [{ type: 'session_started' }],
      }
    }

    if (event.type === 'session_ended') {
      const nextState = withModeState({
        ...state,
        status: 'finished',
        endReason: 'completed',
        endedAt: event.at,
        guidanceState: 'searching',
      }, {
        phase: 'done',
        focusedControlId: null,
        guidanceControlId: null,
      })
      return {
        nextState,
        presentation: buildPresentation(definition, nextState),
        effects: [{ type: 'session_finished' }],
      }
    }

    if (event.type === 'session_timed_out') {
      const nextState = withModeState({
        ...state,
        status: 'failed',
        endReason: 'timed_out',
        endedAt: event.at,
        guidanceState: 'searching',
      }, {
        phase: 'done',
        focusedControlId: null,
        guidanceControlId: null,
      })
      return {
        nextState,
        presentation: buildPresentation(definition, nextState),
        effects: [{ type: 'session_timed_out' }],
      }
    }

    if (state.status !== 'running') {
      return {
        nextState: state,
        presentation: buildPresentation(definition, state),
        effects: [],
      }
    }

    const modeState = getModeState(state)
    const targetControl = state.currentTargetControlId
      ? definition.controls.find((control) => control.id === state.currentTargetControlId) || null
      : null

    if (event.type === 'gps_updated') {
      const referencePoint = { lon: event.lon, lat: event.lat }
      const remainingControls = getRemainingScoreControls(definition, state)
      const nextStateBase = withModeState(state, modeState)
      const focusedTarget = getFocusedTarget(definition, nextStateBase, remainingControls)
      let nextPrimaryTarget = targetControl
      let guidanceTarget = targetControl
      let punchTarget: GameControl | null = null

      if (modeState.phase === 'controls') {
        nextPrimaryTarget = getNearestRemainingControl(definition, state, referencePoint)
        guidanceTarget = getNearestGuidanceTarget(definition, state, modeState, referencePoint)
        if (focusedTarget && getApproxDistanceMeters(focusedTarget.point, referencePoint) <= definition.punchRadiusMeters) {
          punchTarget = focusedTarget
        } else if (isFinishPunchAvailable(definition, state, modeState)) {
          const finishControl = getFinishControl(definition)
          if (finishControl && getApproxDistanceMeters(finishControl.point, referencePoint) <= definition.punchRadiusMeters) {
            punchTarget = finishControl
          }
        }
        if (!punchTarget && !definition.requiresFocusSelection) {
          punchTarget = getNearestInRangeControl(remainingControls, referencePoint, definition.punchRadiusMeters)
        }
      } else if (modeState.phase === 'finish') {
        nextPrimaryTarget = getFinishControl(definition)
        guidanceTarget = nextPrimaryTarget
        if (nextPrimaryTarget && getApproxDistanceMeters(nextPrimaryTarget.point, referencePoint) <= definition.punchRadiusMeters) {
          punchTarget = nextPrimaryTarget
        }
      } else if (targetControl) {
        guidanceTarget = targetControl
        if (getApproxDistanceMeters(targetControl.point, referencePoint) <= definition.punchRadiusMeters) {
          punchTarget = targetControl
        }
      }

      const guidanceState = guidanceTarget
        ? getGuidanceState(definition, getApproxDistanceMeters(guidanceTarget.point, referencePoint))
        : 'searching'
      const nextState: GameSessionState = {
        ...nextStateBase,
        currentTargetControlId: nextPrimaryTarget ? nextPrimaryTarget.id : null,
        inRangeControlId: punchTarget ? punchTarget.id : null,
        guidanceState,
      }
      const nextStateWithMode = withModeState(nextState, {
        ...modeState,
        guidanceControlId: guidanceTarget ? guidanceTarget.id : null,
      })
      const guidanceEffects = getGuidanceEffects(state.guidanceState, guidanceState, guidanceTarget ? guidanceTarget.id : null)

      if (definition.punchPolicy === 'enter' && punchTarget) {
        const completionResult = applyCompletion(definition, nextStateWithMode, punchTarget, event.at, referencePoint)
        return {
          ...completionResult,
          effects: [...guidanceEffects, ...completionResult.effects],
        }
      }

      return {
        nextState: nextStateWithMode,
        presentation: buildPresentation(definition, nextStateWithMode),
        effects: guidanceEffects,
      }
    }

    if (event.type === 'control_focused') {
      if (modeState.phase !== 'controls' && modeState.phase !== 'finish') {
        return {
          nextState: state,
          presentation: buildPresentation(definition, state),
          effects: [],
        }
      }

      const focusableControlIds = getRemainingScoreControls(definition, state).map((control) => control.id)
      const finishControl = getFinishControl(definition)
      if (finishControl && canFocusFinish(definition, state)) {
        focusableControlIds.push(finishControl.id)
      }

      const nextFocusedControlId = event.controlId && focusableControlIds.includes(event.controlId)
        ? modeState.focusedControlId === event.controlId
          ? null
          : event.controlId
        : null
      const nextState = withModeState({
        ...state,
      }, {
        ...modeState,
        focusedControlId: nextFocusedControlId,
        guidanceControlId: modeState.guidanceControlId,
      })
      return {
        nextState,
        presentation: buildPresentation(definition, nextState),
        effects: [],
      }
    }

    if (event.type === 'punch_requested') {
      const focusedTarget = getFocusedTarget(definition, state)
      let stateForPunch = state
      const finishControl = getFinishControl(definition)
      const finishInRange = !!(
        finishControl
        && event.lon !== null
        && event.lat !== null
        && getApproxDistanceMeters(finishControl.point, { lon: event.lon, lat: event.lat }) <= definition.punchRadiusMeters
      )
      if (definition.requiresFocusSelection && modeState.phase === 'controls' && !focusedTarget && !finishInRange) {
        return {
          nextState: state,
          presentation: buildPresentation(definition, state),
          effects: [{ type: 'punch_feedback', text: '请先选中目标点', tone: 'warning' }],
        }
      }

      let controlToPunch: GameControl | null = null
      if (state.inRangeControlId) {
        controlToPunch = definition.controls.find((control) => control.id === state.inRangeControlId) || null
      }

      if (!controlToPunch && event.lon !== null && event.lat !== null) {
        const referencePoint = { lon: event.lon, lat: event.lat }
        const nextStateBase = withModeState(state, modeState)
        stateForPunch = nextStateBase
        const remainingControls = getRemainingScoreControls(definition, state)
        const resolvedFocusedTarget = getFocusedTarget(definition, nextStateBase, remainingControls)

        if (resolvedFocusedTarget && getApproxDistanceMeters(resolvedFocusedTarget.point, referencePoint) <= definition.punchRadiusMeters) {
          controlToPunch = resolvedFocusedTarget
        } else if (isFinishPunchAvailable(definition, state, modeState)) {
          const finishControl = getFinishControl(definition)
          if (finishControl && getApproxDistanceMeters(finishControl.point, referencePoint) <= definition.punchRadiusMeters) {
            controlToPunch = finishControl
          }
        }

        if (!controlToPunch && !definition.requiresFocusSelection && modeState.phase === 'controls') {
          controlToPunch = getNearestInRangeControl(remainingControls, referencePoint, definition.punchRadiusMeters)
        }
      }

      if (!controlToPunch || (definition.requiresFocusSelection && modeState.phase === 'controls' && focusedTarget && controlToPunch.id !== focusedTarget.id)) {
        const isFinishLockedAttempt = !!(
          finishControl
          && event.lon !== null
          && event.lat !== null
          && getApproxDistanceMeters(finishControl.point, { lon: event.lon, lat: event.lat }) <= definition.punchRadiusMeters
          && !hasCompletedEnoughControlsForFinish(definition, state)
        )
        return {
          nextState: state,
          presentation: buildPresentation(definition, state),
          effects: [{
            type: 'punch_feedback',
            text: isFinishLockedAttempt
              ? `至少完成 ${definition.minCompletedControlsBeforeFinish} 个积分点后才能结束`
              : focusedTarget
              ? `未进入${getDisplayTargetLabel(focusedTarget)}打卡范围`
              : modeState.phase === 'start'
                ? '未进入开始点打卡范围'
                : '未进入目标打点范围',
            tone: 'warning',
          }],
        }
      }

      return applyCompletion(definition, stateForPunch, controlToPunch, event.at, this.getReferencePoint(definition, stateForPunch, controlToPunch))
    }

    return {
      nextState: state,
      presentation: buildPresentation(definition, state),
      effects: [],
    }
  }

  private getReferencePoint(definition: GameDefinition, state: GameSessionState, completedControl: GameControl): LonLatPoint | null {
    if (completedControl.kind === 'control') {
      const remaining = getRemainingScoreControls(definition, {
        ...state,
        completedControlIds: [...state.completedControlIds, completedControl.id],
      })
      return remaining.length ? completedControl.point : (getFinishControl(definition) ? getFinishControl(definition)!.point : completedControl.point)
    }

    return completedControl.point
  }
}
