package com.gametime.watch.bridge

import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.location.LocationManager
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.health.services.client.ExerciseClient
import androidx.health.services.client.ExerciseUpdateCallback
import androidx.health.services.client.HealthServices
import androidx.health.services.client.MeasureClient
import androidx.health.services.client.MeasureCallback
import androidx.health.services.client.data.Availability
import androidx.health.services.client.data.DataPointContainer
import androidx.health.services.client.data.DataType
import androidx.health.services.client.data.DeltaDataType
import androidx.health.services.client.data.ExerciseConfig
import androidx.health.services.client.data.ExerciseEvent
import androidx.health.services.client.data.ExerciseLapSummary
import androidx.health.services.client.data.ExerciseType
import com.google.android.gms.wearable.CapabilityClient
import com.google.android.gms.wearable.Node
import com.google.android.gms.wearable.Wearable
import org.json.JSONObject
import java.nio.charset.StandardCharsets
import kotlin.math.roundToInt

private const val MAX_DISTANCE_SECURITY_RETRIES = 3

internal class WatchHeartRateCollector(
    private val phoneCapability: String,
    private val sensorSummaryPath: String,
    private val sensorSamplePath: String,
    private val onLocalSample: (Map<String, Any?>) -> Unit = {},
) {
    private companion object {
        const val TAG = "GTWatchHeartRate"
        const val SAMPLE_FLUSH_INTERVAL_MS = 1500L
        const val SAMPLE_STALE_TIMEOUT_MS = 20000L
        const val SAMPLE_WATCHDOG_INTERVAL_MS = 5000L
        const val DISTANCE_RETRY_DELAY_MS = 30000L
        const val NODE_CACHE_TTL_MS = 10000L
    }

    private val mainHandler = Handler(Looper.getMainLooper())
    private var pendingSample: Map<String, Any?>? = null
    private var sampleFlushRunnable: Runnable? = null
    private var sampleWatchdogRunnable: Runnable? = null
    private var distanceRetryRunnable: Runnable? = null
    private var cachedReachableNodes: List<Node> = emptyList()
    private var cachedReachableNodesAtEpochMs = 0L
    private var nodeLookupInFlight = false
    private var sessionId: String? = null
    private var sampleCount = 0
    private var sampleSum = 0.0
    private var minBpm: Int? = null
    private var maxBpm: Int? = null
    private var distanceMeters: Double? = null
    private var caloriesKcal: Double? = null
    private var sampleSequence = 0
    private var executionContext: Map<String, Any?> = emptyMap()
    private val registeredDataTypes = mutableSetOf<DeltaDataType<*, *>>()
    private var exerciseMetricsStarted = false
    private var exerciseMetricsStartInFlight = false
    private var exerciseHeartRateSupported = false
    private var exerciseHeartRateObserved = false
    private var distanceSecurityFailureCount = 0
    private var shouldRetryDistanceAfterSecurityFailure = false
    private var shouldAggregate = false
    private var latestSampleAtEpochMs = 0L
    private var appContext: Context? = null

    private val measureCallback = object : MeasureCallback {
        override fun onAvailabilityChanged(
            dataType: DeltaDataType<*, *>,
            availability: Availability,
        ) {
            Log.d(TAG, "availability dataType=$dataType availability=$availability")
        }

        override fun onDataReceived(data: DataPointContainer) {
            if (!shouldAggregate) {
                return
            }
            var latestHeartRateBpm: Int? = null
            for (point in data.getData(DataType.HEART_RATE_BPM)) {
                latestHeartRateBpm = recordHeartRate(point.value)
            }
            if (latestHeartRateBpm != null) {
                logHotPath(
                    "heart rate data received sessionId=$sessionId bpm=$latestHeartRateBpm",
                )
                sendSample(latestHeartRateBpm)
            }
        }

        override fun onRegistrationFailed(throwable: Throwable) {
            Log.w(TAG, "measure callback registration failed", throwable)
        }
    }

    private val exerciseCallback = object : ExerciseUpdateCallback {
        override fun onRegistered() {
            Log.d(TAG, "exercise update callback registered sessionId=$sessionId")
        }

        override fun onRegistrationFailed(throwable: Throwable) {
            Log.w(TAG, "exercise update callback registration failed", throwable)
        }

        override fun onExerciseUpdateReceived(update: androidx.health.services.client.data.ExerciseUpdate) {
            if (!shouldAggregate) {
                return
            }
            var updated = false
            var latestHeartRateBpm: Int? = null
            for (point in update.latestMetrics.getData(DataType.HEART_RATE_BPM)) {
                latestHeartRateBpm = recordHeartRate(point.value)
            }
            if (latestHeartRateBpm != null && !exerciseHeartRateObserved) {
                exerciseHeartRateObserved = true
                appContext?.let(::unregister)
            }
            for (point in update.latestMetrics.getData(DataType.DISTANCE)) {
                val value = point.value
                if (value > 0) {
                    distanceMeters = (distanceMeters ?: 0.0) + value
                    updated = true
                }
            }
            for (point in update.latestMetrics.getData(DataType.CALORIES)) {
                val value = point.value
                if (value > 0) {
                    caloriesKcal = (caloriesKcal ?: 0.0) + value
                    updated = true
                }
            }
            if (latestHeartRateBpm != null || updated) {
                logHotPath(
                    "exercise metrics received sessionId=$sessionId bpm=$latestHeartRateBpm distance=$distanceMeters calories=$caloriesKcal",
                )
                sendSample(latestHeartRateBpm)
            }
        }

        override fun onLapSummaryReceived(lapSummary: ExerciseLapSummary) {}

        override fun onAvailabilityChanged(
            dataType: androidx.health.services.client.data.DataType<*, *>,
            availability: Availability,
        ) {
            Log.d(TAG, "exercise availability dataType=$dataType availability=$availability")
        }

        override fun onExerciseEventReceived(event: ExerciseEvent) {
            Log.d(TAG, "exercise event sessionId=$sessionId event=$event")
        }
    }

    fun noteActiveSession(
        nextSessionId: String,
        shouldAggregate: Boolean,
        executionContext: Map<String, Any?> = emptyMap(),
    ) {
        if (sessionId != nextSessionId) {
            reset(nextSessionId)
        }
        this.executionContext = executionContext
        this.shouldAggregate = shouldAggregate
    }

    fun onBodySensorsGranted(context: Context) {
        if (shouldAggregate) {
            start(context)
        }
    }

    fun onPhoneReconnected(context: Context) {
        appContext = context.applicationContext
        cachedReachableNodes = emptyList()
        cachedReachableNodesAtEpochMs = 0L
        flushPendingSample(context, forceNodeRefresh = true)
    }

    fun start(context: Context) {
        if (sessionId.isNullOrBlank()) {
            return
        }
        appContext = context.applicationContext
        if (latestSampleAtEpochMs == 0L) {
            latestSampleAtEpochMs = System.currentTimeMillis()
        }
        scheduleSampleWatchdog(context)
        startMeasureHeartRateFallback(context)
        startExerciseMetrics(context)
    }

    fun pause(context: Context) {
        shouldAggregate = false
        flushPendingSample(context, forceNodeRefresh = false)
        cancelSampleWatchdog()
        cancelDistanceRetry()
        unregister(context)
        stopExerciseMetrics(context)
    }

    fun finishCurrentSession(context: Context) {
        flushPendingSample(context, forceNodeRefresh = false)
        unregister(context)
        stopExerciseMetrics(context)
        val completedSessionId = sessionId
        if (!completedSessionId.isNullOrBlank() && sampleCount >= 3) {
            sendSummary(context, completedSessionId)
        } else {
            Log.d(
                TAG,
                "skip summary sessionId=$completedSessionId sampleCount=$sampleCount",
            )
        }
        reset(null)
    }

    private fun recordHeartRate(bpm: Double): Int? {
        if (bpm <= 0) {
            return null
        }
        sampleCount += 1
        sampleSum += bpm
        val rounded = bpm.roundToInt()
        minBpm = minOf(minBpm ?: rounded, rounded)
        maxBpm = maxOf(maxBpm ?: rounded, rounded)
        return rounded
    }

    private fun sendSample(bpm: Int?) {
        val activeSessionId = sessionId ?: return
        val context = appContext ?: return
        sampleSequence += 1
        val capturedAt = System.currentTimeMillis()
        latestSampleAtEpochMs = capturedAt
        val sample = mapOf(
            "schemaVersion" to 4,
            "sampleId" to "$activeSessionId-$capturedAt-$sampleSequence",
            "sessionId" to activeSessionId,
            "capturedAtEpochMs" to capturedAt,
            "recordedAtEpochMs" to capturedAt,
            "programIndex" to executionContext["programIndex"],
            "exerciseIndex" to executionContext["exerciseIndex"],
            "setIndex" to executionContext["setIndex"],
            "passageIndex" to executionContext["passageIndex"],
            "stepIndex" to executionContext["stepIndex"],
            "heartRateBpm" to bpm,
            "distanceMeters" to distanceMeters,
            "caloriesKcal" to caloriesKcal,
        )
        onLocalSample(sample)
        pendingSample = sample
        scheduleSampleFlush(context)
    }

    private fun sendSummary(context: Context, completedSessionId: String) {
        val min = minBpm ?: return
        val max = maxBpm ?: return
        val payload = JSONObject(
            mapOf(
                "schemaVersion" to 4,
                "sessionId" to completedSessionId,
                "sampleCount" to sampleCount,
                "minHeartRateBpm" to min,
                "averageHeartRateBpm" to sampleSum / sampleCount,
                "maxHeartRateBpm" to max,
                "totalDistanceMeters" to distanceMeters,
                "totalCaloriesKcal" to caloriesKcal,
            ),
        ).toString().toByteArray(StandardCharsets.UTF_8)
        Wearable.getCapabilityClient(context)
            .getCapability(phoneCapability, CapabilityClient.FILTER_REACHABLE)
            .addOnSuccessListener { capability ->
                cacheReachableNodes(capability.nodes.toList())
                Log.d(
                    TAG,
                    "send summary sessionId=$completedSessionId samples=$sampleCount nodes=${capability.nodes.size}",
                )
                for (node in capability.nodes) {
                    Wearable.getMessageClient(context)
                        .sendMessage(node.id, sensorSummaryPath, payload)
                }
            }
            .addOnFailureListener { error ->
                Log.w(TAG, "summary capability lookup failed", error)
            }
    }

    private fun unregister(context: Context) {
        if (registeredDataTypes.isEmpty()) {
            return
        }
        val measureClient = HealthServices.getClient(context).measureClient
        for (dataType in registeredDataTypes.toList()) {
            measureClient.unregisterMeasureCallbackAsync(dataType, measureCallback)
        }
        registeredDataTypes.clear()
    }

    private fun registerMeasureCallbackIfNeeded(
        measureClient: MeasureClient,
        dataType: DeltaDataType<*, *>,
    ) {
        if (registeredDataTypes.contains(dataType)) {
            return
        }
        try {
            measureClient.registerMeasureCallback(dataType, measureCallback)
            registeredDataTypes.add(dataType)
            Log.d(TAG, "measure callback registered dataType=$dataType sessionId=$sessionId")
        } catch (error: RuntimeException) {
            Log.w(TAG, "measure callback registration threw dataType=$dataType", error)
        }
    }

    private fun startExerciseMetrics(context: Context) {
        if (exerciseMetricsStarted || exerciseMetricsStartInFlight) {
            return
        }
        val exerciseClient = HealthServices.getClient(context).exerciseClient
        exerciseMetricsStartInFlight = true
        val capabilitiesFuture = exerciseClient.getCapabilitiesAsync()
        capabilitiesFuture.addListener(
            {
                try {
                    val capabilities = capabilitiesFuture.get()
                    val requestedTypeNames = (
                        executionContext["healthServicesExerciseTypeStrategy"] as? List<*>
                    )?.filterIsInstance<String>().orEmpty()
                    val configs = exerciseConfigsFromCapabilities(
                        capabilities,
                        requestedTypeNames,
                        canUseGps = canStartGpsExercise(context),
                    )
                    if (configs.isEmpty()) {
                        exerciseMetricsStartInFlight = false
                        Log.w(TAG, "no exercise type supports usable live metrics sessionId=$sessionId")
                        if (shouldAggregate) {
                            startMeasureHeartRateFallback(context)
                        }
                        return@addListener
                    }
                    if (!shouldAggregate) {
                        exerciseMetricsStartInFlight = false
                        return@addListener
                    }
                    startExerciseMetricsConfig(
                        context,
                        exerciseClient,
                        configs,
                        index = 0,
                    )
                } catch (error: Exception) {
                    exerciseMetricsStartInFlight = false
                    Log.w(TAG, "exercise capabilities lookup failed", error)
                }
            },
            context.mainExecutor,
        )
    }

    private fun startExerciseMetricsConfig(
        context: Context,
        exerciseClient: ExerciseClient,
        configs: List<ExerciseConfig>,
        index: Int,
    ) {
        if (!shouldAggregate) {
            exerciseMetricsStartInFlight = false
            return
        }
        val config = configs.getOrNull(index)
        if (config == null) {
            exerciseMetricsStartInFlight = false
            clearExerciseCallback(exerciseClient)
            exerciseHeartRateSupported = false
            Log.w(TAG, "all exercise metric configs failed sessionId=$sessionId")
            if (shouldAggregate) {
                startMeasureHeartRateFallback(context)
            }
            return
        }
        exerciseHeartRateSupported = DataType.HEART_RATE_BPM in config.dataTypes
        exerciseClient.setUpdateCallback(context.mainExecutor, exerciseCallback)
        val startFuture = exerciseClient.startExerciseAsync(config)
        startFuture.addListener(
            {
                try {
                    startFuture.get()
                    exerciseMetricsStartInFlight = false
                    exerciseMetricsStarted = true
                    onExerciseMetricsStarted(context, config)
                    Log.d(
                        TAG,
                        "exercise metrics started sessionId=$sessionId type=${config.exerciseType} dataTypes=${config.dataTypes}",
                    )
                } catch (error: Exception) {
                    val failedFromFineLocationSecurity = WatchExerciseMetricsRetryPolicy
                        .isDistanceSecurityFailure(config.dataTypes, error)
                    if (failedFromFineLocationSecurity) {
                        distanceSecurityFailureCount += 1
                        shouldRetryDistanceAfterSecurityFailure = WatchExerciseMetricsRetryPolicy
                            .canRetryDistance(distanceSecurityFailureCount)
                        Log.w(
                            TAG,
                            "distance exercise metrics rejected by security " +
                                "type=${config.exerciseType} attempt=$distanceSecurityFailureCount " +
                                "willRetry=$shouldRetryDistanceAfterSecurityFailure",
                            error,
                        )
                    }
                    Log.w(
                        TAG,
                        "exercise metrics start failed type=${config.exerciseType} dataTypes=${config.dataTypes}",
                        error,
                    )
                    clearExerciseCallback(exerciseClient)
                    exerciseHeartRateSupported = false
                    startExerciseMetricsConfig(
                        context,
                        exerciseClient,
                        configs,
                        index + 1,
                    )
                }
            },
            context.mainExecutor,
        )
    }

    private fun onExerciseMetricsStarted(context: Context, config: ExerciseConfig) {
        if (DataType.DISTANCE in config.dataTypes) {
            distanceSecurityFailureCount = 0
            shouldRetryDistanceAfterSecurityFailure = false
            cancelDistanceRetry()
            return
        }
        if (shouldRetryDistanceAfterSecurityFailure) {
            scheduleDistanceRetry(context)
        }
    }

    private fun exerciseConfigsFromCapabilities(
        capabilities: androidx.health.services.client.data.ExerciseCapabilities,
        requestedTypeNames: List<String>,
        canUseGps: Boolean,
    ): List<ExerciseConfig> {
        val requestedTypes = exerciseTypesFromNames(requestedTypeNames)
        val distanceCaloriesConfigs = mutableListOf<ExerciseConfig>()
        val caloriesConfigs = mutableListOf<ExerciseConfig>()
        val heartRateConfigs = mutableListOf<ExerciseConfig>()
        for (exerciseType in requestedTypes) {
            if (exerciseType !in capabilities.supportedExerciseTypes) {
                continue
            }
            val supported = capabilities.getExerciseTypeCapabilities(exerciseType)
                .supportedDataTypes
            val supportsHeartRate = DataType.HEART_RATE_BPM in supported
            if (
                canUseGps &&
                DataType.DISTANCE in supported &&
                DataType.CALORIES in supported
            ) {
                val dataTypes = mutableSetOf<androidx.health.services.client.data.DataType<*, *>>(
                    DataType.DISTANCE,
                    DataType.CALORIES,
                )
                if (supportsHeartRate) {
                    dataTypes.add(DataType.HEART_RATE_BPM)
                }
                distanceCaloriesConfigs.add(
                    ExerciseConfig.builder(exerciseType)
                        .setDataTypes(dataTypes)
                        .setIsAutoPauseAndResumeEnabled(false)
                        .setIsGpsEnabled(true)
                        .build(),
                )
            }
            if (DataType.CALORIES in supported) {
                val dataTypes = mutableSetOf<androidx.health.services.client.data.DataType<*, *>>(
                    DataType.CALORIES,
                )
                if (supportsHeartRate) {
                    dataTypes.add(DataType.HEART_RATE_BPM)
                }
                caloriesConfigs.add(
                    ExerciseConfig.builder(exerciseType)
                        .setDataTypes(dataTypes)
                        .setIsAutoPauseAndResumeEnabled(false)
                        .setIsGpsEnabled(false)
                        .build(),
                )
            } else if (supportsHeartRate) {
                heartRateConfigs.add(
                    ExerciseConfig.builder(exerciseType)
                        .setDataTypes(setOf(DataType.HEART_RATE_BPM))
                        .setIsAutoPauseAndResumeEnabled(false)
                        .setIsGpsEnabled(false)
                        .build(),
                )
            } else {
                Log.w(
                    TAG,
                    "exercise type lacks usable metrics type=$exerciseType supported=$supported",
                )
            }
        }
        return distanceCaloriesConfigs + caloriesConfigs + heartRateConfigs
    }

    private fun canStartGpsExercise(context: Context): Boolean {
        if (
            context.checkSelfPermission(android.Manifest.permission.ACCESS_FINE_LOCATION) !=
            PackageManager.PERMISSION_GRANTED
        ) {
            return false
        }
        return try {
            val locationManager = context.getSystemService(LocationManager::class.java)
                ?: return false
            locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) ||
                locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
        } catch (error: RuntimeException) {
            Log.w(TAG, "location availability check failed", error)
            false
        }
    }

    private fun exerciseTypesFromNames(names: List<String>): List<ExerciseType> {
        val mapped = names.mapNotNull { name ->
            when (name) {
                "RUNNING" -> ExerciseType.RUNNING
                "WALKING" -> ExerciseType.WALKING
                "HIGH_INTENSITY_INTERVAL_TRAINING" ->
                    ExerciseType.HIGH_INTENSITY_INTERVAL_TRAINING
                "WORKOUT" -> ExerciseType.WORKOUT
                else -> null
            }
        }.toMutableList()
        val fallback = listOf(
            ExerciseType.RUNNING,
            ExerciseType.WALKING,
            ExerciseType.HIGH_INTENSITY_INTERVAL_TRAINING,
            ExerciseType.WORKOUT,
        )
        for (exerciseType in fallback) {
            if (exerciseType !in mapped) {
                mapped.add(exerciseType)
            }
        }
        return mapped.distinct()
    }

    private fun stopExerciseMetrics(context: Context) {
        if (!exerciseMetricsStarted && !exerciseMetricsStartInFlight) {
            return
        }
        val exerciseClient = HealthServices.getClient(context).exerciseClient
        clearExerciseCallback(exerciseClient)
        if (exerciseMetricsStarted) {
            exerciseClient.endExerciseAsync()
        }
        exerciseMetricsStarted = false
        exerciseMetricsStartInFlight = false
        exerciseHeartRateSupported = false
    }

    private fun retryDistanceMetrics(context: Context) {
        val activeSessionId = sessionId
        if (
            activeSessionId.isNullOrBlank() ||
            !shouldAggregate ||
            !shouldRetryDistanceAfterSecurityFailure ||
            exerciseMetricsStartInFlight
        ) {
            return
        }
        Log.w(
            TAG,
            "retrying distance exercise metrics sessionId=$activeSessionId attempt=$distanceSecurityFailureCount",
        )
        stopExerciseMetrics(context)
        startExerciseMetrics(context)
    }

    private fun restartExerciseMetrics(context: Context) {
        val activeSessionId = sessionId
        if (activeSessionId.isNullOrBlank() || !shouldAggregate) {
            return
        }
        Log.w(TAG, "sample watchdog restarting exercise metrics sessionId=$activeSessionId")
        stopExerciseMetrics(context)
        unregister(context)
        latestSampleAtEpochMs = System.currentTimeMillis()
        startMeasureHeartRateFallback(context)
        startExerciseMetrics(context)
    }

    private fun clearExerciseCallback(exerciseClient: ExerciseClient) {
        try {
            exerciseClient.clearUpdateCallbackAsync(exerciseCallback)
        } catch (error: RuntimeException) {
            Log.w(TAG, "clear exercise update callback failed", error)
        }
    }

    private fun reset(nextSessionId: String?) {
        sampleFlushRunnable?.let { mainHandler.removeCallbacks(it) }
        sampleFlushRunnable = null
        pendingSample = null
        sessionId = nextSessionId
        sampleCount = 0
        sampleSum = 0.0
        minBpm = null
        maxBpm = null
        distanceMeters = null
        caloriesKcal = null
        sampleSequence = 0
        latestSampleAtEpochMs = 0L
        executionContext = emptyMap()
        shouldAggregate = false
        cancelSampleWatchdog()
        cancelDistanceRetry()
        exerciseMetricsStarted = false
        exerciseMetricsStartInFlight = false
        exerciseHeartRateSupported = false
        exerciseHeartRateObserved = false
        distanceSecurityFailureCount = 0
        shouldRetryDistanceAfterSecurityFailure = false
    }

    private fun startMeasureHeartRateFallback(context: Context) {
        if (exerciseHeartRateSupported) {
            return
        }
        val measureClient = HealthServices.getClient(context).measureClient
        registerMeasureCallbackIfNeeded(measureClient, DataType.HEART_RATE_BPM)
        Log.d(TAG, "heart rate fallback MeasureClient active sessionId=$sessionId")
    }

    private fun logHotPath(message: String) {
        val context = appContext ?: return
        if ((context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0) {
            Log.d(TAG, message)
        }
    }

    private fun scheduleSampleFlush(context: Context) {
        if (sampleFlushRunnable != null) {
            return
        }
        val appContext = context.applicationContext
        sampleFlushRunnable = Runnable {
            sampleFlushRunnable = null
            flushPendingSample(appContext, forceNodeRefresh = false)
        }.also { runnable ->
            mainHandler.postDelayed(runnable, SAMPLE_FLUSH_INTERVAL_MS)
        }
    }

    private fun scheduleSampleWatchdog(context: Context) {
        if (sampleWatchdogRunnable != null) {
            return
        }
        val appContext = context.applicationContext
        sampleWatchdogRunnable = Runnable {
            sampleWatchdogRunnable = null
            checkSampleFreshness(appContext)
        }.also { runnable ->
            mainHandler.postDelayed(runnable, SAMPLE_WATCHDOG_INTERVAL_MS)
        }
    }

    private fun cancelSampleWatchdog() {
        sampleWatchdogRunnable?.let { mainHandler.removeCallbacks(it) }
        sampleWatchdogRunnable = null
    }

    private fun scheduleDistanceRetry(context: Context) {
        if (distanceRetryRunnable != null) {
            return
        }
        val appContext = context.applicationContext
        distanceRetryRunnable = Runnable {
            distanceRetryRunnable = null
            retryDistanceMetrics(appContext)
        }.also { runnable ->
            mainHandler.postDelayed(runnable, DISTANCE_RETRY_DELAY_MS)
        }
    }

    private fun cancelDistanceRetry() {
        distanceRetryRunnable?.let { mainHandler.removeCallbacks(it) }
        distanceRetryRunnable = null
    }

    private fun checkSampleFreshness(context: Context) {
        if (!shouldAggregate || sessionId.isNullOrBlank()) {
            return
        }
        val latestSampleAt = latestSampleAtEpochMs
        val elapsedMs = System.currentTimeMillis() - latestSampleAt
        if (
            latestSampleAt > 0L &&
            elapsedMs >= SAMPLE_STALE_TIMEOUT_MS &&
            !exerciseMetricsStartInFlight
        ) {
            restartExerciseMetrics(context)
        }
        if (shouldAggregate && !sessionId.isNullOrBlank()) {
            scheduleSampleWatchdog(context)
        }
    }

    private fun flushPendingSample(context: Context, forceNodeRefresh: Boolean) {
        val sample = pendingSample ?: return
        pendingSample = null
        sampleFlushRunnable?.let { mainHandler.removeCallbacks(it) }
        sampleFlushRunnable = null
        sendSampleToNodes(context, sample, forceNodeRefresh)
    }

    private fun sendSampleToNodes(
        context: Context,
        sample: Map<String, Any?>,
        forceNodeRefresh: Boolean,
    ) {
        val payload = JSONObject(sample).toString().toByteArray(StandardCharsets.UTF_8)
        val cachedNodes = cachedNodesIfFresh()
        if (!forceNodeRefresh && cachedNodes.isNotEmpty()) {
            sendSamplePayload(context, payload, cachedNodes, sample)
            return
        }
        if (nodeLookupInFlight && cachedReachableNodes.isNotEmpty()) {
            sendSamplePayload(context, payload, cachedReachableNodes, sample)
            return
        }
        nodeLookupInFlight = true
        Wearable.getCapabilityClient(context)
            .getCapability(phoneCapability, CapabilityClient.FILTER_REACHABLE)
            .addOnSuccessListener { capability ->
                nodeLookupInFlight = false
                val nodes = capability.nodes.toList()
                cacheReachableNodes(nodes)
                sendSamplePayload(context, payload, nodes, sample)
            }
            .addOnFailureListener { error ->
                nodeLookupInFlight = false
                Log.w(TAG, "sample capability lookup failed", error)
            }
    }

    private fun sendSamplePayload(
        context: Context,
        payload: ByteArray,
        nodes: List<Node>,
        sample: Map<String, Any?>,
    ) {
        logHotPath(
            "send sample sessionId=${sample["sessionId"]} bpm=${sample["heartRateBpm"]} distance=${sample["distanceMeters"]} calories=${sample["caloriesKcal"]} nodes=${nodes.size}",
        )
        for (node in nodes) {
            Wearable.getMessageClient(context)
                .sendMessage(node.id, sensorSamplePath, payload)
        }
    }

    private fun cachedNodesIfFresh(): List<Node> {
        val now = System.currentTimeMillis()
        if (now - cachedReachableNodesAtEpochMs > NODE_CACHE_TTL_MS) {
            return emptyList()
        }
        return cachedReachableNodes
    }

    private fun cacheReachableNodes(nodes: List<Node>) {
        cachedReachableNodes = nodes
        cachedReachableNodesAtEpochMs = System.currentTimeMillis()
    }
}

internal object WatchExerciseMetricsRetryPolicy {
    fun isDistanceSecurityFailure(
        dataTypes: Set<androidx.health.services.client.data.DataType<*, *>>,
        error: Throwable,
    ): Boolean = DataType.DISTANCE in dataTypes && error.hasCause<SecurityException>()

    fun canRetryDistance(securityFailureCount: Int): Boolean =
        securityFailureCount in 1..MAX_DISTANCE_SECURITY_RETRIES

    private inline fun <reified T : Throwable> Throwable.hasCause(): Boolean {
        var current: Throwable? = this
        while (current != null) {
            if (current is T) {
                return true
            }
            current = current.cause
        }
        return false
    }
}
