import { type GameDefinition } from '../core/gameDefinition'
import {
  DEFAULT_TELEMETRY_CONFIG,
  getHeartRateToneLabel,
  getHeartRateToneRangeText,
  getSpeedToneRangeText,
  type HeartRateTone,
  type TelemetryConfig,
} from './telemetryConfig'
import { mergeTelemetrySources, type PlayerTelemetryProfile } from './playerTelemetryProfile'
import { type GameSessionState } from '../core/gameSessionState'
import { type TelemetryEvent } from './telemetryEvent'
import { EMPTY_TELEMETRY_PRESENTATION, type TelemetryPresentation } from './telemetryPresentation'
import {
  EMPTY_TELEMETRY_STATE,
  type DevicePose,
  type HeadingConfidence,
  type TelemetryState,
} from './telemetryState'
import { type RecoveryTelemetrySnapshot } from '../core/sessionRecovery'
const SPEED_SMOOTHING_ALPHA = 0.35
const DEVICE_HEADING_SMOOTHING_ALPHA = 0.28
const ACCELEROMETER_SMOOTHING_ALPHA = 0.2
const DEVICE_POSE_FLAT_ENTER_Z = 0.82
const DEVICE_POSE_FLAT_EXIT_Z = 0.7
const DEVICE_POSE_UPRIGHT_ENTER_Z = 0.42
const DEVICE_POSE_UPRIGHT_EXIT_Z = 0.55
const DEVICE_POSE_UPRIGHT_AXIS_ENTER = 0.78
const DEVICE_POSE_UPRIGHT_AXIS_EXIT = 0.65
const HEADING_CONFIDENCE_HIGH_TURN_RATE_RAD = 0.35
const HEADING_CONFIDENCE_MEDIUM_TURN_RATE_RAD = 1.05

function normalizeHeadingDeg(headingDeg: number): number {
  const normalized = headingDeg % 360
  return normalized < 0 ? normalized + 360 : normalized
}

function normalizeHeadingDeltaDeg(deltaDeg: number): number {
  let normalized = deltaDeg

  while (normalized > 180) {
    normalized -= 360
  }

  while (normalized < -180) {
    normalized += 360
  }

  return normalized
}

function interpolateHeadingDeg(currentDeg: number, targetDeg: number, factor: number): number {
  return normalizeHeadingDeg(currentDeg + normalizeHeadingDeltaDeg(targetDeg - currentDeg) * factor)
}

function resolveMotionCompassHeadingDeg(
  alpha: number | null,
  beta: number | null,
  gamma: number | null,
): number | null {
  if (alpha === null) {
    return null
  }

  if (beta === null || gamma === null) {
    return normalizeHeadingDeg(360 - alpha)
  }

  const alphaRad = alpha * Math.PI / 180
  const betaRad = beta * Math.PI / 180
  const gammaRad = gamma * Math.PI / 180

  const cA = Math.cos(alphaRad)
  const sA = Math.sin(alphaRad)
  const sB = Math.sin(betaRad)
  const cG = Math.cos(gammaRad)
  const sG = Math.sin(gammaRad)

  const headingX = -cA * sG - sA * sB * cG
  const headingY = -sA * sG + cA * sB * cG

  if (Math.abs(headingX) < 1e-6 && Math.abs(headingY) < 1e-6) {
    return normalizeHeadingDeg(360 - alpha)
  }

  let headingRad = Math.atan2(headingX, headingY)
  if (headingRad < 0) {
    headingRad += Math.PI * 2
  }

  return normalizeHeadingDeg(headingRad * 180 / Math.PI)
}

function getApproxDistanceMeters(
  a: { lon: number; lat: number },
  b: { lon: number; lat: number },
): 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 formatElapsedTimerText(totalMs: number): string {
  const safeMs = Math.max(0, totalMs)
  const totalSeconds = Math.floor(safeMs / 1000)
  const hours = Math.floor(totalSeconds / 3600)
  const minutes = Math.floor((totalSeconds % 3600) / 60)
  const seconds = totalSeconds % 60
  return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`
}

function formatCountdownTimerText(remainingMs: number): string {
  return formatElapsedTimerText(Math.max(0, remainingMs))
}

function formatDistanceText(distanceMeters: number): string {
  if (distanceMeters >= 1000) {
    return `${(distanceMeters / 1000).toFixed(distanceMeters >= 10000 ? 0 : 1)}km`
  }

  return `${Math.round(distanceMeters)}m`
}

function formatTargetDistance(distanceMeters: number | null): { valueText: string; unitText: string } {
  if (distanceMeters === null) {
    return {
      valueText: '--',
      unitText: '',
    }
  }

  return distanceMeters >= 1000
    ? {
      valueText: `${(distanceMeters / 1000).toFixed(distanceMeters >= 10000 ? 0 : 1)}`,
      unitText: 'km',
    }
    : {
      valueText: String(Math.round(distanceMeters)),
      unitText: 'm',
    }
}

function formatSpeedText(speedKmh: number | null): string {
  if (speedKmh === null || !Number.isFinite(speedKmh) || speedKmh < 0.05) {
    return '0'
  }

  return speedKmh >= 10 ? speedKmh.toFixed(1) : speedKmh.toFixed(2)
}

function smoothSpeedKmh(previousSpeedKmh: number | null, nextSpeedKmh: number): number {
  if (previousSpeedKmh === null || !Number.isFinite(previousSpeedKmh)) {
    return nextSpeedKmh
  }

  return previousSpeedKmh + (nextSpeedKmh - previousSpeedKmh) * SPEED_SMOOTHING_ALPHA
}

function resolveDevicePose(
  previousPose: DevicePose,
  accelerometer: TelemetryState['accelerometer'],
): DevicePose {
  if (!accelerometer) {
    return previousPose
  }

  const magnitude = Math.sqrt(
    accelerometer.x * accelerometer.x
      + accelerometer.y * accelerometer.y
      + accelerometer.z * accelerometer.z,
  )

  if (!Number.isFinite(magnitude) || magnitude <= 0.001) {
    return previousPose
  }

  const normalizedX = Math.abs(accelerometer.x / magnitude)
  const normalizedY = Math.abs(accelerometer.y / magnitude)
  const normalizedZ = Math.abs(accelerometer.z / magnitude)
  const verticalAxis = Math.max(normalizedX, normalizedY)

  const withinFlatEnter = normalizedZ >= DEVICE_POSE_FLAT_ENTER_Z
  const withinFlatExit = normalizedZ >= DEVICE_POSE_FLAT_EXIT_Z
  const withinUprightEnter = normalizedZ <= DEVICE_POSE_UPRIGHT_ENTER_Z && verticalAxis >= DEVICE_POSE_UPRIGHT_AXIS_ENTER
  const withinUprightExit = normalizedZ <= DEVICE_POSE_UPRIGHT_EXIT_Z && verticalAxis >= DEVICE_POSE_UPRIGHT_AXIS_EXIT

  if (previousPose === 'flat') {
    if (withinFlatExit) {
      return 'flat'
    }

    if (withinUprightEnter) {
      return 'upright'
    }

    return 'tilted'
  }

  if (previousPose === 'upright') {
    if (withinUprightExit) {
      return 'upright'
    }

    if (withinFlatEnter) {
      return 'flat'
    }

    return 'tilted'
  }

  if (withinFlatEnter) {
    return 'flat'
  }

  if (withinUprightEnter) {
    return 'upright'
  }

  return 'tilted'
}

function resolveHeadingConfidence(
  headingDeg: number | null,
  pose: DevicePose,
  gyroscope: TelemetryState['gyroscope'],
): HeadingConfidence {
  if (headingDeg === null || pose === 'flat') {
    return 'low'
  }

  if (!gyroscope) {
    return pose === 'upright' ? 'medium' : 'low'
  }

  const turnRate = Math.sqrt(
    gyroscope.x * gyroscope.x
      + gyroscope.y * gyroscope.y
      + gyroscope.z * gyroscope.z,
  )

  if (turnRate <= HEADING_CONFIDENCE_HIGH_TURN_RATE_RAD) {
    return pose === 'upright' ? 'high' : 'medium'
  }

  if (turnRate <= HEADING_CONFIDENCE_MEDIUM_TURN_RATE_RAD) {
    return 'medium'
  }

  return 'low'
}

function getHeartRateTone(
  heartRateBpm: number | null,
  telemetryConfig: TelemetryConfig,
): HeartRateTone {
  if (heartRateBpm === null || !Number.isFinite(heartRateBpm) || heartRateBpm <= 0) {
    return 'blue'
  }

  const maxHeartRate = Math.max(120, 220 - telemetryConfig.heartRateAge)
  const restingHeartRate = Math.min(maxHeartRate - 15, telemetryConfig.restingHeartRateBpm)
  const reserve = Math.max(20, maxHeartRate - restingHeartRate)
  const blueLimit = restingHeartRate + reserve * 0.39
  const purpleLimit = restingHeartRate + reserve * 0.54
  const greenLimit = restingHeartRate + reserve * 0.69
  const yellowLimit = restingHeartRate + reserve * 0.79
  const orangeLimit = restingHeartRate + reserve * 0.89

  if (heartRateBpm <= blueLimit) {
    return 'blue'
  }

  if (heartRateBpm <= purpleLimit) {
    return 'purple'
  }

  if (heartRateBpm <= greenLimit) {
    return 'green'
  }

  if (heartRateBpm <= yellowLimit) {
    return 'yellow'
  }

  if (heartRateBpm <= orangeLimit) {
    return 'orange'
  }

  return 'red'
}

function getSpeedFallbackTone(speedKmh: number | null): HeartRateTone {
  if (speedKmh === null || !Number.isFinite(speedKmh) || speedKmh < 3.2) {
    return 'blue'
  }

  if (speedKmh <= 4.0) {
    return 'purple'
  }

  if (speedKmh <= 5.5) {
    return 'green'
  }

  if (speedKmh <= 7.1) {
    return 'yellow'
  }

  if (speedKmh <= 8.8) {
    return 'orange'
  }

  return 'red'
}

function formatHeartRateMetric(heartRateBpm: number | null): { valueText: string; unitText: string } {
  if (heartRateBpm === null || !Number.isFinite(heartRateBpm) || heartRateBpm <= 0) {
    return {
      valueText: '--',
      unitText: '',
    }
  }

  return {
    valueText: String(Math.round(heartRateBpm)),
    unitText: 'bpm',
  }
}

function formatCaloriesMetric(caloriesKcal: number | null): { valueText: string; unitText: string } {
  if (caloriesKcal === null || !Number.isFinite(caloriesKcal) || caloriesKcal < 0) {
    return {
      valueText: '0',
      unitText: 'kcal',
    }
  }

  return {
    valueText: String(Math.round(caloriesKcal)),
    unitText: 'kcal',
  }
}

function formatAccuracyMetric(accuracyMeters: number | null): { valueText: string; unitText: string } {
  if (accuracyMeters === null || !Number.isFinite(accuracyMeters) || accuracyMeters < 0) {
    return {
      valueText: '--',
      unitText: '',
    }
  }

  return {
    valueText: String(Math.round(accuracyMeters)),
    unitText: 'm',
  }
}

function estimateCaloriesKcal(
  elapsedMs: number,
  heartRateBpm: number,
  telemetryConfig: TelemetryConfig,
): number {
  if (elapsedMs <= 0) {
    return 0
  }

  if (!Number.isFinite(heartRateBpm) || heartRateBpm <= 0) {
    return 0
  }

  const maxHeartRate = Math.max(120, 220 - telemetryConfig.heartRateAge)
  const restingHeartRate = Math.min(maxHeartRate - 15, telemetryConfig.restingHeartRateBpm)
  const reserve = Math.max(20, maxHeartRate - restingHeartRate)
  const intensity = Math.max(0, Math.min(1, (heartRateBpm - restingHeartRate) / reserve))
  const met = 2 + intensity * 10

  return met * telemetryConfig.userWeightKg * (elapsedMs / 3600000)
}

function estimateCaloriesFromSpeedKcal(
  elapsedMs: number,
  speedKmh: number | null,
  telemetryConfig: TelemetryConfig,
): number {
  if (elapsedMs <= 0 || speedKmh === null || !Number.isFinite(speedKmh) || speedKmh < 0.5) {
    return 0
  }

  let met = 2
  if (speedKmh >= 8.9) {
    met = 9.8
  } else if (speedKmh >= 7.2) {
    met = 7.8
  } else if (speedKmh >= 5.6) {
    met = 6
  } else if (speedKmh >= 4.1) {
    met = 4.3
  } else if (speedKmh >= 3.2) {
    met = 3.0
  }

  return (met * 3.5 * telemetryConfig.userWeightKg / 200) * (elapsedMs / 60000)
}

function hasHeartRateSignal(state: TelemetryState): boolean {
  return state.heartRateBpm !== null
    && Number.isFinite(state.heartRateBpm)
    && state.heartRateBpm > 0
}

function hasSpeedSignal(state: TelemetryState): boolean {
  return state.currentSpeedKmh !== null
    && Number.isFinite(state.currentSpeedKmh)
    && state.currentSpeedKmh >= 0.5
}

function shouldTrackCalories(state: TelemetryState): boolean {
  return state.sessionStatus === 'running'
    && state.sessionEndedAt === null
    && (hasHeartRateSignal(state) || hasSpeedSignal(state))
}

export class TelemetryRuntime {
  state: TelemetryState
  config: TelemetryConfig
  activityConfig: TelemetryConfig
  playerProfile: PlayerTelemetryProfile | null
  sessionCloseAfterMs: number
  sessionCloseWarningMs: number

  constructor() {
    this.state = { ...EMPTY_TELEMETRY_STATE }
    this.config = { ...DEFAULT_TELEMETRY_CONFIG }
    this.activityConfig = { ...DEFAULT_TELEMETRY_CONFIG }
    this.playerProfile = null
    this.sessionCloseAfterMs = 2 * 60 * 60 * 1000
    this.sessionCloseWarningMs = 10 * 60 * 1000
  }

  reset(): void {
      this.state = {
        ...EMPTY_TELEMETRY_STATE,
        accelerometer: this.state.accelerometer,
        accelerometerUpdatedAt: this.state.accelerometerUpdatedAt,
        accelerometerSampleCount: this.state.accelerometerSampleCount,
        gyroscope: this.state.gyroscope,
        deviceMotion: this.state.deviceMotion,
        deviceHeadingDeg: this.state.deviceHeadingDeg,
      devicePose: this.state.devicePose,
      headingConfidence: this.state.headingConfidence,
    }
  }

  configure(config?: Partial<TelemetryConfig> | null): void {
    this.activityConfig = mergeTelemetrySources(config, null)
    this.syncEffectiveConfig()
  }

  applyCompiledProfile(
    config: TelemetryConfig,
    playerProfile?: PlayerTelemetryProfile | null,
  ): void {
    this.activityConfig = { ...config }
    this.playerProfile = playerProfile ? { ...playerProfile } : null
    this.config = { ...config }
  }

  setPlayerProfile(profile?: PlayerTelemetryProfile | null): void {
    this.playerProfile = profile ? { ...profile } : null
    this.syncEffectiveConfig()
  }

  clearPlayerProfile(): void {
    this.playerProfile = null
    this.syncEffectiveConfig()
  }

  exportRecoveryState(): RecoveryTelemetrySnapshot {
    return {
      distanceMeters: this.state.distanceMeters,
      currentSpeedKmh: this.state.currentSpeedKmh,
      averageSpeedKmh: this.state.averageSpeedKmh,
      heartRateBpm: this.state.heartRateBpm,
      caloriesKcal: this.state.caloriesKcal,
      lastGpsPoint: this.state.lastGpsPoint
        ? {
          lon: this.state.lastGpsPoint.lon,
          lat: this.state.lastGpsPoint.lat,
        }
        : null,
      lastGpsAt: this.state.lastGpsAt,
      lastGpsAccuracyMeters: this.state.lastGpsAccuracyMeters,
    }
  }

  restoreRecoveryState(
    definition: GameDefinition,
    gameState: GameSessionState,
    snapshot: RecoveryTelemetrySnapshot,
    hudTargetControlId?: string | null,
  ): void {
    const targetControlId = hudTargetControlId || null
    const targetControl = targetControlId
      ? definition.controls.find((control) => control.id === targetControlId) || null
      : null

    this.sessionCloseAfterMs = definition.sessionCloseAfterMs
    this.sessionCloseWarningMs = definition.sessionCloseWarningMs
    this.state = {
      ...EMPTY_TELEMETRY_STATE,
      accelerometer: this.state.accelerometer,
      accelerometerUpdatedAt: this.state.accelerometerUpdatedAt,
      accelerometerSampleCount: this.state.accelerometerSampleCount,
      gyroscope: this.state.gyroscope,
      deviceMotion: this.state.deviceMotion,
      deviceHeadingDeg: this.state.deviceHeadingDeg,
      devicePose: this.state.devicePose,
      headingConfidence: this.state.headingConfidence,
      sessionStatus: gameState.status,
      sessionStartedAt: gameState.startedAt,
      sessionEndedAt: gameState.endedAt,
      elapsedMs: gameState.startedAt === null
        ? 0
        : Math.max(0, ((gameState.endedAt || Date.now()) - gameState.startedAt)),
      distanceMeters: snapshot.distanceMeters,
      currentSpeedKmh: snapshot.currentSpeedKmh,
      averageSpeedKmh: snapshot.averageSpeedKmh,
      distanceToTargetMeters: targetControl && snapshot.lastGpsPoint
        ? getApproxDistanceMeters(snapshot.lastGpsPoint, targetControl.point)
        : null,
      targetControlId: targetControl ? targetControl.id : null,
      targetPoint: targetControl ? targetControl.point : null,
      lastGpsPoint: snapshot.lastGpsPoint
        ? {
          lon: snapshot.lastGpsPoint.lon,
          lat: snapshot.lastGpsPoint.lat,
        }
        : null,
      lastGpsAt: snapshot.lastGpsAt,
      lastGpsAccuracyMeters: snapshot.lastGpsAccuracyMeters,
      heartRateBpm: snapshot.heartRateBpm,
      caloriesKcal: snapshot.caloriesKcal,
      calorieTrackingAt: snapshot.lastGpsAt,
    }
    this.recomputeDerivedState()
  }

  loadDefinition(_definition: GameDefinition): void {
    this.sessionCloseAfterMs = _definition.sessionCloseAfterMs
    this.sessionCloseWarningMs = _definition.sessionCloseWarningMs
    this.reset()
  }

  syncGameState(definition: GameDefinition | null, state: GameSessionState | null, hudTargetControlId?: string | null): void {
    if (!definition || !state) {
      this.dispatch({ type: 'reset' })
      return
    }

    const targetControlId = hudTargetControlId !== undefined ? hudTargetControlId : state.currentTargetControlId
    const targetControl = targetControlId
      ? definition.controls.find((control) => control.id === targetControlId) || null
      : null

    this.dispatch({
      type: 'session_state_updated',
      at: Date.now(),
      status: state.status,
      startedAt: state.startedAt,
      endedAt: state.endedAt,
    })
    this.dispatch({
      type: 'target_updated',
      controlId: targetControl ? targetControl.id : null,
      point: targetControl ? targetControl.point : null,
    })
  }

  dispatch(event: TelemetryEvent): void {
    if (event.type === 'reset') {
      this.reset()
      return
    }

    if (event.type === 'session_state_updated') {
      this.syncCalorieAccumulation(event.at)
      this.state = {
        ...this.state,
        sessionStatus: event.status,
        sessionStartedAt: event.startedAt,
        sessionEndedAt: event.endedAt,
        elapsedMs: event.startedAt === null ? 0 : Math.max(0, (event.endedAt || Date.now()) - event.startedAt),
      }
      this.alignCalorieTracking(event.at)
      this.recomputeDerivedState()
      return
    }

    if (event.type === 'target_updated') {
      this.state = {
        ...this.state,
        targetControlId: event.controlId,
        targetPoint: event.point,
      }
      this.recomputeDerivedState()
      return
    }

    if (event.type === 'gps_updated') {
      this.syncCalorieAccumulation(event.at)
      const nextPoint = { lon: event.lon, lat: event.lat }
      const previousPoint = this.state.lastGpsPoint
      const previousAt = this.state.lastGpsAt
      let nextDistanceMeters = this.state.distanceMeters
      let nextSpeedKmh = this.state.currentSpeedKmh

      if (previousPoint && previousAt !== null && event.at > previousAt) {
        const segmentMeters = getApproxDistanceMeters(previousPoint, nextPoint)
        nextDistanceMeters += segmentMeters
        const rawSpeedKmh = segmentMeters <= 0
          ? 0
          : (segmentMeters / ((event.at - previousAt) / 1000)) * 3.6
        nextSpeedKmh = smoothSpeedKmh(this.state.currentSpeedKmh, rawSpeedKmh)
      }

      this.state = {
        ...this.state,
        distanceMeters: nextDistanceMeters,
        currentSpeedKmh: nextSpeedKmh,
        lastGpsPoint: nextPoint,
        lastGpsAt: event.at,
        lastGpsAccuracyMeters: event.accuracyMeters,
      }
      this.alignCalorieTracking(event.at)
      this.recomputeDerivedState()
      return
    }

    if (event.type === 'accelerometer_updated') {
      const previous = this.state.accelerometer
        this.state = {
          ...this.state,
          accelerometer: previous === null
          ? {
            x: event.x,
            y: event.y,
            z: event.z,
          }
            : {
              x: previous.x + (event.x - previous.x) * ACCELEROMETER_SMOOTHING_ALPHA,
              y: previous.y + (event.y - previous.y) * ACCELEROMETER_SMOOTHING_ALPHA,
              z: previous.z + (event.z - previous.z) * ACCELEROMETER_SMOOTHING_ALPHA,
            },
          accelerometerUpdatedAt: event.at,
          accelerometerSampleCount: this.state.accelerometerSampleCount + 1,
        }
        this.recomputeDerivedState()
        return
    }

    if (event.type === 'gyroscope_updated') {
      this.state = {
        ...this.state,
        gyroscope: {
          x: event.x,
          y: event.y,
          z: event.z,
        },
      }
      this.recomputeDerivedState()
      return
    }

    if (event.type === 'device_motion_updated') {
      const motionHeadingDeg = resolveMotionCompassHeadingDeg(event.alpha, event.beta, event.gamma)
      const nextDeviceHeadingDeg = motionHeadingDeg === null
        ? this.state.deviceHeadingDeg
        : (() => {
          return this.state.deviceHeadingDeg === null
            ? motionHeadingDeg
            : interpolateHeadingDeg(this.state.deviceHeadingDeg, motionHeadingDeg, DEVICE_HEADING_SMOOTHING_ALPHA)
        })()

      this.state = {
        ...this.state,
        deviceMotion: {
          alpha: event.alpha,
          beta: event.beta,
          gamma: event.gamma,
        },
        deviceHeadingDeg: nextDeviceHeadingDeg,
      }
      this.recomputeDerivedState()
      return
    }

    if (event.type === 'heart_rate_updated') {
      this.syncCalorieAccumulation(event.at)
      this.state = {
        ...this.state,
        heartRateBpm: event.bpm,
      }
      this.alignCalorieTracking(event.at)
      this.recomputeDerivedState()
    }
  }

  recomputeDerivedState(now = Date.now()): void {
    const elapsedMs = this.state.sessionStartedAt === null
      ? 0
      : Math.max(0, (this.state.sessionEndedAt || now) - this.state.sessionStartedAt)
    const distanceToTargetMeters = this.state.lastGpsPoint && this.state.targetPoint
      ? getApproxDistanceMeters(this.state.lastGpsPoint, this.state.targetPoint)
      : null
    const averageSpeedKmh = elapsedMs > 0
      ? (this.state.distanceMeters / (elapsedMs / 1000)) * 3.6
      : null
    const devicePose = resolveDevicePose(this.state.devicePose, this.state.accelerometer)
    const headingConfidence = resolveHeadingConfidence(
      this.state.deviceHeadingDeg,
      devicePose,
      this.state.gyroscope,
    )

    this.state = {
      ...this.state,
      elapsedMs,
      distanceToTargetMeters,
      averageSpeedKmh,
      devicePose,
      headingConfidence,
    }
  }

  getPresentation(now = Date.now()): TelemetryPresentation {
    this.syncCalorieAccumulation(now)
    this.alignCalorieTracking(now)
    this.recomputeDerivedState(now)
    const elapsedTimerText = formatElapsedTimerText(this.state.elapsedMs)
    const countdownActive = this.state.sessionStatus === 'running'
      && this.state.sessionEndedAt === null
      && this.state.sessionStartedAt !== null
      && this.sessionCloseAfterMs > 0
      && (this.sessionCloseAfterMs - this.state.elapsedMs) <= this.sessionCloseWarningMs
    const countdownRemainingMs = countdownActive
      ? Math.max(0, this.sessionCloseAfterMs - this.state.elapsedMs)
      : 0
    const targetDistance = formatTargetDistance(this.state.distanceToTargetMeters)
    const hasHeartRate = hasHeartRateSignal(this.state)
    const heartRateTone = hasHeartRate
      ? getHeartRateTone(this.state.heartRateBpm, this.config)
      : getSpeedFallbackTone(this.state.currentSpeedKmh)
    const heartRate = formatHeartRateMetric(this.state.heartRateBpm)
    const calories = formatCaloriesMetric(this.state.caloriesKcal)
    const accuracy = formatAccuracyMetric(this.state.lastGpsAccuracyMeters)

    return {
      ...EMPTY_TELEMETRY_PRESENTATION,
      timerText: countdownActive ? formatCountdownTimerText(countdownRemainingMs) : elapsedTimerText,
      elapsedTimerText,
      timerMode: countdownActive ? 'countdown' : 'elapsed',
      mileageText: formatDistanceText(this.state.distanceMeters),
      distanceToTargetValueText: targetDistance.valueText,
      distanceToTargetUnitText: targetDistance.unitText,
      speedText: formatSpeedText(this.state.currentSpeedKmh),
      heartRateTone,
      heartRateZoneNameText: hasHeartRate || hasSpeedSignal(this.state) ? getHeartRateToneLabel(heartRateTone) : '--',
      heartRateZoneRangeText: hasHeartRate
        ? getHeartRateToneRangeText(heartRateTone)
        : hasSpeedSignal(this.state)
          ? getSpeedToneRangeText(heartRateTone)
          : '',
      heartRateValueText: heartRate.valueText,
      heartRateUnitText: heartRate.unitText,
      caloriesValueText: calories.valueText,
      caloriesUnitText: calories.unitText,
      averageSpeedValueText: formatSpeedText(this.state.averageSpeedKmh),
      averageSpeedUnitText: 'km/h',
      accuracyValueText: accuracy.valueText,
      accuracyUnitText: accuracy.unitText,
    }
  }

  private syncCalorieAccumulation(now: number): void {
    if (!shouldTrackCalories(this.state)) {
      return
    }

    if (this.state.calorieTrackingAt === null) {
      this.state = {
        ...this.state,
        calorieTrackingAt: now,
        caloriesKcal: this.state.caloriesKcal === null ? 0 : this.state.caloriesKcal,
      }
      return
    }

    if (now <= this.state.calorieTrackingAt) {
      return
    }

    const deltaMs = now - this.state.calorieTrackingAt
    const calorieDelta = hasHeartRateSignal(this.state)
      ? estimateCaloriesKcal(deltaMs, this.state.heartRateBpm as number, this.config)
      : estimateCaloriesFromSpeedKcal(deltaMs, this.state.currentSpeedKmh, this.config)

    this.state = {
      ...this.state,
      calorieTrackingAt: now,
      caloriesKcal: (this.state.caloriesKcal === null ? 0 : this.state.caloriesKcal) + calorieDelta,
    }
  }

  private alignCalorieTracking(now: number): void {
    if (shouldTrackCalories(this.state)) {
      if (this.state.calorieTrackingAt === null) {
        this.state = {
          ...this.state,
          calorieTrackingAt: now,
          caloriesKcal: this.state.caloriesKcal === null ? 0 : this.state.caloriesKcal,
        }
      }
      return
    }

    if (this.state.calorieTrackingAt !== null) {
      this.state = {
        ...this.state,
        calorieTrackingAt: null,
        caloriesKcal: this.state.caloriesKcal === null ? 0 : this.state.caloriesKcal,
      }
    }
  }

  private syncEffectiveConfig(): void {
    this.config = mergeTelemetrySources(this.activityConfig, this.playerProfile)
  }
}
