package de.usbserver.bridge

import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbManager
import android.os.Build
import android.os.IBinder
import android.util.Log

/**
 * Foreground service that keeps the client running and offers devices to it.
 *
 * A foreground service with a visible notification is not optional here:
 * Android's Doze and background limits would otherwise suspend or kill the
 * process, and a suspended process means a USB device that silently stops
 * responding for whoever is using it remotely.
 */
class ShareService : Service() {

    private lateinit var clientProcess: ClientProcess
    private lateinit var bridge: UsbBridge
    private lateinit var usbManager: UsbManager

    /**
     * Receives permission results and unplug events.
     *
     * Unplug matters: the descriptor dies with the device, and the client has
     * to be told, or it keeps advertising a device that is no longer there.
     */
    private val receiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context, intent: Intent) {
            when (intent.action) {
                ACTION_USB_PERMISSION -> {
                    val device = intent.usbDevice() ?: return
                    if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
                        shareDevice(device)
                    } else {
                        Log.i(TAG, "permission refused for ${device.deviceName}")
                    }
                }

                UsbManager.ACTION_USB_DEVICE_ATTACHED -> {
                    intent.usbDevice()?.let { requestPermission(it) }
                }

                UsbManager.ACTION_USB_DEVICE_DETACHED -> {
                    intent.usbDevice()?.let { device ->
                        bridge.unshare(device).onFailure {
                            Log.w(TAG, "withdrawing ${device.deviceName} failed: ${it.message}")
                        }
                    }
                }
            }
        }

        @Suppress("DEPRECATION")
        private fun Intent.usbDevice(): UsbDevice? =
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                getParcelableExtra(UsbManager.EXTRA_DEVICE, UsbDevice::class.java)
            } else {
                getParcelableExtra(UsbManager.EXTRA_DEVICE)
            }
    }

    override fun onCreate() {
        super.onCreate()

        usbManager = getSystemService(Context.USB_SERVICE) as UsbManager
        clientProcess = ClientProcess(this)
        bridge = UsbBridge(this, clientProcess.socketPath)

        createNotificationChannel()
        startForeground(NOTIFICATION_ID, buildNotification("Starting…"))

        val filter = IntentFilter().apply {
            addAction(ACTION_USB_PERMISSION)
            addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED)
            addAction(UsbManager.ACTION_USB_DEVICE_DETACHED)
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED)
        } else {
            @Suppress("UnspecifiedRegisterReceiverFlag")
            registerReceiver(receiver, filter)
        }
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        val relay = intent?.getStringExtra(EXTRA_RELAY) ?: return START_NOT_STICKY
        val t1 = intent.getStringExtra(EXTRA_TOKEN1) ?: return START_NOT_STICKY
        val t2 = intent.getStringExtra(EXTRA_TOKEN2) ?: return START_NOT_STICKY
        val t3 = intent.getStringExtra(EXTRA_TOKEN3) ?: return START_NOT_STICKY
        val name = intent.getStringExtra(EXTRA_NAME) ?: Build.MODEL

        clientProcess.writeConfig(relay, Triple(t1, t2, t3), name)

        clientProcess.start()
            .onSuccess {
                updateNotification("Connected to $relay")
                // Give the client a moment to bind its socket before offering
                // anything to it.
                Thread {
                    Thread.sleep(500)
                    askForAllDevices()
                }.start()
            }
            .onFailure {
                Log.e(TAG, "starting the client failed", it)
                updateNotification("Failed: ${it.message}")
                stopSelf()
            }

        // START_STICKY so the service comes back if the system reclaims it.
        return START_STICKY
    }

    override fun onDestroy() {
        super.onDestroy()
        runCatching { unregisterReceiver(receiver) }
        bridge.closeAll()
        clientProcess.stop()
    }

    override fun onBind(intent: Intent?): IBinder? = null

    /** Asks for permission on every device currently attached. */
    private fun askForAllDevices() {
        usbManager.deviceList.values.forEach { device ->
            if (usbManager.hasPermission(device)) {
                shareDevice(device)
            } else {
                requestPermission(device)
            }
        }
    }

    private fun requestPermission(device: UsbDevice) {
        val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            PendingIntent.FLAG_MUTABLE
        } else {
            0
        }
        val intent = PendingIntent.getBroadcast(
            this, 0, Intent(ACTION_USB_PERMISSION).setPackage(packageName), flags
        )
        usbManager.requestPermission(device, intent)
    }

    private fun shareDevice(device: UsbDevice) {
        bridge.share(device)
            .onSuccess {
                updateNotification("Sharing ${bridge.let { device.productName ?: device.deviceName }}")
            }
            .onFailure {
                Log.w(TAG, "sharing ${device.deviceName} failed: ${it.message}")
            }
    }

    private fun createNotificationChannel() {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return

        val channel = NotificationChannel(
            CHANNEL_ID,
            "USB Sharing",
            NotificationManager.IMPORTANCE_LOW,
        ).apply {
            description = "Keeps shared USB devices reachable"
        }
        getSystemService(NotificationManager::class.java).createNotificationChannel(channel)
    }

    private fun buildNotification(text: String): Notification =
        Notification.Builder(this, CHANNEL_ID)
            .setContentTitle("USB Server")
            .setContentText(text)
            .setSmallIcon(android.R.drawable.stat_sys_data_bluetooth)
            .setOngoing(true)
            .build()

    private fun updateNotification(text: String) {
        getSystemService(NotificationManager::class.java)
            .notify(NOTIFICATION_ID, buildNotification(text))
    }

    companion object {
        private const val TAG = "UsbShareService"
        private const val CHANNEL_ID = "usb_sharing"
        private const val NOTIFICATION_ID = 1

        const val ACTION_USB_PERMISSION = "de.usbserver.bridge.USB_PERMISSION"

        const val EXTRA_RELAY = "relay"
        const val EXTRA_TOKEN1 = "token1"
        const val EXTRA_TOKEN2 = "token2"
        const val EXTRA_TOKEN3 = "token3"
        const val EXTRA_NAME = "name"
    }
}
