package com.onthelevel.feature.level

import android.os.Build
import android.view.HapticFeedbackConstants
import android.view.View
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
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.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
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.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.BaselineShift
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.em
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.onthelevel.AppContainer
import com.onthelevel.core.design.KeepScreenOn
import com.onthelevel.core.design.LevelColors
import com.onthelevel.core.sensors.EdgeCalibration
import com.onthelevel.core.sensors.LevelMode
import com.onthelevel.core.sensors.LockDetector
import com.onthelevel.core.sensors.RiseRun
import com.onthelevel.core.sensors.SensorSource
import com.onthelevel.core.sensors.SurfaceCalibration
import com.onthelevel.core.sensors.SurfaceGuidance
import com.onthelevel.core.sensors.SurfacePresentation
import com.onthelevel.core.settings.AudioAssistMode
import com.onthelevel.core.settings.MeasurementUnits
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import java.util.Locale

/**
 * Scaffold Level screen: live numbers, manual Surface|Edge selector, lock state.
 * TODO(feature): the tactile cross-vial visual (spring-animated display value),
 * the brief lime lock reaction, audio cue, and the calibration flow entry.
 * TODO(feature): in Edge mode, counter-rotate the readout so it reads upright
 * while the portrait-locked device stands on its long edge.
 */
@Composable
fun LevelScreen(container: AppContainer, onOpenSettings: () -> Unit) {
    KeepScreenOn()

    val sensorSource = container.sensorSource
    if (!sensorSource.isAvailable) {
        UnsupportedDeviceMessage()
        return
    }

    var mode by rememberSaveable { mutableStateOf(LevelMode.SURFACE) }
    val surfaceCal by container.settings.surfaceCalibration
        .collectAsStateWithLifecycle(initialValue = SurfaceCalibration.NONE)
    val edgeCal by container.settings.edgeCalibration
        .collectAsStateWithLifecycle(initialValue = EdgeCalibration.NONE)
    val hapticsEnabled by container.settings.hapticsEnabled
        .collectAsStateWithLifecycle(initialValue = true)
    val audioMode by container.settings.audioAssistMode
        .collectAsStateWithLifecycle(initialValue = AudioAssistMode.OFF)
    val reducedMotion by container.settings.reducedMotion
        .collectAsStateWithLifecycle(initialValue = false)
    val measurementUnits by container.settings.measurementUnits
        .collectAsStateWithLifecycle(initialValue = MeasurementUnits.METRIC)

    val view = LocalView.current
    val lockDetector = remember { LockDetector() }
    LaunchedEffect(mode) { lockDetector.reset() }

    val readingFlow = remember(mode, surfaceCal, edgeCal, hapticsEnabled, view) {
        val pipeline = LevelPipeline(mode, surfaceCal, edgeCal, lockDetector)
        sensorSource.gravity
            .map { pipeline.process(it) }
            .onEach { if (it.fireFeedback && hapticsEnabled) view.performConfirmHaptic() }
    }
    val reading by readingFlow.collectAsStateWithLifecycle(initialValue = null)
    AudioLevelAssist(reading, audioMode)

    val isLocked = reading?.isLocked == true
    val isCalibrated = when (mode) {
        LevelMode.SURFACE -> surfaceCal != SurfaceCalibration.NONE
        LevelMode.EDGE -> edgeCal != EdgeCalibration.NONE
    }

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(horizontal = 24.dp, vertical = 16.dp),
    ) {
        Row(
            modifier = Modifier.fillMaxWidth(),
            horizontalArrangement = Arrangement.SpaceBetween,
            verticalAlignment = Alignment.CenterVertically,
        ) {
            Column {
                Text("LEVEL", style = MaterialTheme.typography.labelSmall, color = LevelColors.TextDim)
                Text(
                    text = statusLine(isCalibrated, sensorSource.kind),
                    style = MaterialTheme.typography.labelSmall,
                    color = if (isCalibrated) LevelColors.LimeLock else LevelColors.TextFaint,
                )
            }
            Row(verticalAlignment = Alignment.CenterVertically) {
                // Tap cycles Off -> Tones -> Voice. Lime when active (a confirmed on-state).
                TextButton(onClick = {
                    val next = when (audioMode) {
                        AudioAssistMode.OFF -> AudioAssistMode.TONES
                        AudioAssistMode.TONES -> AudioAssistMode.VOICE
                        AudioAssistMode.VOICE -> AudioAssistMode.OFF
                    }
                    container.applicationScope.launch { container.settings.setAudioAssistMode(next) }
                }) {
                    Text(
                        text = when (audioMode) {
                            AudioAssistMode.OFF -> "AUDIO OFF"
                            AudioAssistMode.TONES -> "TONES"
                            AudioAssistMode.VOICE -> "VOICE"
                        },
                        style = MaterialTheme.typography.labelSmall,
                        color = if (audioMode == AudioAssistMode.OFF) LevelColors.TextDim else LevelColors.LimeLockText,
                    )
                }
                IconButton(onClick = onOpenSettings) {
                    Icon(
                        Icons.Outlined.Settings,
                        contentDescription = "Settings",
                        tint = LevelColors.TextDim,
                    )
                }
            }
        }

        SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth().padding(top = 12.dp)) {
            LevelMode.entries.forEachIndexed { index, entry ->
                SegmentedButton(
                    selected = mode == entry,
                    onClick = { mode = entry },
                    shape = SegmentedButtonDefaults.itemShape(index = index, count = LevelMode.entries.size),
                ) {
                    Text(if (entry == LevelMode.SURFACE) "Surface" else "Edge")
                }
            }
        }

        Box(
            modifier = Modifier
                .weight(1f)
                .fillMaxWidth(),
            contentAlignment = Alignment.Center,
        ) {
            val placementOk = reading?.placementOk ?: true
            val surfacePresentation = reading?.surfacePresentation
            Column(
                modifier = Modifier.offset(y = 16.dp),
                horizontalAlignment = Alignment.CenterHorizontally,
            ) {
                if (mode == LevelMode.SURFACE && surfacePresentation == SurfacePresentation.FACE_UP) {
                    val pitch = reading?.stableSurfacePitchDegrees
                    val roll = reading?.stableSurfaceRollDegrees
                    if (pitch != null && roll != null) {
                        SurfaceBullseye(pitch, roll, isLocked, reducedMotion)
                    }
                }
                Text(
                    text = reading?.let { primaryReadout(formatDegrees(it.displayPrimaryDegrees)) }
                        ?: AnnotatedString("—"),
                    style = MaterialTheme.typography.displayLarge,
                    color = LevelColors.TextPrimary,
                )
                // Lock state is announced with a label, never color alone (BRIEF.md).
                // The label states the tolerance (the lock's exit threshold), so the
                // rounded readout and the "level" claim can never contradict.
                Text(
                    text = when {
                        mode == LevelMode.SURFACE && surfacePresentation == SurfacePresentation.SCREEN_DOWN ->
                            "Screen facing down · turn phone screen up"
                        mode == LevelMode.SURFACE && surfacePresentation == SurfacePresentation.NEAR_VERTICAL ->
                            "Near vertical · pitch and roll unavailable"
                        !placementOk && mode == LevelMode.SURFACE -> "Lay the phone flat, screen up"
                        !placementOk -> "Stand the phone upright on a long edge"
                        isLocked && mode == LevelMode.SURFACE -> "Flat within ${formatTolerance()}"
                        isLocked -> "Level within ${formatTolerance()}"
                        else -> ""
                    },
                    style = MaterialTheme.typography.headlineMedium,
                    color = when {
                        !placementOk -> LevelColors.TextDim
                        isLocked -> LevelColors.LimeLockText
                        else -> LevelColors.TextPrimary
                    },
                    textAlign = TextAlign.Center,
                )
                if (mode == LevelMode.SURFACE && surfacePresentation == SurfacePresentation.FACE_UP) {
                    Spacer(Modifier.height(8.dp))
                    // This slot stays the same height for Settling, guidance, and
                    // level states so a stable phone never makes the instrument
                    // jump as its correction copy changes.
                    Box(
                        modifier = Modifier
                            .fillMaxWidth()
                            .height(56.dp),
                        contentAlignment = Alignment.TopCenter,
                    ) {
                        // Guidance and the status label are mutually exclusive
                        // (guidance renders only while the label line is empty),
                        // so it rises to visually take the label's place.
                        SurfaceAdjustmentInfo(
                            reading,
                            measurementUnits,
                            Modifier.offset(y = (-36).dp),
                        )
                    }
                }
            }
        }

        Row(
            modifier = Modifier.fillMaxWidth(),
            horizontalArrangement = Arrangement.spacedBy(14.dp),
        ) {
            ValuePanel(
                label = if (mode == LevelMode.SURFACE) "PITCH" else "LEVEL",
                value = reading?.secondaryADegrees,
                modifier = Modifier.weight(1f),
            )
            ValuePanel(
                label = if (mode == LevelMode.SURFACE) "ROLL" else "LEAN",
                value = reading?.secondaryBDegrees,
                modifier = Modifier.weight(1f),
            )
        }
    }
}

/**
 * Surface-only adjustment guidance. Direction comes exclusively from
 * [SurfaceGuidance] using the displayed (deadbanded) axes, so the wording has
 * the same hysteresis as the pitch and roll panels instead of flapping at a
 * direction boundary. The shared pipeline settling gate deliberately defers a
 * strong instruction while the phone is still being placed.
 */
@Composable
private fun SurfaceAdjustmentInfo(
    reading: LevelReading?,
    units: MeasurementUnits,
    modifier: Modifier = Modifier,
) {
    Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) {
        if (reading == null) return@Column

        // A locked surface is flat within tolerance: issuing correction guidance
        // under the lock line would contradict it and crowd the status stack.
        if (reading.isLocked) return@Column

        if (reading.isSettling) {
            Text(
                text = "Settling",
                style = MaterialTheme.typography.labelSmall,
                color = LevelColors.TextDim,
            )
            return@Column
        }

        val pitch = reading.secondaryADegrees ?: return@Column
        val roll = reading.secondaryBDegrees ?: return@Column
        val highLabel = SurfaceGuidance.from(pitch, roll).highLabel ?: return@Column

        // One rank below the lock/status line (28sp mono): guidance advises, it
        // doesn't compete.
        Text(
            text = highLabel,
            style = MaterialTheme.typography.titleLarge,
            color = LevelColors.Amber,
            textAlign = TextAlign.Center,
        )
        Text(
            text = formatRiseRun(reading.displayPrimaryDegrees, units),
            style = MaterialTheme.typography.labelSmall,
            color = LevelColors.TextDim,
        )
    }
}

@Composable
private fun ValuePanel(label: String, value: Double?, modifier: Modifier = Modifier) {
    Surface(
        modifier = modifier,
        color = LevelColors.ReadoutPanel,
        contentColor = LevelColors.TextPrimary,
        shape = MaterialTheme.shapes.large,
    ) {
        Column(
            modifier = Modifier.padding(16.dp),
            horizontalAlignment = Alignment.CenterHorizontally,
        ) {
            Text(
                label,
                modifier = Modifier.fillMaxWidth(),
                style = MaterialTheme.typography.labelSmall,
                color = LevelColors.TextDim,
                textAlign = TextAlign.Center,
            )
            Text(
                text = value?.let { formatDegrees(it) } ?: "—",
                modifier = Modifier.fillMaxWidth(),
                style = MaterialTheme.typography.headlineMedium,
                textAlign = TextAlign.Center,
            )
        }
    }
}

@Composable
private fun UnsupportedDeviceMessage() {
    Box(modifier = Modifier.fillMaxSize().padding(32.dp), contentAlignment = Alignment.Center) {
        Text(
            text = "This device has no usable tilt sensor, so On the Level can't take measurements here.",
            style = MaterialTheme.typography.headlineMedium,
            textAlign = TextAlign.Center,
            color = LevelColors.TextDim,
        )
    }
}

private fun statusLine(isCalibrated: Boolean, kind: SensorSource.Kind): String {
    val source = when (kind) {
        SensorSource.Kind.GAME_ROTATION_VECTOR -> "fused"
        SensorSource.Kind.GRAVITY -> "gravity"
        SensorSource.Kind.ACCELEROMETER -> "accelerometer"
        SensorSource.Kind.NONE -> "no sensor"
    }
    return (if (isCalibrated) "calibrated" else "live") + " · " + source
}

internal fun formatDegrees(value: Double): String = String.format(Locale.US, "%.1f°", value)

/**
 * Primary readout typography: the degree symbol rendered small and faint keeps the
 * number elegant without losing the instrument voice — and its invisible leading
 * twin balances the trailing one, so the digits sit optically dead-center no
 * matter the glyph widths.
 */
private fun primaryReadout(text: String): AnnotatedString = buildAnnotatedString {
    // The shrunken symbol would otherwise sit on the digits' baseline at
    // mid-height; the baseline shift floats it back up to the cap line where a
    // degree mark belongs.
    val degreeStyle = SpanStyle(
        fontSize = DEGREE_SYMBOL_EM.em,
        baselineShift = BaselineShift(DEGREE_SYMBOL_SHIFT),
    )
    withStyle(degreeStyle.copy(color = Color.Transparent)) { append("°") }
    append(text.removeSuffix("°"))
    withStyle(degreeStyle.copy(color = LevelColors.TextDim)) { append("°") }
}

private const val DEGREE_SYMBOL_EM = .55f
// No cap-height-alignment primitive exists in Compose; this value is derived from
// Roboto's metrics (digit cap height minus the degree glyph's top at the symbol
// size above — resize them together), verified against on-device captures.
private const val DEGREE_SYMBOL_SHIFT = .38f

internal fun formatRiseRun(valueDegrees: Double, units: MeasurementUnits): String = when (units) {
    MeasurementUnits.METRIC ->
        String.format(Locale.US, "%.1f mm/m", RiseRun.millimetersPerMeter(valueDegrees))
    MeasurementUnits.IMPERIAL ->
        String.format(Locale.US, "%.2f in/ft", RiseRun.inchesPerFoot(valueDegrees))
}

private fun formatTolerance(): String =
    String.format(Locale.US, "%.2f°", LockDetector.DEFAULT_EXIT_DEGREES)

internal fun View.performConfirmHaptic() {
    val constant = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
        HapticFeedbackConstants.CONFIRM
    } else {
        HapticFeedbackConstants.VIRTUAL_KEY
    }
    performHapticFeedback(constant)
}
