package com.ledgrab.android

import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo
import android.media.projection.MediaProjection
import android.media.projection.MediaProjectionManager
import android.os.Build
import android.os.IBinder
import android.util.DisplayMetrics
import android.util.Log
import android.view.WindowManager
import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch

/**
 * Foreground service that runs the Python LedGrab server and captures
 * the screen via MediaProjection or root screenrecord.
 *
 * On Android 14+ the foreground-service "type" must match the work
 * being done. We promote the service with the correct type (mediaProjection
 * for the consent path, specialUse for the root path) instead of
 * declaring a single fixed type in the manifest — the manifest now
 * declares the *union* so promotion at runtime is permitted.
 */
class CaptureService : Service() {

    companion object {
        private const val TAG = "CaptureService"
        private const val CHANNEL_ID = "ledgrab_capture"
        private const val NOTIFICATION_ID = 1
        private const val EXTRA_RESULT_CODE = "result_code"
        private const val EXTRA_RESULT_DATA = "result_data"
        private const val EXTRA_USE_ROOT = "use_root"
        private const val SERVER_PORT = 8080
        private const val CAPTURE_WIDTH = 480
        private const val CAPTURE_HEIGHT = 270
        private const val CAPTURE_FPS = 30

        // Watchdog: if no new root-capture frames arrive inside this
        // window the pipeline is considered stalled and respawned.
        // Grace gives screenrecord time to produce its first I-frame.
        private const val WATCHDOG_GRACE_MS = 5_000L
        private const val WATCHDOG_CHECK_MS = 5_000L
        private const val WATCHDOG_MAX_RESTARTS = 3

        /** True while the service is alive.  Survives activity recreation. */
        @Volatile
        @JvmStatic
        var isRunning: Boolean = false
            private set

        fun createIntent(
            context: Context,
            resultCode: Int,
            resultData: Intent,
        ): Intent {
            return Intent(context, CaptureService::class.java).apply {
                putExtra(EXTRA_RESULT_CODE, resultCode)
                putExtra(EXTRA_RESULT_DATA, resultData)
            }
        }

        /** Root-mode intent — no MediaProjection consent data required. */
        fun createRootIntent(context: Context): Intent {
            return Intent(context, CaptureService::class.java).apply {
                putExtra(EXTRA_USE_ROOT, true)
            }
        }
    }

    private var bridge: PythonBridge? = null
    private var screenCapture: ScreenCapture? = null
    private var rootCapture: RootScreenrecord? = null
    private var mediaProjection: MediaProjection? = null

    // Service-scoped coroutine scope for the root-capture watchdog.
    // SupervisorJob so a watchdog crash doesn't tear down other children.
    private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
    private var watchdogJob: Job? = null

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

    override fun onCreate() {
        super.onCreate()
        createNotificationChannel()
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        val useRoot = intent?.getBooleanExtra(EXTRA_USE_ROOT, false) ?: false

        // CRITICAL: startForeground must be called IMMEDIATELY — before
        // any other work, especially before getMediaProjection(). The
        // service type must match the work; pass it explicitly via
        // ServiceCompat so we stay compatible back to API 24.
        val localIp = NetworkUtils.getLocalIpAddress(this) ?: "—"
        val url = "http://$localIp:$SERVER_PORT"
        try {
            val type = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
                if (useRoot) {
                    ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
                } else {
                    ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION
                }
            } else {
                0
            }
            ServiceCompat.startForeground(
                this,
                NOTIFICATION_ID,
                buildNotification(url),
                type,
            )
        } catch (e: Exception) {
            // Most common cause: missing foregroundServiceType permission
            // or denied POST_NOTIFICATIONS on API 33+.
            Log.e(TAG, "startForeground failed — service cannot run", e)
            stopSelf()
            return START_NOT_STICKY
        }
        // Only flip the public flag once the FG transition has succeeded,
        // otherwise `isRunning=true` sticks forever when startForeground throws.
        isRunning = true

        if (intent == null && !useRoot) {
            // MediaProjection mode can't recover from a redelivery —
            // the consent token in the original intent is single-use.
            Log.w(TAG, "Service restarted without intent (MediaProjection mode) — stopping")
            isRunning = false
            stopSelf()
            return START_NOT_STICKY
        }

        try {
            if (useRoot) {
                startRootCapture(url)
            } else {
                startMediaProjectionCapture(intent!!, url)
            }
        } catch (e: Exception) {
            Log.e(TAG, "Failed to start capture", e)
            isRunning = false
            stopSelf()
        }

        // Root mode can be cleanly restarted from the original intent
        // because it carries no perishable consent token. MediaProjection
        // mode cannot — a restart there would silently start the service
        // with a dead projection. START_NOT_STICKY there, the user has
        // to tap Start again.
        return if (useRoot) START_REDELIVER_INTENT else START_NOT_STICKY
    }

    private fun apiKey(): String? =
        (application as? LedGrabApp)?.apiKeyManager?.apiKey

    private fun startRootCapture(url: String) {
        val newBridge = PythonBridge(this).also { b ->
            b.configureRootCapture(CAPTURE_WIDTH, CAPTURE_HEIGHT)
            b.startServer(SERVER_PORT, apiKey())
        }
        bridge = newBridge

        val pipeline = RootScreenrecord(
            bridge = newBridge,
            width = CAPTURE_WIDTH,
            height = CAPTURE_HEIGHT,
            fps = CAPTURE_FPS,
        )
        if (!pipeline.start()) {
            Log.w(TAG, "Root capture failed to launch — stopping service")
            stopSelf()
            return
        }
        rootCapture = pipeline
        startWatchdog()
        Log.i(TAG, "LedGrab service started (root mode) — web UI at $url")
    }

    /**
     * Replace the active root pipeline with a fresh instance, reusing
     * the existing Python bridge (no server restart). Returns true if
     * the new pipeline launched, false otherwise.
     *
     * Synchronized so a concurrent onDestroy() either (a) sees the old
     * instance and stops it then null-out, or (b) sees the new instance
     * and stops it. There is no window where a fresh instance can be
     * orphaned with no one holding a reference to it.
     */
    @Synchronized
    private fun restartRootPipeline(): Boolean {
        val currentBridge = bridge ?: return false
        // Tear down the old instance first so we don't run two
        // screenrecord processes simultaneously fighting for the GPU.
        rootCapture?.let { old ->
            rootCapture = null
            runCatching { old.stop() }
        }

        val next = RootScreenrecord(
            bridge = currentBridge,
            width = CAPTURE_WIDTH,
            height = CAPTURE_HEIGHT,
            fps = CAPTURE_FPS,
        )
        // Publish BEFORE start() — if onDestroy fires after this
        // assignment but before start() completes, the field is non-null
        // and onDestroy will stop() it properly. start() is idempotent
        // enough (running=true, then resource construction) that being
        // raced by stop() at most produces a brief partial-init that
        // the next stop() call cleans up.
        rootCapture = next
        if (!next.start()) {
            Log.e(TAG, "Root capture failed to restart")
            // start() already called stop() on itself on the failure
            // path — but null out the field so the watchdog/onDestroy
            // don't try to stop it again.
            rootCapture = null
            return false
        }
        return true
    }

    /**
     * Monitor the root pipeline's frame counter.  If no new frames
     * arrive within one check window, respawn the pipeline.  Caps at
     * [WATCHDOG_MAX_RESTARTS] consecutive failures before giving up so
     * we don't burn battery forever on a device that genuinely can't
     * produce frames.
     */
    private fun startWatchdog() {
        watchdogJob?.cancel()
        watchdogJob = serviceScope.launch {
            delay(WATCHDOG_GRACE_MS)
            var last = rootCapture?.framesDelivered ?: 0
            var restartAttempts = 0
            while (isActive) {
                delay(WATCHDOG_CHECK_MS)
                val pipe = rootCapture ?: break
                val current = pipe.framesDelivered
                if (current == last) {
                    restartAttempts += 1
                    Log.w(
                        TAG,
                        "Root capture stalled (no new frames in ${WATCHDOG_CHECK_MS}ms); " +
                            "restart attempt $restartAttempts/$WATCHDOG_MAX_RESTARTS",
                    )
                    if (restartAttempts >= WATCHDOG_MAX_RESTARTS) {
                        Log.e(TAG, "Watchdog gave up after $WATCHDOG_MAX_RESTARTS restarts")
                        stopSelf()
                        return@launch
                    }
                    if (!restartRootPipeline()) {
                        stopSelf()
                        return@launch
                    }
                    last = rootCapture?.framesDelivered ?: 0
                } else {
                    // Forward progress — forgive earlier glitches.
                    restartAttempts = 0
                    last = current
                }
            }
        }
    }

    private fun startMediaProjectionCapture(intent: Intent, url: String) {
        val resultCode = intent.getIntExtra(EXTRA_RESULT_CODE, 0)
        @Suppress("DEPRECATION")
        val resultData = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            intent.getParcelableExtra(EXTRA_RESULT_DATA, Intent::class.java)
        } else {
            intent.getParcelableExtra(EXTRA_RESULT_DATA)
        }

        if (resultData == null) {
            Log.e(TAG, "No MediaProjection result data")
            stopSelf()
            return
        }

        val projectionManager =
            getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
        val projection = projectionManager.getMediaProjection(resultCode, resultData)
        if (projection == null) {
            Log.e(TAG, "Failed to create MediaProjection")
            stopSelf()
            return
        }
        mediaProjection = projection

        val metrics = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            val windowMetrics = (getSystemService(Context.WINDOW_SERVICE) as WindowManager)
                .currentWindowMetrics
            DisplayMetrics().apply {
                val bounds = windowMetrics.bounds
                widthPixels = bounds.width()
                heightPixels = bounds.height()
                densityDpi = resources.displayMetrics.densityDpi
            }
        } else {
            val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
            DisplayMetrics().also { m ->
                @Suppress("DEPRECATION")
                windowManager.defaultDisplay.getRealMetrics(m)
            }
        }

        val newBridge = PythonBridge(this).also { b ->
            b.configureCapture(CAPTURE_WIDTH, CAPTURE_HEIGHT)
            b.startServer(SERVER_PORT, apiKey())
        }
        bridge = newBridge

        screenCapture = ScreenCapture(
            projection = projection,
            metrics = metrics,
            bridge = newBridge,
            targetWidth = CAPTURE_WIDTH,
            targetHeight = CAPTURE_HEIGHT,
            targetFps = CAPTURE_FPS,
            // If the user taps the system Cast/Screen-capture stop banner,
            // MediaProjection.Callback.onStop fires — tear the whole
            // service down so the notification/Python server don't linger.
            onProjectionStopped = { stopSelf() },
        ).also { it.start() }

        Log.i(TAG, "LedGrab service started (MediaProjection) — web UI at $url")
    }

    override fun onDestroy() {
        isRunning = false

        watchdogJob?.cancel()
        watchdogJob = null
        serviceScope.cancel()

        screenCapture?.stop()
        screenCapture = null

        rootCapture?.stop()
        rootCapture = null

        bridge?.stopServer()
        bridge = null

        mediaProjection?.stop()
        mediaProjection = null

        Log.i(TAG, "LedGrab service destroyed")
        super.onDestroy()
    }

    private fun createNotificationChannel() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(
                CHANNEL_ID,
                getString(R.string.notification_channel_name),
                NotificationManager.IMPORTANCE_LOW,
            ).apply {
                description = getString(R.string.notification_channel_description)
            }
            val manager = getSystemService(NotificationManager::class.java)
            manager.createNotificationChannel(channel)
        }
    }

    private fun buildNotification(url: String): Notification {
        val tapIntent = PendingIntent.getActivity(
            this,
            0,
            Intent(this, MainActivity::class.java).apply {
                flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
            },
            PendingIntent.FLAG_IMMUTABLE,
        )
        return NotificationCompat.Builder(this, CHANNEL_ID)
            .setContentTitle(getString(R.string.notification_title))
            .setContentText(getString(R.string.notification_text, url))
            // ic_notification is a monochrome 24dp vector — status-bar
            // icons must be white-on-transparent or they render as a
            // gray blob on Android 5+.
            .setSmallIcon(R.drawable.ic_notification)
            .setColor(0xFF64FFDA.toInt())
            .setColorized(true)
            .setContentIntent(tapIntent)
            .setOngoing(true)
            .build()
    }
}
