package com.ledgrab.android

import android.Manifest
import android.animation.ObjectAnimator
import android.animation.ValueAnimator
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.media.projection.MediaProjectionManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.PowerManager
import android.provider.Settings
import android.util.Log
import android.view.View
import android.view.ViewStub
import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.Button
import android.widget.CheckBox
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.ScrollView
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import com.google.zxing.BarcodeFormat
import com.google.zxing.qrcode.QRCodeWriter
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

/**
 * Main (and only) Activity for the Android TV app.
 *
 * Two-state UI: stopped (Start button) and running (URL + QR + Stop).
 * Navigable with D-pad / USB controller.
 */
class MainActivity : Activity() {

    // Activity-scoped coroutine scope. We don't depend on AppCompat /
    // androidx.lifecycle's lifecycleScope here because the TV launcher
    // theme inherits from Leanback (non-AppCompat).
    private val uiScope: CoroutineScope = MainScope()

    companion object {
        private const val TAG = "MainActivity"
        private const val SERVER_PORT = 8080
        private const val REQUEST_MEDIA_PROJECTION = 1001
        private const val REQUEST_POST_NOTIFICATIONS = 1002
        private const val QR_SIZE_PX = 560
    }

    // Stopped-state views (always inflated).
    private lateinit var stoppedPanel: View
    private lateinit var statusText: TextView
    private lateinit var toggleButton: Button
    private lateinit var versionText: TextView
    private lateinit var autostartCheck: CheckBox
    private lateinit var autostartPrefs: AutostartPrefs

    // Running-state views (lazy-inflated via ViewStub).
    private lateinit var runningPanelStub: ViewStub
    private var runningPanel: View? = null
    private var urlText: TextView? = null
    private var qrImage: ImageView? = null
    private var stopButtonRunning: Button? = null
    private var statusDot: View? = null
    private var statusDotAnimator: ObjectAnimator? = null

    // Cache of the most recently rendered QR (and the URL it encodes).
    // updateUI() runs on every onResume (HDMI-CEC wakes, app switches,
    // overlay dismissal, etc.). Rebuilding the 560×560 bitmap each time
    // is wasteful — usually the IP and key are unchanged. Cache and
    // short-circuit when the URL matches.
    private var cachedQrUrl: String? = null
    private var cachedQrBitmap: Bitmap? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        // Install the splash screen BEFORE super.onCreate so the system
        // keeps it on screen until our first frame is ready. This hides
        // the Chaquopy stdlib unpack delay on cold first launch.
        val splashScreen = installSplashScreen()
        super.onCreate(savedInstanceState)

        // Surface fatal Python init errors instead of crashing.
        val initError = (application as? LedGrabApp)?.initError
        if (initError != null) {
            // Tell the splash screen to dismiss immediately — we're
            // about to render an error screen, not the main UI.
            splashScreen.setKeepOnScreenCondition { false }
            showFatalErrorScreen(initError)
            return
        }

        setContentView(R.layout.activity_main)

        stoppedPanel = findViewById(R.id.stopped_panel)
        runningPanelStub = findViewById(R.id.running_panel_stub)
        statusText = findViewById(R.id.status_text)
        toggleButton = findViewById(R.id.toggle_button)
        versionText = findViewById(R.id.version_text)
        autostartCheck = findViewById(R.id.autostart_check)

        val versionName = packageManager.getPackageInfo(packageName, 0).versionName
        versionText.text = getString(R.string.version_prefix, versionName ?: "?")

        autostartPrefs = AutostartPrefs(this)
        // Autostart only takes effect on rooted devices. Hide the
        // checkbox entirely on unrooted hardware instead of showing a
        // disabled-but-visible control, which reads as broken UI from
        // across the room.
        if (Root.looksRooted()) {
            autostartCheck.visibility = View.VISIBLE
            autostartCheck.isChecked = autostartPrefs.isEnabled
            autostartCheck.setOnCheckedChangeListener { _, isChecked ->
                autostartPrefs.isEnabled = isChecked
                if (isChecked) ensureIgnoringBatteryOptimizations()
            }
        } else {
            autostartCheck.visibility = View.GONE
        }

        toggleButton.setOnClickListener { startCapture() }

        updateUI()
    }

    override fun onDestroy() {
        stopStatusDotPulse()
        uiScope.cancel()
        super.onDestroy()
    }

    override fun onStop() {
        // ObjectAnimator retains a hard reference to the dot View. On
        // backgrounded TV apps onDestroy may never fire, so cancel here
        // to avoid leaking the entire view hierarchy through an
        // INFINITE-repeat animator.
        stopStatusDotPulse()
        super.onStop()
    }

    override fun onResume() {
        super.onResume()
        // Restart the pulse if we returned to the foreground while the
        // service is still running. The running panel's view may have
        // been recreated; ensureRunningPanelInflated already keys off
        // the field reference.
        if (CaptureService.isRunning && ::stoppedPanel.isInitialized) {
            updateUI()
        }
    }

    /**
     * Decide whether to go through the MediaProjection consent flow or
     * jump straight into root capture. Root check is fast but may block
     * briefly the first time Magisk shows its grant dialog — running it
     * on the UI thread is acceptable because we're responding to a
     * button press and we want to block until the user answers.
     */
    private fun startCapture() {
        // `su -c id` can block for seconds while Magisk shows its grant
        // dialog; running it on the Main thread caused ANRs. Render an
        // explicit "starting" state so the button doesn't look frozen.
        val originalText = toggleButton.text
        toggleButton.isEnabled = false
        toggleButton.text = getString(R.string.btn_starting)
        statusText.text = getString(R.string.status_checking_root)
        uiScope.launch(Dispatchers.IO) {
            val rooted = Root.requestGrant()
            withContext(Dispatchers.Main) {
                toggleButton.isEnabled = true
                toggleButton.text = originalText
                statusText.text = ""
                if (rooted) {
                    Log.i(TAG, "Root available — skipping MediaProjection consent")
                    startRootCaptureService()
                } else {
                    requestMediaProjection()
                }
            }
        }
    }

    private fun requestMediaProjection() {
        val manager = getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
        @Suppress("DEPRECATION")
        startActivityForResult(manager.createScreenCaptureIntent(), REQUEST_MEDIA_PROJECTION)
    }

    private fun startRootCaptureService() {
        ensureNotificationPermission()
        ContextCompat.startForegroundService(this, CaptureService.createRootIntent(this))
        updateUI()
    }

    @Deprecated("Using deprecated API for plain Activity compatibility")
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == REQUEST_MEDIA_PROJECTION) {
            if (resultCode == RESULT_OK && data != null) {
                startCaptureService(resultCode, data)
            } else {
                statusText.text = getString(R.string.status_permission_denied)
                Log.w(TAG, "MediaProjection permission denied")
            }
        }
    }

    private fun startCaptureService(resultCode: Int, resultData: Intent) {
        ensureNotificationPermission()
        val intent = CaptureService.createIntent(this, resultCode, resultData)
        ContextCompat.startForegroundService(this, intent)
        updateUI()
    }

    private fun stopCaptureService() {
        stopService(Intent(this, CaptureService::class.java))
        updateUI()
    }

    private fun ensureRunningPanelInflated(): View {
        runningPanel?.let { return it }
        val view = runningPanelStub.inflate()
        urlText = view.findViewById(R.id.url_text)
        qrImage = view.findViewById(R.id.qr_image)
        stopButtonRunning = view.findViewById(R.id.stop_button_running)
        statusDot = view.findViewById(R.id.status_dot)
        stopButtonRunning?.setOnClickListener { stopCaptureService() }
        runningPanel = view
        return view
    }

    private fun updateUI() {
        // Fatal-init-error path took over setContentView and the
        // lateinit view fields are unassigned. Guard so any future
        // caller (Resume, broadcast receiver, etc.) doesn't NPE.
        if (!::stoppedPanel.isInitialized) return
        if (CaptureService.isRunning) {
            val running = ensureRunningPanelInflated()
            val localIp = NetworkUtils.getLocalIpAddress(this)
            if (localIp == null) {
                // No network — show the no-network state inside the
                // stopped panel and keep capture stopped. The service
                // is alive (capture works on loopback) but the URL/QR
                // are useless without a routable address.
                statusText.text = getString(R.string.status_no_network)
                stoppedPanel.visibility = View.VISIBLE
                versionText.visibility = View.VISIBLE
                running.visibility = View.GONE
                toggleButton.requestFocus()
                return
            }

            val displayUrl = "http://$localIp:$SERVER_PORT"
            val qrUrl = qrUrlFor(displayUrl)

            urlText?.text = displayUrl
            val cachedForUrl = cachedQrBitmap?.takeIf { cachedQrUrl == qrUrl }
            if (cachedForUrl != null) {
                qrImage?.setImageBitmap(cachedForUrl)
            } else {
                qrImage?.setImageBitmap(null)
                // Build the bitmap pixels off the Main thread — encode + 313k
                // setPixel calls were noticeably janky on slow TV boxes.
                uiScope.launch(Dispatchers.Default) {
                    val bitmap = generateQrCode(qrUrl)
                    withContext(Dispatchers.Main) {
                        if (CaptureService.isRunning && urlText?.text == displayUrl) {
                            cachedQrUrl = qrUrl
                            cachedQrBitmap = bitmap
                            qrImage?.setImageBitmap(bitmap)
                        }
                    }
                }
            }

            stoppedPanel.visibility = View.GONE
            versionText.visibility = View.GONE
            running.visibility = View.VISIBLE
            stopButtonRunning?.requestFocus()
            startStatusDotPulse()
        } else {
            stopStatusDotPulse()
            urlText?.text = ""
            qrImage?.setImageBitmap(null)
            // Drop the cached bitmap so a Start → IP change → Start
            // sequence rebuilds the QR for the new address.
            cachedQrUrl = null
            cachedQrBitmap = null

            runningPanel?.visibility = View.GONE
            stoppedPanel.visibility = View.VISIBLE
            versionText.visibility = View.VISIBLE
            toggleButton.requestFocus()
        }
    }

    /**
     * Build the URL we encode into the QR. Embeds the API key as a
     * URL fragment (``#k=<token>``) so:
     *   - The token never appears in HTTP requests (fragments aren't
     *     sent over the wire) — no access-log leak.
     *   - The frontend can read [location.hash] on first visit and
     *     persist the key to localStorage (see static/js/app.ts).
     *   - The visible URL chip stays short and human-readable.
     *
     * The chip text in [updateUI] intentionally uses the *base* URL
     * (without the fragment) so a human reading the URL out loud
     * doesn't have to dictate 64 hex chars; only the QR carries the
     * key. Do not collapse these into a single string — that would
     * leak the key onto the screen.
     */
    private fun qrUrlFor(base: String): String {
        val key = (application as? LedGrabApp)?.apiKeyManager?.apiKey
        return if (key.isNullOrBlank()) base else "$base/#k=$key"
    }

    private fun startStatusDotPulse() {
        val dot = statusDot ?: return
        if (statusDotAnimator?.isStarted == true) return
        val animator = ObjectAnimator.ofFloat(dot, "alpha", 1f, 0.35f).apply {
            duration = 900
            repeatCount = ValueAnimator.INFINITE
            repeatMode = ValueAnimator.REVERSE
            interpolator = AccelerateDecelerateInterpolator()
        }
        animator.start()
        statusDotAnimator = animator
    }

    private fun stopStatusDotPulse() {
        statusDotAnimator?.cancel()
        statusDotAnimator = null
        statusDot?.alpha = 1f
    }

    private fun generateQrCode(text: String): Bitmap {
        val size = QR_SIZE_PX
        val bitMatrix = QRCodeWriter().encode(text, BarcodeFormat.QR_CODE, size, size)
        // ALPHA_8 = 1 byte/px instead of 2 (RGB_565) or 4 (ARGB_8888).
        // The ImageView gets tinted white via the matrix — for a pure
        // black-and-white QR that's all we need and it halves heap usage
        // compared to the previous RGB_565 path.
        val bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
        val pixels = IntArray(size * size)
        for (y in 0 until size) {
            val rowOffset = y * size
            for (x in 0 until size) {
                pixels[rowOffset + x] =
                    if (bitMatrix[x, y]) 0xFF000000.toInt() else 0xFFFFFFFF.toInt()
            }
        }
        bitmap.setPixels(pixels, 0, size, 0, 0, size, size)
        return bitmap
    }

    /**
     * Minimal failure UI shown when Python.start() (Chaquopy) blew up.
     * Stack trace is hidden behind a "Show details" toggle so we don't
     * print user-path data on shared TV screens by default.
     */
    private fun showFatalErrorScreen(error: Throwable) {
        Log.e(TAG, "Fatal init error — showing error screen", error)
        val stackText = Log.getStackTraceString(error)
        val container = LinearLayout(this).apply {
            orientation = LinearLayout.VERTICAL
            setPadding(48, 48, 48, 48)
        }
        val title = TextView(this).apply {
            text = getString(R.string.fatal_title)
            textSize = 22f
        }
        val description = TextView(this).apply {
            text = getString(R.string.fatal_body_prefix)
            textSize = 14f
            setPadding(0, 24, 0, 12)
        }
        val body = TextView(this).apply {
            text = stackText
            textSize = 12f
            setTextIsSelectable(true)
            visibility = View.GONE
        }
        val scroll = ScrollView(this).apply {
            addView(body)
            visibility = View.GONE
        }
        val toggleBtn = Button(this).apply {
            text = getString(R.string.fatal_show_details)
            setOnClickListener {
                val showing = scroll.visibility == View.VISIBLE
                scroll.visibility = if (showing) View.GONE else View.VISIBLE
                body.visibility = scroll.visibility
                text = getString(
                    if (showing) R.string.fatal_show_details else R.string.fatal_hide_details,
                )
            }
        }
        val copyBtn = Button(this).apply {
            text = getString(R.string.fatal_copy_log)
            setOnClickListener {
                val cm = getSystemService(CLIPBOARD_SERVICE)
                    as android.content.ClipboardManager
                cm.setPrimaryClip(
                    android.content.ClipData.newPlainText("LedGrab error", stackText)
                )
            }
        }
        container.addView(title)
        container.addView(description)
        container.addView(toggleBtn)
        container.addView(copyBtn)
        container.addView(scroll)
        setContentView(container)
    }

    /**
     * Prompt the user to exempt LedGrab from battery optimization.
     * Strictly a phone-side concern (Doze/App Standby kill the FG
     * service after hours of sleep); essentially a no-op on TV boxes.
     * Only asked when autostart is turned on, which is itself only
     * available on rooted devices.
     *
     * Play Store flags REQUEST_IGNORE_BATTERY_OPTIMIZATIONS by default
     * — LedGrab's ambient-capture use case falls under the documented
     * acceptable-use exceptions (a foreground service that must not be
     * killed to fulfill its primary function).
     */
    @SuppressLint("BatteryLife")
    private fun ensureIgnoringBatteryOptimizations() {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return
        val pm = getSystemService(POWER_SERVICE) as PowerManager
        if (pm.isIgnoringBatteryOptimizations(packageName)) return
        try {
            val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
                data = Uri.parse("package:$packageName")
            }
            startActivity(intent)
        } catch (e: Exception) {
            // Some TV-box OEM builds strip this intent. Fall back to the
            // generic settings screen so the user can find it manually.
            Log.w(TAG, "Direct exemption intent unavailable: ${e.message}")
            runCatching {
                startActivity(Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS))
            }
        }
    }

    /**
     * Request POST_NOTIFICATIONS permission on Android 13+ so the
     * foreground service notification is visible.  On older API levels
     * this is a no-op.
     */
    private fun ensureNotificationPermission() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            if (checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS)
                != PackageManager.PERMISSION_GRANTED
            ) {
                @Suppress("DEPRECATION")
                requestPermissions(
                    arrayOf(Manifest.permission.POST_NOTIFICATIONS),
                    REQUEST_POST_NOTIFICATIONS,
                )
            }
        }
    }
}
