package dev.privacyllc.period.lock

import androidx.activity.compose.LocalActivity
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import dev.privacyllc.period.feature.lock.LockScreen

/**
 * The lock, wrapped around everything.
 *
 * ## Why it wraps the composition rather than being a screen
 *
 * [content] is invoked **only** in the unlocked branch, and that is the whole
 * design. Every one of Today, Calendar and Insights starts collecting from
 * `CycleRepository` the moment it composes, so a lock implemented as a
 * navigation destination — even the start destination — would have already
 * built the ViewModels and read the history before the user proved anything.
 * There is no lazy tab to hide behind.
 *
 * ## Re-locking, and the two guards that make it usable
 *
 * `ON_STOP`, not `ON_PAUSE`. Pause fires for the notification shade, quick
 * settings, a permission dialog and losing focus in split-screen — re-locking on
 * any of those makes the app unusable rather than secure.
 *
 * Even on stop there are two cases that must not re-lock:
 *
 * - **A configuration change.** Rotation, a theme switch and a font-scale change
 *   all stop and recreate the activity. This project actively tests at font
 *   scale 2.0, so without this guard the accessibility pass would fight the lock.
 * - **The biometric prompt.** It is a system window, and on some devices it
 *   stops the activity underneath. Without this guard, a successful fingerprint
 *   returns to a locked screen and the next attempt does the same — a loop with
 *   no way out but the PIN.
 *
 * Deliberately no grace period. `SECURITY.md`'s threat model leads with
 * "someone who picks up an unlocked phone", and a grace period is precisely the
 * window that person uses.
 */
@Composable
fun AppLockGate(
    viewModel: AppLockViewModel = hiltViewModel(),
    content: @Composable () -> Unit,
) {
    val state by viewModel.state.collectAsStateWithLifecycle()
    val activity = LocalActivity.current
    val lifecycleOwner = LocalLifecycleOwner.current

    DisposableEffect(lifecycleOwner, activity) {
        val observer = LifecycleEventObserver { _, event ->
            if (event != Lifecycle.Event.ON_STOP) return@LifecycleEventObserver
            if (activity?.isChangingConfigurations == true) return@LifecycleEventObserver
            if (viewModel.authInProgress) return@LifecycleEventObserver
            viewModel.relock()
        }
        lifecycleOwner.lifecycle.addObserver(observer)
        onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
    }

    when (state) {
        // One frame, drawing nothing. Same choice as RootState.Loading in
        // PeriodApp.kt: a spinner that appears for 16ms reads as jank. Crucially
        // it is not the unlocked branch, so no cycle data composes while the
        // stores are still answering.
        LockState.Unknown -> Unit

        LockState.Locked -> LockScreen(viewModel = viewModel)

        LockState.Unlocked -> {
            // Drains any notification action parked while the lock was closed.
            // Collected here rather than in the view model's own scope, so it
            // can only fire while the unlocked branch is on screen.
            val pending by viewModel.pendingNotificationAction.collectAsStateWithLifecycle()
            LaunchedEffect(pending) {
                if (pending != null) viewModel.deliverPendingNotificationAction()
            }
            content()
        }
    }
}
