package dev.privacyllc.period.feature.lock

import android.content.res.Configuration
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.LiveRegionMode
import androidx.compose.ui.semantics.liveRegion
import androidx.compose.ui.semantics.password
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.activity.compose.LocalActivity
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import dev.privacyllc.period.designsystem.PeriodTheme
import dev.privacyllc.period.lock.AppLockViewModel
import dev.privacyllc.period.lock.rememberBiometricUnlock
import dev.privacyllc.period.lock.LockScreenState
import kotlinx.coroutines.delay

/**
 * What somebody sees before they have proved who they are.
 *
 * ## It says almost nothing, on purpose
 *
 * This is the one screen in the app that is **semi-public**: it is what appears
 * the instant the owner opens the app in front of somebody else, and what
 * anybody who picks the phone up will see. So it carries none of
 * `NotificationCopy.SENSITIVE_WORDS` — no "period", no "cycle", no "fertility" —
 * and there is a unit test asserting exactly that, because this is copy that
 * gets edited later by somebody who has forgotten why it is bare.
 *
 * There is also no illustration. `docs/design/README.md` warns that a padlock
 * motif "would read as a security product rather than a calm one", and the
 * lock screen is where that temptation is strongest.
 *
 * ## Why an ordinary text field and not a custom keypad
 *
 * A hand-rolled keypad would let the PIN live in a `CharArray` that can be
 * zeroed, where a `TextField` hands back an immutable `String` that cannot. That
 * is a real difference and it is the wrong trade here. The zeroing is
 * best-effort at most — the JVM may have copied the buffer during GC — while the
 * costs are concrete: TalkBack does not read a custom grid the way it reads a
 * password field, and a 3×4 grid does not fit at font scale 2.0, which this
 * project has already shipped a defect against once. The documented adversary is
 * somebody holding the phone, not somebody dumping its heap; that person would
 * read the unencrypted database instead.
 */
@Composable
fun LockScreen(viewModel: AppLockViewModel) {
    val state by viewModel.screen.collectAsStateWithLifecycle()
    val biometricAllowed by viewModel.biometricAllowed.collectAsStateWithLifecycle()
    var erasing by remember { mutableStateOf(false) }

    if (erasing) {
        ForgotPinScreen(onCancel = { erasing = false })
        return
    }

    // Offered on a tap, never fired automatically. An auto-prompt on every
    // appearance fights the user who wants the PIN, and on a device that
    // returns an immediate error it becomes a loop with nothing to press.
    val biometric = rememberBiometricUnlock(
        activity = LocalActivity.current as? FragmentActivity,
        onBeginAuth = viewModel::beginBiometricAuth,
        onEndAuth = viewModel::endBiometricAuth,
        onUnlocked = viewModel::unlockFromBiometric,
        onUnavailable = viewModel::disableBiometric,
    )

    LockScreenContent(
        state = state,
        biometricAllowed = biometricAllowed && biometric.available,
        onSubmit = viewModel::submit,
        onTick = viewModel::refreshLockout,
        onForgot = { erasing = true },
        onUseBiometric = biometric.prompt,
    )
}

@Composable
internal fun LockScreenContent(
    state: LockScreenState,
    biometricAllowed: Boolean,
    onSubmit: (CharArray) -> Unit,
    onTick: () -> Unit,
    onForgot: () -> Unit,
    onUseBiometric: () -> Unit = {},
) {
    var pin by remember { mutableStateOf("") }
    // Reset on every entry, never remembered. A "show PIN" left on from last
    // time would reveal the next one to whoever is standing there.
    var revealed by remember { mutableStateOf(false) }

    val lockedOut = state.waitMillis > 0L

    // Ticks only while a lockout is running, and only once a second — a
    // per-frame countdown would make TalkBack announce continuously.
    LaunchedEffect(lockedOut) {
        while (lockedOut) {
            delay(1_000)
            onTick()
        }
    }

    Surface(modifier = Modifier.fillMaxSize()) {
        Column(
            modifier = Modifier
                .fillMaxSize()
                .verticalScroll(rememberScrollState())
                .padding(24.dp),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally,
        ) {
            Text(
                text = LockCopy.TITLE,
                style = MaterialTheme.typography.headlineMedium,
                textAlign = TextAlign.Center,
            )

            Spacer(Modifier.height(24.dp))

            OutlinedTextField(
                value = pin,
                onValueChange = { entered -> pin = entered.filter(Char::isDigit).take(MAX_PIN) },
                singleLine = true,
                enabled = !state.checking && !lockedOut && !state.keyUnavailable,
                label = { Text(LockCopy.FIELD_LABEL) },
                visualTransformation =
                    if (revealed) VisualTransformation.None else PasswordVisualTransformation(),
                keyboardOptions = KeyboardOptions(
                    keyboardType = KeyboardType.NumberPassword,
                    imeAction = ImeAction.Done,
                ),
                keyboardActions = KeyboardActions(onDone = { submit(pin, onSubmit) { pin = "" } }),
                // Tells TalkBack this is a password so it is not read aloud, and
                // keeps it out of autofill.
                modifier = Modifier
                    .fillMaxWidth()
                    .semantics { password() },
            )

            TextButton(onClick = { revealed = !revealed }) {
                Text(if (revealed) LockCopy.HIDE else LockCopy.SHOW)
            }

            Spacer(Modifier.height(8.dp))

            Message(state = state)

            Spacer(Modifier.height(16.dp))

            Button(
                onClick = { submit(pin, onSubmit) { pin = "" } },
                enabled = pin.length >= MIN_PIN && !state.checking && !lockedOut && !state.keyUnavailable,
                modifier = Modifier.fillMaxWidth(),
            ) {
                Text(LockCopy.UNLOCK)
            }

            if (biometricAllowed && !lockedOut && !state.keyUnavailable) {
                TextButton(onClick = onUseBiometric, modifier = Modifier.fillMaxWidth()) {
                    Text(LockCopy.USE_BIOMETRIC)
                }
            }

            Spacer(Modifier.height(24.dp))

            TextButton(onClick = onForgot) {
                Text(LockCopy.FORGOT, style = MaterialTheme.typography.bodyMedium)
            }
        }
    }
}

private inline fun submit(pin: String, onSubmit: (CharArray) -> Unit, clear: () -> Unit) {
    if (pin.length < MIN_PIN) return
    onSubmit(pin.toCharArray())
    clear()
}

/**
 * The one place the screen says anything, and it never says how many attempts
 * are left.
 *
 * A remaining-attempts count tells an attacker how much room they have and
 * tells the owner they are about to lose everything — and since nothing is ever
 * erased automatically, it would also be a lie.
 */
@Composable
private fun Message(state: LockScreenState) {
    val text = when {
        state.keyUnavailable -> LockCopy.KEY_UNAVAILABLE
        state.waitMillis > 0L -> LockCopy.tryAgainIn(formatWait(state.waitMillis))
        state.wrong -> LockCopy.WRONG
        else -> null
    } ?: return

    Text(
        text = text,
        style = MaterialTheme.typography.bodyMedium,
        color = MaterialTheme.colorScheme.error,
        textAlign = TextAlign.Center,
        // Announced when it changes, rather than on every countdown tick.
        modifier = Modifier
            .fillMaxWidth()
            .semantics { liveRegion = LiveRegionMode.Polite },
    )
}

internal fun formatWait(millis: Long): String {
    val totalSeconds = ((millis + 999) / 1000).toInt()
    val minutes = totalSeconds / 60
    val seconds = totalSeconds % 60
    return if (minutes > 0) "$minutes:${seconds.toString().padStart(2, '0')}" else "${seconds}s"
}

private const val MIN_PIN = 4
private const val MAX_PIN = 12

@Preview(name = "Lock · light", showBackground = true)
@Preview(name = "Lock · dark", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(name = "Lock · font 2.0", showBackground = true, fontScale = 2.0f)
@Composable
private fun LockPreview() {
    PeriodTheme {
        LockScreenContent(
            state = LockScreenState(wrong = true),
            biometricAllowed = true,
            onSubmit = {},
            onTick = {},
            onForgot = {},
        )
    }
}

@Preview(name = "Lock · waiting", showBackground = true)
@Preview(name = "Lock · waiting, font 2.0", showBackground = true, fontScale = 2.0f)
@Composable
private fun LockWaitingPreview() {
    PeriodTheme {
        LockScreenContent(
            state = LockScreenState(waitMillis = 125_000L),
            biometricAllowed = false,
            onSubmit = {},
            onTick = {},
            onForgot = {},
        )
    }
}
