package dev.privacyllc.period

import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.view.WindowManager
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.enableEdgeToEdge
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import dagger.hilt.android.AndroidEntryPoint
import dev.privacyllc.period.core.notifications.PeriodNotifier
import dev.privacyllc.period.core.security.AppLockRepository
import dev.privacyllc.period.designsystem.PeriodTheme
import dev.privacyllc.period.feature.export.ExportController
import dev.privacyllc.period.lock.AppLockController
import dev.privacyllc.period.lock.AppLockGate
import dev.privacyllc.period.navigation.PeriodRoot
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.launch
import javax.inject.Inject

/**
 * A [FragmentActivity], and not by preference.
 *
 * Every `BiometricPrompt` constructor in androidx.biometric 1.1.0 takes a
 * `FragmentActivity` or a `Fragment`; there is no `ComponentActivity` overload.
 * `FragmentActivity` extends `ComponentActivity`, so `enableEdgeToEdge`,
 * `setContent`, `@AndroidEntryPoint` and `hiltViewModel()` are all unaffected.
 * Only 1.4.0-alpha avoids this, and an alpha in the component that decides
 * whether the app opens is the wrong trade for this product.
 */
@AndroidEntryPoint
class MainActivity : FragmentActivity() {

    @Inject lateinit var lockController: AppLockController

    @Inject lateinit var appLock: AppLockRepository

    @Inject lateinit var exportController: ExportController

    /**
     * The save dialog, registered as an activity field.
     *
     * Registered here rather than in a composable because the picker leaves the
     * app entirely: the process can be killed while it is open, and coming back
     * can re-lock. An activity-scoped registration is restored by
     * `ActivityResultRegistry` from saved state, so the destination the user
     * chose still arrives. A launcher remembered inside the composition would
     * not survive the re-lock that tears that composition down.
     *
     * `CreateDocument("application/json")` and never the deprecated no-arg
     * constructor — that one requests a wildcard MIME type, and the picker then
     * declines to append the extension.
     */
    private val createExport =
        registerForActivityResult(ActivityResultContracts.CreateDocument("application/json")) { uri ->
            exportController.onDestination(uri)
        }

    override fun onCreate(savedInstanceState: Bundle?) {
        // Set BEFORE anything can be drawn, and cleared later only once the
        // store has confirmed there is no lock. Reading the preference first
        // would mean disk I/O on the startup path; starting insecure and
        // tightening afterwards would mean a frame of real content in the
        // recents thumbnail, which is exactly the leak this prevents.
        window.setFlags(
            WindowManager.LayoutParams.FLAG_SECURE,
            WindowManager.LayoutParams.FLAG_SECURE,
        )

        enableEdgeToEdge()
        super.onCreate(savedInstanceState)

        // Parked, never applied here. With a lock on, the write waits for the
        // unlock; the gate delivers it. See AppLockController.
        lockController.holdNotificationAction(intent?.getStringExtra(EXTRA_REMINDER_ACTION))

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                appLock.hasPin.collect(::applySecureFlag)
            }
        }

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                exportController.launchRequest.filterNotNull().collect { fileName ->
                    exportController.onPickerLaunched()
                    // Throws SYNCHRONOUSLY on managed profiles and some OEM
                    // builds where no document provider is reachable, so it
                    // cannot be left to the result callback.
                    try {
                        createExport.launch(fileName)
                    } catch (unavailable: ActivityNotFoundException) {
                        exportController.onFailed(partialFileLeft = false)
                    }
                }
            }
        }

        setContent {
            PeriodTheme {
                AppLockGate {
                    PeriodRoot()
                }
            }
        }
    }

    /**
     * A notification tapped while the app is already open comes here rather than
     * through `onCreate`, so without this the action would be dropped.
     */
    override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent)
        setIntent(intent)
        lockController.holdNotificationAction(intent.getStringExtra(EXTRA_REMINDER_ACTION))
    }

    /**
     * Conditioned on the lock being set, never on the build type.
     *
     * A debug-only exemption would mean the release-only behaviour is the one
     * nobody ever sees, and a recents leak is exactly the defect that hides
     * there. The consequence — no screenshots while the lock is on — is stated
     * in Settings so it is a choice rather than a surprise.
     */
    private fun applySecureFlag(locked: Boolean) {
        if (locked) {
            window.setFlags(
                WindowManager.LayoutParams.FLAG_SECURE,
                WindowManager.LayoutParams.FLAG_SECURE,
            )
        } else {
            window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
        }
    }

    private companion object {
        /**
         * The same key `core/notifications` writes, not a second copy of the
         * literal: the two sides of a notification tap have to agree, and a
         * repeated string is one rename away from an action that quietly stops
         * arriving.
         */
        const val EXTRA_REMINDER_ACTION = PeriodNotifier.EXTRA_REMINDER_ACTION
    }
}
