package com.gametime.app.watch

import android.content.Context
import android.content.Intent
import android.os.Handler
import android.os.Looper
import com.google.android.gms.wearable.CapabilityClient
import com.google.android.gms.wearable.PutDataMapRequest
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.JSONObject
import java.nio.charset.StandardCharsets
import java.util.concurrent.ConcurrentHashMap

object WatchBridgePlugin {
    private const val METHOD_CHANNEL = "gametime.watch_bridge/methods"
    private const val COMMAND_CHANNEL = "gametime.watch_bridge/commands"
    private const val SENSOR_SUMMARY_CHANNEL = "gametime.watch_bridge/sensor_summaries"
    private const val SENSOR_SAMPLE_CHANNEL = "gametime.watch_bridge/sensor_samples"
    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 WATCH_CAPABILITY = "gametime_watch_companion"

    private val pendingCommandNodes = ConcurrentHashMap<String, String>()
    private var appContext: Context? = null
    private var commandSink: EventChannel.EventSink? = null
    private var sensorSummarySink: EventChannel.EventSink? = null
    private var sensorSampleSink: EventChannel.EventSink? = null
    private var connectionSink: EventChannel.EventSink? = null
    private val mainHandler = Handler(Looper.getMainLooper())

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

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

                    override fun onCancel(arguments: Any?) {
                        sensorSummarySink = 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
                    }
                },
            )
    }

    fun emitCommand(payload: Map<String, Any?>, sourceNodeId: String): Boolean {
        val sink = commandSink ?: return false
        val commandId = payload["commandId"] as? String
        if (commandId != null) {
            pendingCommandNodes[commandId] = sourceNodeId
        }
        mainHandler.post {
            sink.success(payload)
        }
        return true
    }

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

    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,
                ),
            )
        }
    }

    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) {
            "publishProjection" -> publishProjection(context, call.arguments, result)
            "publishAlert" -> publishAlert(context, call.arguments, result)
            "sendCommandAck" -> sendCommandAck(context, call.arguments, result)
            "requestCapabilityRefresh" -> {
                requestCapabilityRefresh(context)
                result.success(null)
            }
            "startForegroundService" -> {
                startForegroundService(context)
                result.success(null)
            }
            "stopForegroundService" -> {
                context.stopService(Intent(context, WatchCompanionForegroundService::class.java))
                result.success(null)
            }
            else -> result.notImplemented()
        }
    }

    private fun publishProjection(
        context: Context,
        arguments: Any?,
        result: MethodChannel.Result,
    ) {
        val map = arguments as? Map<*, *>
        if (map == null) {
            result.error("invalid_projection", "Projection payload must be a map.", null)
            return
        }
        val projection = map.filterKeys { it != "urgent" }
        val projectionJson = JSONObject(projection).toString()
        val request = PutDataMapRequest.create(STATE_PATH).apply {
            dataMap.putString("projectionJson", projectionJson)
            dataMap.putInt(
                "schemaVersion",
                (projection["schemaVersion"] as? Number)?.toInt() ?: 1,
            )
            dataMap.putInt("revision", (projection["revision"] as? Number)?.toInt() ?: 0)
            dataMap.putLong(
                "projectedAtEpochMs",
                (projection["projectedAtEpochMs"] as? Number)?.toLong() ?: 0L,
            )
            dataMap.putLong(
                "expiresAtEpochMs",
                (projection["expiresAtEpochMs"] as? Number)?.toLong() ?: 0L,
            )
        }.asPutDataRequest()
        if (map["urgent"] == true) {
            request.setUrgent()
        }
        Wearable.getDataClient(context).putDataItem(request)
            .addOnSuccessListener { result.success(null) }
            .addOnFailureListener { error ->
                result.error("publish_projection_failed", error.message, null)
            }
    }

    private fun sendCommandAck(
        context: Context,
        arguments: Any?,
        result: MethodChannel.Result,
    ) {
        val map = arguments as? Map<*, *>
        if (map == null) {
            result.error("invalid_ack", "Ack payload must be a map.", null)
            return
        }
        val commandId = map["commandId"] as? String
        val targetNode = commandId?.let { pendingCommandNodes.remove(it) }
        val payload = JSONObject(map).toString().toByteArray(StandardCharsets.UTF_8)
        if (targetNode != null) {
            sendMessage(context, targetNode, ACK_PATH, payload, result)
            return
        }
        Wearable.getCapabilityClient(context)
            .getCapability(WATCH_CAPABILITY, CapabilityClient.FILTER_REACHABLE)
            .addOnSuccessListener { capability ->
                val nodes = capability.nodes.toList()
                if (nodes.isEmpty()) {
                    result.success(null)
                    return@addOnSuccessListener
                }
                var remaining = nodes.size
                var failed = false
                for (node in nodes) {
                    Wearable.getMessageClient(context)
                        .sendMessage(node.id, ACK_PATH, payload)
                        .addOnSuccessListener {
                            remaining -= 1
                            if (remaining == 0 && !failed) result.success(null)
                        }
                        .addOnFailureListener { error ->
                            failed = true
                            result.error("send_ack_failed", error.message, null)
                        }
                }
            }
            .addOnFailureListener { error ->
                result.error("capability_lookup_failed", error.message, null)
            }
    }

    private fun sendMessage(
        context: Context,
        nodeId: String,
        path: String,
        payload: ByteArray,
        result: MethodChannel.Result,
    ) {
        Wearable.getMessageClient(context)
            .sendMessage(nodeId, path, payload)
            .addOnSuccessListener { result.success(null) }
            .addOnFailureListener { error ->
                result.error("send_message_failed", error.message, null)
            }
    }

    private fun publishAlert(
        context: Context,
        arguments: Any?,
        result: MethodChannel.Result,
    ) {
        val map = arguments as? Map<*, *>
        if (map == null) {
            result.error("invalid_alert", "Alert payload must be a map.", null)
            return
        }
        val payload = JSONObject(map).toString().toByteArray(StandardCharsets.UTF_8)
        Wearable.getCapabilityClient(context)
            .getCapability(WATCH_CAPABILITY, CapabilityClient.FILTER_REACHABLE)
            .addOnSuccessListener { capability ->
                val nodes = capability.nodes.toList()
                if (nodes.isEmpty()) {
                    result.success(null)
                    return@addOnSuccessListener
                }
                var remaining = nodes.size
                var failed = false
                for (node in nodes) {
                    Wearable.getMessageClient(context)
                        .sendMessage(node.id, ALERT_PATH, payload)
                        .addOnSuccessListener {
                            remaining -= 1
                            if (remaining == 0 && !failed) result.success(null)
                        }
                        .addOnFailureListener { error ->
                            if (failed) {
                                return@addOnFailureListener
                            }
                            failed = true
                            result.error("send_alert_failed", error.message, null)
                        }
                }
            }
            .addOnFailureListener { error ->
                result.error("capability_lookup_failed", error.message, null)
            }
    }

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

    private fun startForegroundService(context: Context) {
        val intent = Intent(context, WatchCompanionForegroundService::class.java)
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
            context.startForegroundService(intent)
        } else {
            context.startService(intent)
        }
    }
}
