package com.gametime.watch.bridge

import android.app.Activity
import android.content.Context
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.VibrationEffect
import android.os.Vibrator
import android.os.VibratorManager
import android.util.Log
import com.google.android.gms.wearable.CapabilityClient
import com.google.android.gms.wearable.DataEvent
import com.google.android.gms.wearable.DataMapItem
import com.google.android.gms.wearable.Wearable
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import org.json.JSONArray
import org.json.JSONObject
import java.nio.charset.StandardCharsets

object WatchBridgePlugin {
    private const val TAG = "GTWatchBridgeWatch"
    private const val METHOD_CHANNEL = "gametime.watch_bridge/methods"
    private const val PROJECTION_CHANNEL = "gametime.watch_bridge/projections"
    private const val SENSOR_SAMPLE_CHANNEL = "gametime.watch_bridge/sensor_samples"
    private const val ACK_CHANNEL = "gametime.watch_bridge/acks"
    private const val ALERT_CHANNEL = "gametime.watch_bridge/alerts"
    private const val CONNECTION_CHANNEL = "gametime.watch_bridge/connection"

    const val COMMAND_PATH = "/gametime/watch/command"
    const val SENSOR_SUMMARY_PATH = "/gametime/watch/sensor-summary"
    const val SENSOR_SAMPLE_PATH = "/gametime/watch/sensor-sample"
    const val ACK_PATH = "/gametime/phone/ack"
    const val ALERT_PATH = "/gametime/phone/alert"
    const val STATE_PATH = "/gametime/phone/projection"
    const val PHONE_CAPABILITY = "gametime_phone_companion"
    const val ACTION_OPEN_ACTIVE_SESSION = "com.gametime.watch.OPEN_ACTIVE_SESSION"
    private const val SENSOR_PERMISSION_REQUEST = 4106
    private const val SENSOR_PERMISSION_RETRY_DELAY_MS = 30000L
    private var appContext: Context? = null
    private var activity: Activity? = null
    private var projectionSink: EventChannel.EventSink? = null
    private var sensorSampleSink: EventChannel.EventSink? = null
    private var ackSink: EventChannel.EventSink? = null
    private var alertSink: EventChannel.EventSink? = null
    private var connectionSink: EventChannel.EventSink? = null
    private val mainHandler = Handler(Looper.getMainLooper())
    private val heartRateCollector = WatchHeartRateCollector(
        phoneCapability = PHONE_CAPABILITY,
        sensorSummaryPath = SENSOR_SUMMARY_PATH,
        sensorSamplePath = SENSOR_SAMPLE_PATH,
        onLocalSample = ::emitSensorSample,
    )
    private var sensorPermissionRequestInFlight = false
    private var lastSensorPermissionRequestEpochMs = 0L
    private var pendingSensorPermissionRequest = false
    private var lastSensorProjection: Map<String, Any?>? = null
    private var lastActiveProjection: Map<String, Any?>? = null
    private var lastActiveProjectionReceivedAtEpochMs = 0L
    private var activeProjectionExpiryRunnable: Runnable? = null

    fun attachApplicationContext(context: Context) {
        appContext = context.applicationContext
    }

    fun register(flutterEngine: FlutterEngine, context: Context) {
        attachApplicationContext(context)
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, METHOD_CHANNEL)
            .setMethodCallHandler(::handleMethodCall)
        EventChannel(flutterEngine.dartExecutor.binaryMessenger, PROJECTION_CHANNEL)
            .setStreamHandler(
                object : EventChannel.StreamHandler {
                    override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
                        projectionSink = events
                        requestLatestProjection(context.applicationContext)
                    }

                    override fun onCancel(arguments: Any?) {
                        projectionSink = null
                    }
                },
            )
        EventChannel(flutterEngine.dartExecutor.binaryMessenger, ACK_CHANNEL)
            .setStreamHandler(
                object : EventChannel.StreamHandler {
                    override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
                        ackSink = events
                    }

                    override fun onCancel(arguments: Any?) {
                        ackSink = null
                    }
                },
            )
        EventChannel(flutterEngine.dartExecutor.binaryMessenger, ALERT_CHANNEL)
            .setStreamHandler(
                object : EventChannel.StreamHandler {
                    override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
                        alertSink = events
                    }

                    override fun onCancel(arguments: Any?) {
                        alertSink = null
                    }
                },
            )
        EventChannel(flutterEngine.dartExecutor.binaryMessenger, SENSOR_SAMPLE_CHANNEL)
            .setStreamHandler(
                object : EventChannel.StreamHandler {
                    override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
                        sensorSampleSink = events
                    }

                    override fun onCancel(arguments: Any?) {
                        sensorSampleSink = null
                    }
                },
            )
        EventChannel(flutterEngine.dartExecutor.binaryMessenger, CONNECTION_CHANNEL)
            .setStreamHandler(
                object : EventChannel.StreamHandler {
                    override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
                        connectionSink = events
                        requestCapabilityRefresh(context.applicationContext)
                    }

                    override fun onCancel(arguments: Any?) {
                        connectionSink = null
                    }
                },
            )
        requestPendingSensorPermissionIfPossible()
    }

    fun attachActivity(activity: Activity) {
        this.activity = activity
        requestPendingSensorPermissionIfPossible()
    }

    fun handleActivityReentry(activity: Activity, intent: android.content.Intent?) {
        attachActivity(activity)
        val context = activity.applicationContext
        requestCapabilityRefresh(context)
        requestLatestProjection(context)
        if (intent?.action == ACTION_OPEN_ACTIVE_SESSION && hasFreshActiveProjection()) {
            emitProjection(lastActiveProjection ?: return)
        }
    }

    fun detachActivity(activity: Activity) {
        if (this.activity === activity) {
            this.activity = null
        }
    }

    fun handlePermissionResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray,
    ) {
        if (requestCode != SENSOR_PERMISSION_REQUEST) {
            return
        }
        sensorPermissionRequestInFlight = false
        val granted = appContext?.let(::hasRequiredRuntimePermissions) == true
        if (granted) {
            pendingSensorPermissionRequest = false
            Log.d(TAG, "sensor permissions granted")
            appContext?.let { context ->
                val projection = lastSensorProjection
                if (projection != null) {
                    WatchHeartRateForegroundService.start(context, projection)
                }
                heartRateCollector.onBodySensorsGranted(context)
            }
        } else {
            val denied = permissions.filterIndexed { index, _ ->
                grantResults.getOrNull(index) != PackageManager.PERMISSION_GRANTED
            }
            Log.w(TAG, "sensor permissions denied=${denied.joinToString()}")
            emitSensorPermissionRequired()
        }
    }

    fun emitProjection(payload: Map<String, Any?>): Boolean {
        rememberActiveProjection(payload)
        appContext?.let {
            scheduleActiveProjectionExpiry(it, payload)
            WatchOngoingActivityController.update(it, payload, activity)
            updateHeartRateCollection(it, payload)
        }
        val sink = projectionSink ?: return false
        mainHandler.post {
            sink.success(payload)
        }
        return true
    }

    fun emitAck(payload: Map<String, Any?>): Boolean {
        val sink = ackSink ?: return false
        mainHandler.post {
            sink.success(payload)
        }
        return true
    }

    fun emitAlert(payload: Map<String, Any?>): Boolean {
        val sink = alertSink ?: return false
        mainHandler.post {
            sink.success(payload)
        }
        return true
    }

    fun handleAlert(payload: Map<String, Any?>) {
        if (!emitAlert(payload)) {
            triggerNativeAlertHaptic(payload)
        }
    }

    fun emitSensorSample(payload: Map<String, Any?>): Boolean {
        val sink = sensorSampleSink ?: return false
        mainHandler.post {
            sink.success(payload)
        }
        return true
    }

    fun emitConnection(isReachable: Boolean, requestsResync: Boolean) {
        val sink = connectionSink ?: return
        mainHandler.post {
            sink.success(
                mapOf(
                    "isReachable" to isReachable,
                    "requestsResync" to requestsResync,
                ),
            )
        }
    }

    fun handleDataEvent(event: DataEvent) {
        if (event.type != DataEvent.TYPE_CHANGED ||
            event.dataItem.uri.path != STATE_PATH
        ) {
            return
        }
        val projectionJson = DataMapItem.fromDataItem(event.dataItem)
            .dataMap
            .getString("projectionJson")
            ?: return
        val projection = JSONObject(projectionJson).toMap()
        emitProjection(projection)
    }

    private fun handleMethodCall(call: MethodCall, result: MethodChannel.Result) {
        val context = appContext
        if (context == null) {
            result.error("watch_bridge_unavailable", "Application context unavailable.", null)
            return
        }
        when (call.method) {
            "sendCommand" -> sendCommand(context, call.arguments, result)
            "requestCapabilityRefresh" -> {
                requestCapabilityRefresh(context)
                result.success(null)
            }
            "requestResync" -> {
                requestLatestProjection(context)
                requestCapabilityRefresh(context)
                resyncLiveStatsAfterReconnect(context)
                result.success(null)
            }
            "invalidateActiveProjection" -> {
                invalidateActiveProjection(context)
                result.success(null)
            }
            else -> result.notImplemented()
        }
    }

    fun invalidateActiveProjection(context: Context) {
        lastActiveProjection = null
        lastActiveProjectionReceivedAtEpochMs = 0L
        activeProjectionExpiryRunnable?.let { mainHandler.removeCallbacks(it) }
        activeProjectionExpiryRunnable = null
        WatchOngoingActivityController.cancel(context)
        WatchHeartRateForegroundService.stop(context, force = true)
        heartRateCollector.finishCurrentSession(context)
    }

    private fun sendCommand(
        context: Context,
        arguments: Any?,
        result: MethodChannel.Result,
    ) {
        val map = arguments as? Map<*, *>
        if (map == null) {
            result.error("invalid_command", "Command payload must be a map.", null)
            return
        }
        val payload = JSONObject(map).toString().toByteArray(StandardCharsets.UTF_8)
        Wearable.getCapabilityClient(context)
            .getCapability(PHONE_CAPABILITY, CapabilityClient.FILTER_REACHABLE)
            .addOnSuccessListener { capability ->
                val nodes = capability.nodes.toList()
                if (nodes.isEmpty()) {
                    emitConnection(isReachable = false, requestsResync = false)
                    result.error("phone_unreachable", "No reachable phone companion.", null)
                    return@addOnSuccessListener
                }
                var remaining = nodes.size
                var failed = false
                for (node in nodes) {
                    Wearable.getMessageClient(context)
                        .sendMessage(node.id, COMMAND_PATH, payload)
                        .addOnSuccessListener {
                            remaining -= 1
                            if (remaining == 0 && !failed) {
                                emitConnection(isReachable = true, requestsResync = false)
                                result.success(null)
                            }
                        }
                        .addOnFailureListener { error ->
                            if (failed) {
                                return@addOnFailureListener
                            }
                            failed = true
                            emitConnection(isReachable = false, requestsResync = false)
                            result.error("send_command_failed", error.message, null)
                        }
                }
            }
            .addOnFailureListener { error ->
                emitConnection(isReachable = false, requestsResync = false)
                result.error("capability_lookup_failed", error.message, null)
            }
    }

    fun requestCapabilityRefresh(context: Context) {
        Wearable.getCapabilityClient(context)
            .getCapability(PHONE_CAPABILITY, CapabilityClient.FILTER_REACHABLE)
            .addOnSuccessListener { capability ->
                if (capability.nodes.isNotEmpty()) {
                    resyncLiveStatsAfterReconnect(context)
                }
                emitConnection(
                    isReachable = capability.nodes.isNotEmpty(),
                    requestsResync = capability.nodes.isNotEmpty(),
                )
            }
            .addOnFailureListener {
                emitConnection(isReachable = false, requestsResync = false)
            }
    }

    fun resyncLiveStatsAfterReconnect(context: Context) {
        heartRateCollector.onPhoneReconnected(context)
        val projection = lastSensorProjection ?: lastActiveProjection ?: return
        updateHeartRateCollection(context, projection)
    }

    fun requestLatestProjection(context: Context) {
        val uri = Uri.Builder()
            .scheme("wear")
            .authority("*")
            .path(STATE_PATH)
            .build()
        Wearable.getDataClient(context)
            .getDataItems(uri)
            .addOnSuccessListener { buffer ->
                try {
                    for (item in buffer) {
                        val projectionJson = DataMapItem.fromDataItem(item)
                            .dataMap
                            .getString("projectionJson")
                            ?: continue
                        emitProjection(JSONObject(projectionJson).toMap())
                    }
                } finally {
                    buffer.release()
                }
            }
    }

    fun openActiveSessionIntent(context: Context): android.content.Intent {
        return android.content.Intent(context, com.gametime.watch.MainActivity::class.java).apply {
            action = ACTION_OPEN_ACTIVE_SESSION
            flags = android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP or
                android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP
        }
    }

    private fun rememberActiveProjection(projection: Map<String, Any?>) {
        val phase = projection["phase"] as? String ?: "noActiveSession"
        val sessionId = projection["deviceSessionId"] as? String ?: ""
        if (phase == "noActiveSession" || sessionId.isBlank()) {
            lastActiveProjection = null
            lastActiveProjectionReceivedAtEpochMs = 0L
            activeProjectionExpiryRunnable?.let { mainHandler.removeCallbacks(it) }
            activeProjectionExpiryRunnable = null
            return
        }
        lastActiveProjection = projection
        lastActiveProjectionReceivedAtEpochMs = System.currentTimeMillis()
    }

    private fun scheduleActiveProjectionExpiry(context: Context, projection: Map<String, Any?>) {
        activeProjectionExpiryRunnable?.let { mainHandler.removeCallbacks(it) }
        val phase = projection["phase"] as? String ?: "noActiveSession"
        val sessionId = projection["deviceSessionId"] as? String ?: ""
        if (phase == "noActiveSession" || sessionId.isBlank()) {
            activeProjectionExpiryRunnable = null
            return
        }
        val now = System.currentTimeMillis()
        val delayMs = projectionTtlMs(projection)
        val appContext = context.applicationContext
        activeProjectionExpiryRunnable = Runnable {
            if (!hasFreshActiveProjection()) {
                invalidateActiveProjection(appContext)
            }
        }.also { runnable ->
            mainHandler.postDelayed(runnable, delayMs + 250L)
        }
    }

    private fun hasFreshActiveProjection(): Boolean {
        val projection = lastActiveProjection ?: return false
        val now = System.currentTimeMillis()
        return now - lastActiveProjectionReceivedAtEpochMs <= projectionTtlMs(projection)
    }

    private fun projectionTtlMs(projection: Map<String, Any?>): Long {
        val projectedAt = (projection["projectedAtEpochMs"] as? Number)?.toLong() ?: 0L
        val expiresAt = (projection["expiresAtEpochMs"] as? Number)?.toLong() ?: 0L
        if (projectedAt > 0L && expiresAt > projectedAt) {
            return expiresAt - projectedAt
        }
        return 12000L
    }

    private fun triggerNativeAlertHaptic(payload: Map<String, Any?>) {
        if (payload["pattern"] != "timerFinished") {
            return
        }
        val context = appContext ?: return
        val vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            context.getSystemService(VibratorManager::class.java)?.defaultVibrator
        } else {
            @Suppress("DEPRECATION")
            context.getSystemService(Vibrator::class.java)
        } ?: return
        val timings = longArrayOf(0L, 320L, 90L, 420L, 90L, 520L)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            vibrator.vibrate(
                VibrationEffect.createWaveform(
                    timings,
                    intArrayOf(0, 255, 0, 255, 0, 255),
                    -1,
                ),
            )
        } else {
            @Suppress("DEPRECATION")
            vibrator.vibrate(timings, -1)
        }
    }

    private fun updateHeartRateCollection(context: Context, projection: Map<String, Any?>) {
        val phase = projection["phase"] as? String ?: "noActiveSession"
        val sessionId = projection["deviceSessionId"] as? String ?: ""
        if (phase == "noActiveSession" || sessionId.isBlank()) {
            lastSensorProjection = null
            WatchHeartRateForegroundService.stop(context, force = phase == "noActiveSession")
            heartRateCollector.finishCurrentSession(context)
            return
        }
        lastSensorProjection = projection
        val shouldAggregate = phase == "running"
        if (!hasRequiredRuntimePermissions(context)) {
            WatchHeartRateForegroundService.stop(context)
            heartRateCollector.noteActiveSession(
                sessionId,
                shouldAggregate = shouldAggregate,
                executionContext = telemetryContext(projection),
            )
            pendingSensorPermissionRequest = true
            emitSensorPermissionRequired()
            requestSensorPermissionsOnce()
            return
        }
        pendingSensorPermissionRequest = false
        heartRateCollector.noteActiveSession(
            sessionId,
            shouldAggregate,
            executionContext = telemetryContext(projection),
        )
        if (shouldAggregate) {
            WatchHeartRateForegroundService.start(context, projection)
            heartRateCollector.start(context)
        } else {
            WatchHeartRateForegroundService.stop(context)
            heartRateCollector.pause(context)
        }
    }

    private fun hasRequiredSensorPermissions(context: Context): Boolean {
        return requiredSensorPermissions().all { permission ->
            context.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED
        }
    }

    private fun hasRequiredRuntimePermissions(context: Context): Boolean {
        return requiredRuntimePermissions().all { permission ->
            context.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED
        }
    }

    private fun requestSensorPermissionsOnce() {
        val activity = activity ?: run {
            Log.d(TAG, "sensor permission request pending: activity unavailable")
            return
        }
        val permissions = requiredRuntimePermissions()
            .filter { activity.checkSelfPermission(it) != PackageManager.PERMISSION_GRANTED }
            .toTypedArray()
        if (permissions.isEmpty()) {
            pendingSensorPermissionRequest = false
            appContext?.let { heartRateCollector.onBodySensorsGranted(it) }
            return
        }
        val now = System.currentTimeMillis()
        if (sensorPermissionRequestInFlight &&
            now - lastSensorPermissionRequestEpochMs < SENSOR_PERMISSION_RETRY_DELAY_MS
        ) {
            Log.d(
                TAG,
                "sensor permission request in flight missing=${permissions.joinToString()}",
            )
            return
        }
        if (!sensorPermissionRequestInFlight &&
            now - lastSensorPermissionRequestEpochMs < SENSOR_PERMISSION_RETRY_DELAY_MS
        ) {
            Log.d(
                TAG,
                "sensor permission request throttled missing=${permissions.joinToString()}",
            )
            return
        }
        sensorPermissionRequestInFlight = true
        lastSensorPermissionRequestEpochMs = now
        Log.d(TAG, "request sensor permissions=${permissions.joinToString()}")
        activity.requestPermissions(permissions, SENSOR_PERMISSION_REQUEST)
    }

    private fun requestPendingSensorPermissionIfPossible() {
        val context = appContext ?: return
        if (!pendingSensorPermissionRequest || hasRequiredRuntimePermissions(context)) {
            return
        }
        requestSensorPermissionsOnce()
    }

    private fun requiredSensorPermissions(): List<String> {
        return WatchSensorPermissionPolicy.requiredSensorPermissions(Build.VERSION.SDK_INT)
    }

    private fun requiredRuntimePermissions(): List<String> {
        return WatchSensorPermissionPolicy.requiredRuntimePermissions(Build.VERSION.SDK_INT)
    }

    private fun telemetryContext(projection: Map<String, Any?>): Map<String, Any?> {
        return mapOf(
            "programIndex" to projection["programIndex"],
            "exerciseIndex" to projection["exerciseIndex"],
            "setIndex" to projection["setIndex"],
            "passageIndex" to zeroBasedNullableIndex(projection["passageIndex"]),
            "stepIndex" to zeroBasedNullableIndex(projection["stepIndex"]),
            "healthServicesExerciseTypeStrategy" to
                projection["healthServicesExerciseTypeStrategy"],
        )
    }

    private fun zeroBasedNullableIndex(value: Any?): Int? {
        val index = (value as? Number)?.toInt() ?: return null
        return if (index > 0) index - 1 else null
    }

    private fun emitSensorPermissionRequired() {
        val projection = lastSensorProjection ?: lastActiveProjection ?: return
        val sessionId = projection["deviceSessionId"] as? String ?: return
        if (sessionId.isBlank()) {
            return
        }
        emitSensorSample(
            mapOf(
                "schemaVersion" to 4,
                "sampleId" to "$sessionId-permission-${System.currentTimeMillis()}",
                "sessionId" to sessionId,
                "capturedAtEpochMs" to System.currentTimeMillis(),
                "recordedAtEpochMs" to System.currentTimeMillis(),
                "programIndex" to projection["programIndex"],
                "exerciseIndex" to projection["exerciseIndex"],
                "setIndex" to projection["setIndex"],
                "passageIndex" to zeroBasedNullableIndex(projection["passageIndex"]),
                "stepIndex" to zeroBasedNullableIndex(projection["stepIndex"]),
                "sensorCollectionStatus" to "authorizationRequired",
            ),
        )
    }
}

internal object WatchSensorPermissionPolicy {
    private const val READ_HEART_RATE_PERMISSION =
        "android.permission.health.READ_HEART_RATE"

    fun requiredSensorPermissions(sdkInt: Int): List<String> {
        val heartRatePermission = if (sdkInt >= 36) {
            READ_HEART_RATE_PERMISSION
        } else {
            android.Manifest.permission.BODY_SENSORS
        }
        return listOf(
            heartRatePermission,
            android.Manifest.permission.ACTIVITY_RECOGNITION,
        )
    }

    fun requiredRuntimePermissions(sdkInt: Int): List<String> {
        val permissions = requiredSensorPermissions(sdkInt).toMutableList()
        if (sdkInt >= Build.VERSION_CODES.TIRAMISU) {
            permissions.add(android.Manifest.permission.POST_NOTIFICATIONS)
        }
        return permissions
    }
}

private fun JSONObject.toMap(): Map<String, Any?> {
    val output = linkedMapOf<String, Any?>()
    val keys = keys()
    while (keys.hasNext()) {
        val key = keys.next()
        output[key] = unwrapJsonValue(get(key))
    }
    return output
}

private fun JSONArray.toList(): List<Any?> {
    val output = mutableListOf<Any?>()
    for (index in 0 until length()) {
        output.add(unwrapJsonValue(get(index)))
    }
    return output
}

private fun unwrapJsonValue(value: Any?): Any? {
    return when (value) {
        JSONObject.NULL -> null
        is JSONObject -> value.toMap()
        is JSONArray -> value.toList()
        else -> value
    }
}
