// Copyright (C) 2026 Jason M. Schwefel. All Rights Reserved.
package com.coldboreballisticsllc.scopedope.ui.screens.dope

import android.content.Intent
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Save
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.em
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import kotlinx.coroutines.delay
import com.coldboreballisticsllc.scopedope.domain.model.DopeEntry
import com.coldboreballisticsllc.scopedope.ballistics.TwistDirection
import com.coldboreballisticsllc.scopedope.ballistics.UnitConversion
import com.coldboreballisticsllc.scopedope.ui.components.*
import kotlin.math.roundToInt
import com.coldboreballisticsllc.scopedope.ui.session.AdjUnit
import com.coldboreballisticsllc.scopedope.ui.session.DistancePreset
import com.coldboreballisticsllc.scopedope.ui.session.LoadResult
import com.coldboreballisticsllc.scopedope.ui.session.MeasureSystem
import com.coldboreballisticsllc.scopedope.ui.session.RifleSetup
import com.coldboreballisticsllc.scopedope.ui.session.ScopeInfo
import com.coldboreballisticsllc.scopedope.ui.session.SessionViewModel
import com.coldboreballisticsllc.scopedope.ui.theme.*

private fun Int.toDisplayDist(isMetric: Boolean): String =
    if (isMetric) "${(this * 0.9144).roundToInt()} m" else "$this yd"

private fun parseDisplayDist(str: String, isMetric: Boolean): Int? =
    if (isMetric) str.toDoubleOrNull()?.let { (it / 0.9144).roundToInt() }
    else str.toIntOrNull()

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DopeScreen(
    onNavigateAmmo:     () -> Unit,
    onNavigateSettings: () -> Unit,
    sessionVm: SessionViewModel = hiltViewModel(),
) {
    val state    by sessionVm.state.collectAsState()
    val context  = LocalContext.current
    val isMetric = state.measureSystem == MeasureSystem.METRIC

    LaunchedEffect(Unit) {
        sessionVm.shareEvent.collect { uri ->
            val intent = Intent(Intent.ACTION_SEND).apply {
                type = "application/json"
                putExtra(Intent.EXTRA_STREAM, uri)
                addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
            }
            context.startActivity(Intent.createChooser(intent, "Share DOPE Session"))
        }
    }

    var showFileBrowser by remember { mutableStateOf(false) }

    var pendingAmmoNavigation by remember { mutableStateOf(false) }
    val saveSessionLauncher = rememberLauncherForActivityResult(
        ActivityResultContracts.CreateDocument("application/vnd.schwefel.scopedope+json")
    ) { uri ->
        if (uri != null) {
            sessionVm.saveSession(uri)
            if (pendingAmmoNavigation) onNavigateAmmo()
        }
        pendingAmmoNavigation = false
    }

    if (showFileBrowser) {
        DopeFileBrowserSheet(sessionVm = sessionVm, onDismiss = { showFileBrowser = false })
    }

    var showMenu               by remember { mutableStateOf(false) }
    var showSaveBeforeAmmoDialog by remember { mutableStateOf(false) }

    if (showSaveBeforeAmmoDialog) {
        AlertDialog(
            onDismissRequest = { showSaveBeforeAmmoDialog = false },
            title            = { Text("Save Session?") },
            text             = { Text("Save '${state.sessionName}' before selecting new ammo?") },
            confirmButton    = {
                TextButton(onClick = {
                    showSaveBeforeAmmoDialog = false
                    pendingAmmoNavigation = true
                    saveSessionLauncher.launch("${state.sessionName.replace(" ", "_")}.dope")
                }) { Text("Save & Change") }
            },
            dismissButton    = {
                TextButton(onClick = {
                    showSaveBeforeAmmoDialog = false
                    onNavigateAmmo()
                }) { Text("Change Anyway") }
            },
            containerColor    = ColorSurface,
            titleContentColor = ColorTextPrimary,
            textContentColor  = ColorTextMuted,
        )
    }

    Scaffold(
        topBar = {
            TopAppBar(
                title  = { Text("ScopeDOPE", style = MaterialTheme.typography.titleLarge) },
                actions = {
                    Box {
                        IconButton(onClick = { showMenu = true }) {
                            Icon(Icons.Default.MoreVert, contentDescription = "More options",
                                 tint = ColorAmberDim)
                        }
                        DropdownMenu(
                            expanded         = showMenu,
                            onDismissRequest = { showMenu = false },
                            containerColor   = ColorSurface,
                        ) {
                            DropdownMenuItem(
                                text    = { Text("Load .dope", color = ColorTextPrimary) },
                                onClick = {
                                    showMenu = false
                                    showFileBrowser = true
                                },
                            )
                            DropdownMenuItem(
                                text    = { Text("Save .dope", color = ColorTextPrimary) },
                                onClick = {
                                    showMenu = false
                                    val filename = if (state.sessionName == "Unsaved") "scopedope_session.dope"
                                                   else "${state.sessionName.replace(" ", "_")}.dope"
                                    saveSessionLauncher.launch(filename)
                                },
                            )
                            DropdownMenuItem(
                                text    = { Text("Share .dope", color = ColorTextPrimary) },
                                onClick = {
                                    showMenu = false
                                    sessionVm.shareDope()
                                },
                            )
                        }
                    }
                    IconButton(
                        onClick = {
                            val filename = if (state.sessionName == "Unsaved") "scopedope_session.dope"
                                           else "${state.sessionName.replace(" ", "_")}.dope"
                            saveSessionLauncher.launch(filename)
                        },
                        enabled = state.ammo != null,
                    ) {
                        Icon(
                            Icons.Default.Save,
                            contentDescription = "Save session",
                            tint = if (state.isDirty && state.ammo != null) ColorAmber else ColorAmberDim,
                        )
                    }
                    IconButton(onClick = onNavigateSettings) {
                        Icon(Icons.Default.Settings, contentDescription = "Settings",
                             tint = ColorAmberDim)
                    }
                },
                colors = TopAppBarDefaults.topAppBarColors(
                    containerColor    = ColorBackground,
                    titleContentColor = ColorTextPrimary,
                ),
            )
        },
        containerColor = ColorBackground,
    ) { padding ->
        Column(
            modifier = Modifier
                .padding(padding)
                .fillMaxSize(),
        ) {
            // IME handling: add the keyboard height as bottom content padding on the
            // scroll list (not imePadding() on the whole column, which collapses the
            // viewport and leaves a gap). This lets the focused field scroll to sit
            // just above the keyboard.
            val imeBottom = WindowInsets.ime.asPaddingValues().calculateBottomPadding()
            LazyColumn(
                modifier            = Modifier.weight(1f),
                contentPadding      = PaddingValues(
                    start = 16.dp, end = 16.dp, top = 16.dp,
                    bottom = 16.dp + imeBottom,
                ),
                verticalArrangement = Arrangement.spacedBy(12.dp),
            ) {
                // ── Ammo panel ────────────────────────────────────────────────────
                item {
                    InstrumentPanel(label = "Ammo") {
                        val ammo = state.ammo
                        if (ammo == null) {
                            Text(
                                text  = "No ammo selected",
                                style = MaterialTheme.typography.bodyMedium,
                                color = ColorTextMuted,
                            )
                        } else {
                            LabeledValue(label = ammo.caliber, value = ammo.name)
                            val bcDisplay = when {
                                ammo.bcG1 > 0.0 && ammo.bcG7 > 0.0 ->
                                    "G1 ${ammo.bcG1}  •  G7 ${ammo.bcG7}"
                                else ->
                                    "BC ${ammo.bcModel.name} ${ammo.bc}"
                            }
                            LabeledValue(
                                label = "MV",
                                value = "${ammo.muzzleVelFps} fps  •  $bcDisplay",
                            )
                        }
                        Spacer(Modifier.height(8.dp))
                        ActionButton(
                            text     = if (state.ammo == null) "Select Ammo" else "Change Ammo",
                            onClick  = {
                                if (state.ammo != null && state.sessionName != "Unsaved") {
                                    showSaveBeforeAmmoDialog = true
                                } else {
                                    onNavigateAmmo()
                                }
                            },
                            modifier = Modifier.fillMaxWidth(),
                        )
                    }
                }

                // ── Rifle setup ───────────────────────────────────────────────────
                item {
                    RifleSetupPanel(
                        setup    = state.rifle,
                        onUpdate = sessionVm::setRifleSetup,
                        isMetric = isMetric,
                    )
                }

                // ── Scope info ────────────────────────────────────────────────────
                item {
                    ScopeInfoPanel(
                        scope    = state.scope,
                        onUpdate = sessionVm::setScopeInfo,
                    )
                }

                // ── Known DOPE table ──────────────────────────────────────────────
                item {
                    InstrumentPanel(label = "Known DOPE") {
                        if (state.dopeEntries.isEmpty()) {
                            Text(
                                text = if (state.useRealDope)
                                    "Add your confirmed range impacts below. 3+ entries recommended for an accurate curve."
                                else
                                    "Physics-only mode — DOPE entries are collected but not used in calculations.",
                                style = MaterialTheme.typography.bodyMedium,
                                color = if (state.useRealDope) ColorTextMuted else ColorAmberDim,
                            )
                        }
                    }
                }

                itemsIndexed(state.dopeEntries) { index, entry ->
                    DopeEntryRow(
                        index    = index,
                        entry    = entry,
                        onUpdate = { sessionVm.setDopeEntry(index, it) },
                        onDelete = { sessionVm.removeDopeEntry(index) },
                        isMetric = isMetric,
                        adjUnit  = state.adjUnit,
                    )
                    Spacer(Modifier.height(7.dp))
                }

                item {
                    AddDopeEntryRow(
                        onAdd    = { sessionVm.setDopeEntry(state.dopeEntries.size, it) },
                        isMetric = isMetric,
                        adjUnit  = state.adjUnit,
                    )
                }

                // ── Output distances ──────────────────────────────────────────────
                item {
                    OutputDistancesPanel(
                        distances      = state.outputDistances,
                        activePreset   = state.distancePreset,
                        onApplyPreset  = sessionVm::applyDistancePreset,
                        onAdd          = sessionVm::addOutputDistance,
                        onRemove       = sessionVm::removeOutputDistance,
                        isMetric       = isMetric,
                    )
                }
            }

            // ── Session load feedback ──────────────────────────────────────────
            Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp)) {
                // ── Load result toast ─────────────────────────────────────────
                when (val r = state.loadResult) {
                    is LoadResult.Success -> {
                        Text(
                            text  = "Session loaded",
                            color = ColorSuccess,
                            style = MaterialTheme.typography.bodyMedium,
                        )
                        LaunchedEffect(r) {
                            delay(3_000)
                            sessionVm.clearLoadResult()
                        }
                    }
                    is LoadResult.Failure -> {
                        Text(
                            text  = "Load failed: ${r.message}",
                            color = ColorError,
                            style = MaterialTheme.typography.bodyMedium,
                        )
                        LaunchedEffect(r) {
                            delay(4_000)
                            sessionVm.clearLoadResult()
                        }
                    }
                    null -> {}
                }
                // ── Share error feedback ──────────────────────────────────────
                state.shareError?.let { err ->
                    Text(
                        text  = "Share failed: $err",
                        color = ColorError,
                        style = MaterialTheme.typography.bodyMedium,
                    )
                    LaunchedEffect(err) {
                        delay(4_000)
                        sessionVm.clearShareError()
                    }
                }
            }
        }
    }
}

// ── Rifle setup sub-panel ─────────────────────────────────────────────────────

@Composable
private fun RifleSetupPanel(setup: RifleSetup, onUpdate: (RifleSetup) -> Unit, isMetric: Boolean) {
    var nameStr     by remember(setup.name)          { mutableStateOf(setup.name) }
    var brandStr    by remember(setup.brand)         { mutableStateOf(setup.brand) }
    var modelStr    by remember(setup.model)         { mutableStateOf(setup.model) }
    var zeroStr     by remember(setup.zeroYards, isMetric) {
        mutableStateOf(if (isMetric) "${(setup.zeroYards * 0.9144).roundToInt()}" else setup.zeroYards.toString())
    }
    var sightStr    by remember(setup.sightHeightIn) { mutableStateOf(setup.sightHeightIn.toString()) }
    var twistStr    by remember(setup.twistRateIn)   {
        mutableStateOf(if (setup.twistRateIn <= 0.0) "" else setup.twistRateIn.toString())
    }
    var zeroTempStr by remember(setup.zeroTempF, isMetric) {
        mutableStateOf(
            if (isMetric && setup.zeroTempF != null)
                "%.1f".format((setup.zeroTempF - 32.0) * 5.0 / 9.0)
            else
                setup.zeroTempF?.toString() ?: ""
        )
    }

    fun commitAll() = onUpdate(setup.copy(
        name          = nameStr.trim(),
        brand         = brandStr.trim(),
        model         = modelStr.trim(),
        zeroYards     = parseDisplayDist(zeroStr, isMetric) ?: setup.zeroYards,
        sightHeightIn = sightStr.toDoubleOrNull() ?: setup.sightHeightIn,
        twistRateIn   = twistStr.toDoubleOrNull() ?: 0.0,
        zeroTempF     = if (isMetric) zeroTempStr.toDoubleOrNull()?.let { it * 9.0 / 5.0 + 32.0 }
                        else zeroTempStr.toDoubleOrNull(),
    ))

    InstrumentPanel(label = "Rifle Setup") {
        Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
            LabelTextField(
                value         = brandStr,
                onValueChange = { brandStr = it },
                label         = "Brand",
                modifier      = Modifier.weight(1f),
                onFocusLost   = ::commitAll,
            )
            LabelTextField(
                value         = modelStr,
                onValueChange = { modelStr = it },
                label         = "Model",
                modifier      = Modifier.weight(1f),
                onFocusLost   = ::commitAll,
            )
        }
        Spacer(Modifier.height(6.dp))
        LabelTextField(
            value         = nameStr,
            onValueChange = { nameStr = it },
            label         = "Friendly name (optional)",
            modifier      = Modifier.fillMaxWidth(),
            imeAction     = ImeAction.Done,
            onFocusLost   = ::commitAll,
        )
        Spacer(Modifier.height(6.dp))
        Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
            NumericField(
                value         = zeroStr,
                onValueChange = { zeroStr = it },
                label         = "Zero",
                suffix        = if (isMetric) "m" else "yd",
                modifier      = Modifier.weight(1f),
                onDone        = ::commitAll,
                onFocusLost   = ::commitAll,
            )
            NumericField(
                value         = sightStr,
                onValueChange = { sightStr = it },
                label         = "Sight height",
                suffix        = "in",
                modifier      = Modifier.weight(1f),
                onDone        = ::commitAll,
                onFocusLost   = ::commitAll,
            )
        }
        Spacer(Modifier.height(6.dp))
        Row(
            Modifier.fillMaxWidth(),
            horizontalArrangement = Arrangement.spacedBy(8.dp),
            verticalAlignment     = Alignment.CenterVertically,
        ) {
            NumericField(
                value         = twistStr,
                onValueChange = { twistStr = it },
                label         = "Twist (1:? in)",
                suffix        = "in",
                modifier      = Modifier.weight(1f),
                onDone        = ::commitAll,
                onFocusLost   = ::commitAll,
            )
            TwistDirectionToggle(
                direction = setup.twistDirection,
                onChange  = { onUpdate(setup.copy(twistDirection = it)) },
                modifier  = Modifier.weight(1f),
            )
        }
        Spacer(Modifier.height(6.dp))
        NumericField(
            value         = zeroTempStr,
            onValueChange = { zeroTempStr = it },
            label         = "Zero temp (optional)",
            suffix        = if (isMetric) "°C" else "°F",
            modifier      = Modifier.fillMaxWidth(0.5f),
            imeAction     = ImeAction.Done,
            onDone        = ::commitAll,
            onFocusLost   = ::commitAll,
        )
    }
}

// ── Scope info sub-panel ──────────────────────────────────────────────────────

@Composable
private fun ScopeInfoPanel(scope: ScopeInfo, onUpdate: (ScopeInfo) -> Unit) {
    var nameStr  by remember(scope.name)  { mutableStateOf(scope.name) }
    var brandStr by remember(scope.brand) { mutableStateOf(scope.brand) }
    var modelStr by remember(scope.model) { mutableStateOf(scope.model) }

    fun commitAll() = onUpdate(scope.copy(
        name  = nameStr.trim(),
        brand = brandStr.trim(),
        model = modelStr.trim(),
    ))

    InstrumentPanel(label = "Scope") {
        Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
            LabelTextField(
                value         = brandStr,
                onValueChange = { brandStr = it },
                label         = "Brand",
                modifier      = Modifier.weight(1f),
                onFocusLost   = ::commitAll,
            )
            LabelTextField(
                value         = modelStr,
                onValueChange = { modelStr = it },
                label         = "Model",
                modifier      = Modifier.weight(1f),
                onFocusLost   = ::commitAll,
            )
        }
        Spacer(Modifier.height(6.dp))
        LabelTextField(
            value         = nameStr,
            onValueChange = { nameStr = it },
            label         = "Friendly name (optional)",
            modifier      = Modifier.fillMaxWidth(),
            imeAction     = ImeAction.Done,
            onFocusLost   = ::commitAll,
        )
    }
}

@Composable
private fun TwistDirectionToggle(
    direction: TwistDirection,
    onChange:  (TwistDirection) -> Unit,
    modifier:  Modifier = Modifier,
) {
    Row(
        modifier              = modifier,
        horizontalArrangement = Arrangement.spacedBy(4.dp),
        verticalAlignment     = Alignment.CenterVertically,
    ) {
        Text("Twist:", color = ColorTextMuted, style = MaterialTheme.typography.labelMedium)
        TwistDirection.entries.forEach { dir ->
            val isSel = direction == dir
            FilterChip(
                selected = isSel,
                onClick  = { onChange(dir) },
                // Color set directly on the Text: FilterChip's own content-color
                // resolution ignores/alpha-blends selectedLabelColor, which is what
                // muddied the text on amber (DOPE-37). ActionButton uses this same
                // direct approach for its black-on-amber label.
                label    = {
                    Text(
                        dir.name,
                        color    = if (isSel) ColorOnAmber else ColorTextPrimary,
                        style    = MaterialTheme.typography.labelMedium,
                        maxLines = 1,
                        softWrap = false,
                    )
                },
                colors   = FilterChipDefaults.filterChipColors(
                    selectedContainerColor = ColorAmber,
                    containerColor         = ColorSurface,
                ),
                border   = FilterChipDefaults.filterChipBorder(
                    enabled             = true,
                    selected            = direction == dir,
                    borderColor         = ColorBorder,
                    selectedBorderColor = ColorAmber,
                    borderWidth         = 1.dp,
                    selectedBorderWidth = 1.dp,
                ),
            )
        }
    }
}

// ── Individual DOPE entry row (display mode — tap to edit) ───────────────────

@Composable
private fun DopeEntryRow(
    index:    Int,
    entry:    DopeEntry,
    onUpdate: (DopeEntry) -> Unit,
    onDelete: () -> Unit,
    isMetric: Boolean,
    adjUnit:  AdjUnit,
) {
    val milToAdj  = if (adjUnit == AdjUnit.MOA) UnitConversion.MIL_TO_MOA else 1.0
    val adjSuffix = if (adjUnit == AdjUnit.MOA) "moa" else "mil"
    val distDisplay = entry.distanceYards.toDisplayDist(isMetric)
    val adjDisplay  = "%.1f".format(entry.adjustmentMil * milToAdj)
    val adjColor    = when {
        entry.adjustmentMil > 0 -> ColorDataPositive
        entry.adjustmentMil < 0 -> ColorDataNegative
        else                    -> ColorTextPrimary
    }

    var showEditDialog  by remember { mutableStateOf(false) }

    if (showEditDialog) {
        var distStr by remember { mutableStateOf(if (isMetric) "${(entry.distanceYards * 0.9144).roundToInt()}" else entry.distanceYards.toString()) }
        var adjStr  by remember { mutableStateOf(adjDisplay) }
        AlertDialog(
            onDismissRequest = { showEditDialog = false },
            title            = { Text("Edit Entry", color = ColorTextPrimary) },
            text             = {
                Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
                    NumericField(
                        value         = distStr,
                        onValueChange = { distStr = it },
                        label         = "Dist",
                        suffix        = if (isMetric) "m" else "yd",
                        modifier      = Modifier.weight(1f),
                    )
                    NumericField(
                        value         = adjStr,
                        onValueChange = { adjStr = it },
                        label         = "Adj",
                        suffix        = adjSuffix,
                        modifier      = Modifier.weight(1f),
                    )
                }
            },
            confirmButton = {
                TextButton(onClick = {
                    val d = parseDisplayDist(distStr, isMetric) ?: entry.distanceYards
                    val a = adjStr.toDoubleOrNull()?.div(milToAdj) ?: entry.adjustmentMil
                    onUpdate(entry.copy(distanceYards = d, adjustmentMil = a))
                    showEditDialog = false
                }) { Text("Save", color = ColorAmber) }
            },
            dismissButton = {
                Row {
                    TextButton(
                        onClick = { showEditDialog = false; onDelete() },
                        colors  = ButtonDefaults.textButtonColors(contentColor = ColorError),
                    ) { Text("Remove") }
                    TextButton(onClick = { showEditDialog = false }) { Text("Cancel", color = ColorTextMuted) }
                }
            },
            containerColor    = ColorSurface,
            titleContentColor = ColorTextPrimary,
        )
    }

    Row(
        modifier = Modifier
            .fillMaxWidth()
            .clickable { showEditDialog = true }
            .background(ColorSurface2)
            .border(1.dp, ColorBorder)
            .padding(horizontal = 12.dp)
            .heightIn(min = 60.dp),
        verticalAlignment     = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.spacedBy(10.dp),
    ) {
        Text(
            text     = "%02d".format(index + 1),
            style    = DataValueStyle.copy(fontSize = 12.sp, letterSpacing = 0.08.em),
            color    = ColorTextMuted,
            modifier = Modifier.width(20.dp),
        )
        Column(Modifier.weight(1f)) {
            Text("DIST", style = FieldLabelStyle, color = ColorTextMuted)
            Text(distDisplay, style = DataValueStyle, color = ColorDataDist)
        }
        Box(Modifier.width(1.dp).height(32.dp).background(ColorBorder))
        Column(Modifier.weight(1f)) {
            Text("ADJ", style = FieldLabelStyle, color = ColorTextMuted)
            Text(adjDisplay, style = DataValueStyle, color = adjColor)
        }
        IconButton(
            onClick  = { showEditDialog = true },
            modifier = Modifier.size(48.dp),
        ) {
            Icon(Icons.Default.Delete, contentDescription = "Remove", tint = ColorTextMuted, modifier = Modifier.size(18.dp))
        }
    }
}

// ── Add new DOPE entry ────────────────────────────────────────────────────────

@Composable
private fun AddDopeEntryRow(onAdd: (DopeEntry) -> Unit, isMetric: Boolean, adjUnit: AdjUnit) {
    val milToAdj  = if (adjUnit == AdjUnit.MOA) UnitConversion.MIL_TO_MOA else 1.0
    val adjSuffix = if (adjUnit == AdjUnit.MOA) "moa" else "mil"

    var distStr by remember { mutableStateOf("") }
    var adjStr  by remember { mutableStateOf("") }
    val distFocusRequester = remember { FocusRequester() }
    val adjFocusRequester  = remember { FocusRequester() }

    Row(
        modifier              = Modifier
            .fillMaxWidth()
            .padding(horizontal = 16.dp),
        verticalAlignment     = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.spacedBy(8.dp),
    ) {
        NumericField(
            value         = distStr,
            onValueChange = { distStr = it },
            label         = "New dist",
            suffix        = if (isMetric) "m" else "yd",
            modifier      = Modifier.weight(1f).focusRequester(distFocusRequester),
            imeAction     = ImeAction.Next,
            onNext        = { adjFocusRequester.requestFocus() },
        )
        NumericField(
            value         = adjStr,
            onValueChange = { adjStr = it },
            label         = "Adj",
            suffix        = adjSuffix,
            modifier      = Modifier.weight(1f),
            focusRequester = adjFocusRequester,
            imeAction     = ImeAction.Done,
            onDone        = {
                val parsedDist = parseDisplayDist(distStr, isMetric)
                if (parsedDist != null && adjStr.toDoubleOrNull() != null) {
                    onAdd(DopeEntry(parsedDist, adjStr.toDoubleOrNull()!! / milToAdj))
                    distStr = ""; adjStr = ""
                    distFocusRequester.requestFocus()
                }
            },
        )
        val parsedDist = parseDisplayDist(distStr, isMetric)
        val valid = parsedDist != null && adjStr.toDoubleOrNull() != null
        // Square add button — aligned to the field row (label + 4dp spacer above field).
        ActionButton(
            text    = "+",
            enabled = valid,
            height  = 56.dp,
            notch   = 6.dp,
            onClick = {
                onAdd(DopeEntry(parsedDist!!, adjStr.toDoubleOrNull()!! / milToAdj))
                distStr = ""; adjStr = ""
                distFocusRequester.requestFocus()
            },
            modifier = Modifier.padding(top = 22.dp).width(56.dp),
        )
    }
}

// ── Output distances panel ────────────────────────────────────────────────────

@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun OutputDistancesPanel(
    distances:     List<Int>,
    activePreset:  DistancePreset,
    onApplyPreset: (DistancePreset) -> Unit,
    onAdd:         (Int) -> Unit,
    onRemove:      (Int) -> Unit,
    isMetric:      Boolean,
) {
    var newDist by remember { mutableStateOf("") }

    InstrumentPanel(label = "Output Distances") {
        // Preset buttons
        Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(6.dp)) {
            listOf(DistancePreset.NRL22, DistancePreset.PRS).forEach { preset ->
                FilterChip(
                    selected = activePreset == preset,
                    onClick  = { onApplyPreset(preset) },
                    label    = {
                        Text(
                            text  = preset.label,
                            style = MaterialTheme.typography.labelMedium,
                            color = if (activePreset == preset) ColorBackground else ColorTextPrimary,
                        )
                    },
                    colors   = FilterChipDefaults.filterChipColors(
                        selectedContainerColor = ColorAmber,
                        selectedLabelColor     = ColorBackground,
                        containerColor         = ColorSurface2,
                        labelColor             = ColorTextPrimary,
                    ),
                )
            }
        }

        Spacer(Modifier.height(4.dp))

        if (distances.isEmpty()) {
            Text(
                text  = "Add distances or choose a preset above",
                style = MaterialTheme.typography.bodyMedium,
                color = ColorTextMuted,
            )
        } else {
            FlowRow(
                modifier              = Modifier.fillMaxWidth(),
                horizontalArrangement = Arrangement.spacedBy(6.dp),
                verticalArrangement   = Arrangement.spacedBy(6.dp),
            ) {
                distances.forEach { dist ->
                    InputChip(
                        selected = false,
                        onClick  = { onRemove(dist) },
                        label    = { Text(dist.toDisplayDist(isMetric)) },
                        trailingIcon = {
                            Icon(Icons.Default.Delete, null,
                                 Modifier.size(14.dp), tint = ColorTextMuted)
                        },
                        colors = InputChipDefaults.inputChipColors(
                            containerColor = ColorSurface2,
                            labelColor     = ColorTextPrimary,
                        ),
                    )
                }
            }
        }

        Spacer(Modifier.height(8.dp))
        Row(verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.spacedBy(8.dp)) {
            NumericField(
                value         = newDist,
                onValueChange = { newDist = it },
                label         = "Add distance",
                suffix        = if (isMetric) "m" else "yd",
                modifier      = Modifier.weight(1f),
                imeAction     = ImeAction.Done,
                onDone        = {
                    parseDisplayDist(newDist, isMetric)?.let { onAdd(it); newDist = "" }
                },
            )
            ActionButton(
                text     = "+",
                enabled  = parseDisplayDist(newDist, isMetric) != null,
                height   = 56.dp,
                notch    = 6.dp,
                onClick  = {
                    parseDisplayDist(newDist, isMetric)?.let { onAdd(it); newDist = "" }
                },
                modifier = Modifier.padding(top = 22.dp).width(56.dp),
            )
        }
    }
}
