package com.kawomi.githugandroid

import android.content.Context
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.draggable
import androidx.compose.foundation.gestures.rememberDraggableState
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
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.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.darkColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
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.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.sp
import androidx.compose.ui.unit.dp
import androidx.compose.foundation.relocation.BringIntoViewRequester
import androidx.compose.foundation.relocation.bringIntoViewRequester
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.emptyPreferences
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStoreFile
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import java.io.IOException

private val AppBackground = Color(0xFF000000)
private val PanelPrimary = Color(0xFF121212)
private val PanelSecondary = Color(0xFF1E1E1E)
private val PanelTertiary = Color(0xFF262626)
private val TerminalBackground = Color(0xFF050505)
private val TextPrimary = Color(0xFFFFFFFF)
private val TextSecondary = Color(0xFFE6E6E6)
private val TextMuted = Color(0xFFBDBDBD)
private val Accent = Color(0xFF00E5FF)
private val Success = Color(0xFF00FF95)

private val GitHugColorScheme = darkColorScheme(
    primary = Accent,
    onPrimary = Color.Black,
    secondary = TextPrimary,
    onSecondary = Color.Black,
    background = AppBackground,
    onBackground = TextPrimary,
    surface = PanelPrimary,
    onSurface = TextPrimary,
    surfaceVariant = PanelSecondary,
    onSurfaceVariant = TextSecondary,
    outline = TextMuted,
)

private enum class PaneId(val title: String) {
    LEVELS("Levels"),
    VISUAL("Visual"),
    EXERCISE("Exercise"),
    TERMINAL("Terminal"),
}

private data class PaneLayout(
    val order: List<PaneId>,
    val collapsed: Set<PaneId>,
    val weights: Map<PaneId, Float>,
)

private enum class ExerciseDetailPanel {
    HINT,
    SUGGESTIONS,
}

private fun defaultPaneLayout(): PaneLayout = PaneLayout(
    order = listOf(PaneId.EXERCISE, PaneId.TERMINAL, PaneId.VISUAL, PaneId.LEVELS),
    collapsed = emptySet(),
    weights = mapOf(
        PaneId.EXERCISE to 0.95f,
        PaneId.TERMINAL to 1.45f,
        PaneId.VISUAL to 1.0f,
        PaneId.LEVELS to 0.8f,
    ),
)

private class PaneLayoutStore(private val context: Context) {
    private val dataStore = PreferenceDataStoreFactory.create(
        produceFile = { context.preferencesDataStoreFile("pane_layout_preferences") }
    )

    private val orderKey = stringPreferencesKey("pane_order")
    private val collapsedKey = stringPreferencesKey("pane_collapsed")
    private val weightsKey = stringPreferencesKey("pane_weights")

    val layoutFlow = dataStore.data
        .catch { error ->
            if (error is IOException) emit(emptyPreferences()) else throw error
        }
        .map { preferences ->
            val defaults = defaultPaneLayout()
            val order = preferences[orderKey]
                ?.split(',')
                ?.mapNotNull { value -> PaneId.entries.firstOrNull { it.name == value } }
                ?.let { parsed ->
                    val missing = PaneId.entries.filterNot { it in parsed }
                    parsed + missing
                }
                ?.takeIf { it.isNotEmpty() }
                ?: defaults.order

            val collapsed = preferences[collapsedKey]
                ?.split(',')
                ?.mapNotNull { value -> PaneId.entries.firstOrNull { it.name == value } }
                ?.toSet()
                ?: emptySet()

            val parsedWeights = preferences[weightsKey]
                ?.split(';')
                ?.mapNotNull { item ->
                    val parts = item.split(':', limit = 2)
                    val paneId = PaneId.entries.firstOrNull { it.name == parts.getOrNull(0) }
                    val weight = parts.getOrNull(1)?.toFloatOrNull()
                    if (paneId != null && weight != null) paneId to weight.coerceAtLeast(0.6f) else null
                }
                ?.toMap()
                .orEmpty()

            PaneLayout(
                order = order,
                collapsed = collapsed,
                weights = PaneId.entries.associateWith { pane -> parsedWeights[pane] ?: defaults.weights.getValue(pane) },
            )
        }

    suspend fun save(layout: PaneLayout) {
        dataStore.edit { preferences ->
            preferences[orderKey] = layout.order.joinToString(",") { it.name }
            preferences[collapsedKey] = layout.collapsed.joinToString(",") { it.name }
            preferences[weightsKey] = layout.order.joinToString(";") { pane ->
                "${pane.name}:${layout.weights[pane] ?: 1f}"
            }
        }
    }
}

@Composable
fun GitHugApp() {
    MaterialTheme(colorScheme = GitHugColorScheme) {
        val context = LocalContext.current
        val configuration = LocalConfiguration.current
        val levels = remember { sampleLevels() }
        val runtime = remember(context) { GitRepositoryRuntime(context.applicationContext) }
        val paneLayoutStore = remember(context) { PaneLayoutStore(context.applicationContext) }
        val persistedPaneLayout by paneLayoutStore.layoutFlow.collectAsState(initial = defaultPaneLayout())
        val scope = rememberCoroutineScope()
        val screenScrollState = rememberScrollState()
        val screenHeightDp = configuration.screenHeightDp

        var paneLayout by remember { mutableStateOf(defaultPaneLayout()) }
        var currentLevelIndex by remember { mutableStateOf(0) }
        var repo by remember { mutableStateOf(runtime.prepareLevel(levels.first())) }
        var commandInput by remember { mutableStateOf(TextFieldValue("")) }
        var inputFieldVersion by remember { mutableStateOf(0) }
        var suppressedImeEcho by remember { mutableStateOf<String?>(null) }
        var output by remember { mutableStateOf(listOf(runtime.startupBanner())) }
        var hintIndex by remember { mutableStateOf(0) }
        var activeExerciseDetail by remember { mutableStateOf<ExerciseDetailPanel?>(null) }
        var visibleHint by remember { mutableStateOf<String?>(null) }
        var completedLevels by remember { mutableStateOf(setOf<String>()) }
        var commandHistory by remember { mutableStateOf(listOf<String>()) }
        var historyIndex by remember { mutableStateOf(-1) }
        var historyDraft by remember { mutableStateOf("") }
        val currentLevel = levels[currentLevelIndex]

        LaunchedEffect(persistedPaneLayout) {
            paneLayout = persistedPaneLayout
        }

        fun updatePaneLayout(transform: (PaneLayout) -> PaneLayout, persist: Boolean = true) {
            val updated = transform(paneLayout)
            paneLayout = updated
            if (persist) {
                scope.launch { paneLayoutStore.save(updated) }
            }
        }

        fun clearCommandInput(recreateField: Boolean = false) {
            commandInput = TextFieldValue(text = "", selection = TextRange.Zero)
            if (recreateField) {
                inputFieldVersion += 1
            }
        }

        fun applyRecommendedPaneWeights(persist: Boolean = true) {
            updatePaneLayout(
                transform = { layout ->
                    layout.copy(
                        weights = layout.weights + recommendedPaneWeights(
                            heights = recommendedPaneHeights(
                                level = currentLevel,
                                levelCount = levels.size,
                                outputLineCount = output.size,
                                screenHeightDp = screenHeightDp,
                                suggestionsVisible = activeExerciseDetail == ExerciseDetailPanel.SUGGESTIONS,
                                visibleHint = visibleHint,
                            ),
                        )
                    )
                },
                persist = persist,
            )
        }

        val recommendedHeights = recommendedPaneHeights(
            level = currentLevel,
            levelCount = levels.size,
            outputLineCount = output.size,
            screenHeightDp = screenHeightDp,
            suggestionsVisible = activeExerciseDetail == ExerciseDetailPanel.SUGGESTIONS,
            visibleHint = visibleHint,
        )
        val recommendedWeights = recommendedPaneWeights(
            heights = recommendedHeights,
        )
        val workspaceHeight = recommendedWorkspaceHeight(
            paneLayout = paneLayout,
            recommendedHeights = recommendedHeights,
        )

        fun resetCurrentLevel(message: String = "Level reset.") {
            repo = runtime.prepareLevel(currentLevel)
            clearCommandInput()
            suppressedImeEcho = null
            output = listOf(message)
            hintIndex = 0
            activeExerciseDetail = null
            visibleHint = null
            historyIndex = -1
            historyDraft = ""
            applyRecommendedPaneWeights(persist = false)
        }

        fun loadLevel(index: Int) {
            currentLevelIndex = index
            repo = runtime.prepareLevel(levels[index])
            output = listOf("Loaded level: ${levels[index].title}")
            clearCommandInput()
            suppressedImeEcho = null
            hintIndex = 0
            activeExerciseDetail = null
            visibleHint = null
            historyIndex = -1
            historyDraft = ""
            paneLayout = paneLayout.copy(
                weights = paneLayout.weights + recommendedPaneWeights(
                    heights = recommendedPaneHeights(
                        level = levels[index],
                        levelCount = levels.size,
                        outputLineCount = 1,
                        screenHeightDp = screenHeightDp,
                        suggestionsVisible = false,
                        visibleHint = null,
                    ),
                )
            )
        }

        fun setCommandText(text: String) {
            commandInput = TextFieldValue(text = text, selection = TextRange(text.length))
        }

        fun moveCursor(delta: Int) {
            val next = (commandInput.selection.start + delta).coerceIn(0, commandInput.text.length)
            commandInput = commandInput.copy(selection = TextRange(next))
        }

        fun historyUp() {
            if (commandHistory.isEmpty()) return
            if (historyIndex == -1) {
                historyDraft = commandInput.text
                historyIndex = commandHistory.lastIndex
            } else {
                historyIndex = (historyIndex - 1).coerceAtLeast(0)
            }
            setCommandText(commandHistory[historyIndex])
        }

        fun historyDown() {
            if (commandHistory.isEmpty() || historyIndex == -1) return
            if (historyIndex >= commandHistory.lastIndex) {
                historyIndex = -1
                setCommandText(historyDraft)
            } else {
                historyIndex += 1
                setCommandText(commandHistory[historyIndex])
            }
        }

        fun tabComplete() {
            val cursor = commandInput.selection.start.coerceIn(0, commandInput.text.length)
            val beforeCursor = commandInput.text.substring(0, cursor)
            val tokenStart = beforeCursor.lastIndexOf(' ').let { if (it == -1) 0 else it + 1 }
            val token = commandInput.text.substring(tokenStart, cursor)
            if (token.isBlank()) return

            val matches = repo.files.map { it.name }.sorted().filter { it.startsWith(token) }
            if (matches.isEmpty()) return

            val replacement = if (matches.size == 1) matches.first() else commonPrefix(matches)
            if (replacement == token && matches.size > 1) {
                output = output + "completion> ${matches.joinToString(" ")}"
                return
            }

            val newText = commandInput.text.replaceRange(tokenStart, cursor, replacement)
            val newCursor = tokenStart + replacement.length
            commandInput = TextFieldValue(newText, selection = TextRange(newCursor))
        }

        fun runCommand() {
            val submittedText = commandInput.text
            val raw = submittedText.trim()
            if (raw.isBlank()) return

            suppressedImeEcho = submittedText
            clearCommandInput(recreateField = true)

            if (commandHistory.lastOrNull() != raw) {
                commandHistory = commandHistory + raw
            }
            historyIndex = -1
            historyDraft = ""

            val (newRepo, lines) = runtime.execute(currentLevel, repo, raw)
            val solvedAfterCommand = currentLevel.validator(newRepo)
            val wasAlreadyCompleted = currentLevel.id in completedLevels
            val newOutput = buildList {
                addAll(output)
                add("$ $raw")
                addAll(lines)
                if (solvedAfterCommand && !wasAlreadyCompleted) {
                    add("✔ Level solved: ${currentLevel.title}")
                }
            }
            if (solvedAfterCommand && !wasAlreadyCompleted) {
                completedLevels = completedLevels + currentLevel.id
                val hasNextLevel = currentLevelIndex < levels.lastIndex
                if (hasNextLevel) {
                    loadLevel(currentLevelIndex + 1)
                } else {
                    repo = newRepo
                    output = listOf("🏁 All available MVP levels completed.")
                    clearCommandInput()
                    suppressedImeEcho = null
                }
            } else {
                repo = newRepo
                output = newOutput
                applyRecommendedPaneWeights(persist = false)
            }
        }

        fun showCommandHelp() {
            output = output + runtime.commandReferenceLines()
        }

        Scaffold(containerColor = AppBackground) { padding ->
            Surface(
                modifier = Modifier
                    .fillMaxSize()
                    .padding(padding),
                color = AppBackground,
            ) {
                Column(
                    modifier = Modifier
                        .fillMaxWidth()
                        .background(AppBackground)
                        .verticalScroll(screenScrollState)
                        .imePadding()
                        .padding(horizontal = 10.dp, vertical = 8.dp),
                    verticalArrangement = Arrangement.spacedBy(8.dp),
                ) {
                    FixedHeader()
                    PaneWorkspace(
                        modifier = Modifier
                            .fillMaxWidth()
                            .height(workspaceHeight),
                        paneLayout = paneLayout,
                        onMovePane = { paneId, delta ->
                            updatePaneLayout(transform = { layout -> movePane(layout, paneId, delta) })
                        },
                        onTogglePane = { paneId ->
                            updatePaneLayout(transform = { layout ->
                                val collapsed = layout.collapsed.toMutableSet()
                                if (!collapsed.add(paneId)) collapsed.remove(paneId)
                                layout.copy(collapsed = collapsed)
                            })
                        },
                        onResize = { upper, lower, dragFraction ->
                            updatePaneLayout(transform = { layout -> resizePanes(layout, upper, lower, dragFraction) }, persist = false)
                        },
                        onResizeFinished = {
                            scope.launch { paneLayoutStore.save(paneLayout) }
                        },
                        levelsContent = {
                            LevelsPane(levels, currentLevelIndex, completedLevels) { index -> loadLevel(index) }
                        },
                        visualContent = { VisualPane(repo) },
                        exerciseContent = {
                            ExercisePane(
                                level = currentLevel,
                                visibleHint = if (activeExerciseDetail == ExerciseDetailPanel.HINT) visibleHint else null,
                                suggestionsExpanded = activeExerciseDetail == ExerciseDetailPanel.SUGGESTIONS,
                                onToggleSuggestions = {
                                    activeExerciseDetail = if (activeExerciseDetail == ExerciseDetailPanel.SUGGESTIONS) null else ExerciseDetailPanel.SUGGESTIONS
                                    if (activeExerciseDetail == ExerciseDetailPanel.SUGGESTIONS) {
                                        visibleHint = null
                                    }
                                    applyRecommendedPaneWeights(persist = false)
                                },
                                onHint = {
                                    val hint = currentLevel.hints.getOrNull(hintIndex) ?: "No more hints for this level."
                                    visibleHint = hint
                                    activeExerciseDetail = ExerciseDetailPanel.HINT
                                    hintIndex = (hintIndex + 1).coerceAtMost(currentLevel.hints.size)
                                    applyRecommendedPaneWeights(persist = false)
                                },
                                onReset = { resetCurrentLevel() },
                            )
                        },
                        terminalContent = {
                            TerminalPane(
                                output = output,
                                inputFieldVersion = inputFieldVersion,
                                commandInput = commandInput,
                                onValueChange = {
                                    val blockedEcho = suppressedImeEcho
                                    if (blockedEcho != null && commandInput.text.isEmpty()) {
                                        val blockedTrimmed = blockedEcho.trim()
                                        if (it.text == blockedEcho || (blockedTrimmed.isNotEmpty() && it.text == blockedTrimmed)) {
                                            return@TerminalPane
                                        }
                                    }
                                    suppressedImeEcho = null
                                    commandInput = it
                                },
                                onRun = { runCommand() },
                                onTab = { tabComplete() },
                                onHelp = { showCommandHelp() },
                                onCursorLeft = { moveCursor(-1) },
                                onCursorRight = { moveCursor(1) },
                                onHistoryUp = { historyUp() },
                                onHistoryDown = { historyDown() },
                            )
                        },
                    )
                    Spacer(modifier = Modifier.height(220.dp))
                }
            }
        }
    }
}

@Composable
private fun FixedHeader() {
    Card(
        colors = CardDefaults.cardColors(containerColor = PanelPrimary),
        shape = RoundedCornerShape(14.dp),
    ) {
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(horizontal = 12.dp, vertical = 10.dp),
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.spacedBy(10.dp),
        ) {
            Image(
                painter = painterResource(id = R.drawable.githug_android_logo),
                contentDescription = "GitHug Android logo",
                modifier = Modifier.size(28.dp),
            )
            Text("GitHug Android", style = MaterialTheme.typography.titleLarge, color = TextPrimary)
        }
    }
}

@Composable
private fun PaneWorkspace(
    modifier: Modifier = Modifier,
    paneLayout: PaneLayout,
    onMovePane: (PaneId, Int) -> Unit,
    onTogglePane: (PaneId) -> Unit,
    onResize: (PaneId, PaneId, Float) -> Unit,
    onResizeFinished: () -> Unit,
    levelsContent: @Composable () -> Unit,
    visualContent: @Composable () -> Unit,
    exerciseContent: @Composable () -> Unit,
    terminalContent: @Composable () -> Unit,
) {
    BoxWithConstraints(modifier = modifier) {
        val heightBasis = maxHeight.value.takeIf { it > 0f } ?: 1f

        Column(
            modifier = Modifier.fillMaxSize(),
            verticalArrangement = Arrangement.spacedBy(0.dp),
        ) {
            paneLayout.order.forEachIndexed { index, paneId ->
                val isCollapsed = paneId in paneLayout.collapsed
                if (isCollapsed) {
                    CollapsedPaneCard(
                        paneId = paneId,
                        canMoveUp = index > 0,
                        canMoveDown = index < paneLayout.order.lastIndex,
                        onMoveUp = { onMovePane(paneId, -1) },
                        onMoveDown = { onMovePane(paneId, 1) },
                        onToggleCollapse = { onTogglePane(paneId) },
                    )
                } else {
                    PaneCard(
                        paneId = paneId,
                        modifier = Modifier
                            .fillMaxWidth()
                            .weight((paneLayout.weights[paneId] ?: 1f).coerceAtLeast(0.6f)),
                        canMoveUp = index > 0,
                        canMoveDown = index < paneLayout.order.lastIndex,
                        onMoveUp = { onMovePane(paneId, -1) },
                        onMoveDown = { onMovePane(paneId, 1) },
                        onToggleCollapse = { onTogglePane(paneId) },
                    ) {
                        when (paneId) {
                            PaneId.LEVELS -> levelsContent()
                            PaneId.VISUAL -> visualContent()
                            PaneId.EXERCISE -> exerciseContent()
                            PaneId.TERMINAL -> terminalContent()
                        }
                    }
                }

                if (index < paneLayout.order.lastIndex) {
                    val upper = paneId
                    val lower = paneLayout.order[index + 1]
                    PaneDivider(
                        enabled = upper !in paneLayout.collapsed && lower !in paneLayout.collapsed,
                        onDrag = { deltaPx ->
                            val fraction = deltaPx / heightBasis
                            onResize(upper, lower, fraction)
                        },
                        onDragFinished = onResizeFinished,
                    )
                }
            }
        }
    }
}

@Composable
private fun PaneCard(
    paneId: PaneId,
    modifier: Modifier = Modifier,
    canMoveUp: Boolean,
    canMoveDown: Boolean,
    onMoveUp: () -> Unit,
    onMoveDown: () -> Unit,
    onToggleCollapse: () -> Unit,
    content: @Composable () -> Unit,
) {
    Card(
        modifier = modifier,
        colors = CardDefaults.cardColors(containerColor = PanelSecondary),
        shape = RoundedCornerShape(16.dp),
    ) {
        Column(modifier = Modifier.fillMaxSize()) {
            PaneHeader(
                paneId = paneId,
                collapsed = false,
                canMoveUp = canMoveUp,
                canMoveDown = canMoveDown,
                onMoveUp = onMoveUp,
                onMoveDown = onMoveDown,
                onToggleCollapse = onToggleCollapse,
            )
            Box(
                modifier = Modifier
                    .fillMaxSize()
                    .padding(horizontal = 12.dp, vertical = 10.dp),
            ) {
                content()
            }
        }
    }
}

@Composable
private fun CollapsedPaneCard(
    paneId: PaneId,
    canMoveUp: Boolean,
    canMoveDown: Boolean,
    onMoveUp: () -> Unit,
    onMoveDown: () -> Unit,
    onToggleCollapse: () -> Unit,
) {
    Card(
        modifier = Modifier.fillMaxWidth(),
        colors = CardDefaults.cardColors(containerColor = PanelSecondary),
        shape = RoundedCornerShape(16.dp),
    ) {
        PaneHeader(
            paneId = paneId,
            collapsed = true,
            canMoveUp = canMoveUp,
            canMoveDown = canMoveDown,
            onMoveUp = onMoveUp,
            onMoveDown = onMoveDown,
            onToggleCollapse = onToggleCollapse,
        )
    }
}

@Composable
private fun PaneHeader(
    paneId: PaneId,
    collapsed: Boolean,
    canMoveUp: Boolean,
    canMoveDown: Boolean,
    onMoveUp: () -> Unit,
    onMoveDown: () -> Unit,
    onToggleCollapse: () -> Unit,
) {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .background(if (collapsed) PanelTertiary else PanelPrimary)
            .padding(horizontal = 10.dp, vertical = 6.dp),
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.SpaceBetween,
    ) {
        Text(paneId.title, color = TextPrimary, fontWeight = FontWeight.Bold)
        Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
            HeaderActionButton(label = if (collapsed) "Expand" else "Collapse", onClick = onToggleCollapse)
            HeaderActionButton(label = "↑", enabled = canMoveUp, onClick = onMoveUp)
            HeaderActionButton(label = "↓", enabled = canMoveDown, onClick = onMoveDown)
        }
    }
}

@Composable
private fun HeaderActionButton(
    label: String,
    enabled: Boolean = true,
    onClick: () -> Unit,
) {
    TextButton(
        onClick = onClick,
        enabled = enabled,
        contentPadding = PaddingValues(horizontal = 6.dp, vertical = 0.dp),
        colors = ButtonDefaults.textButtonColors(
            contentColor = Accent,
            disabledContentColor = TextMuted,
        ),
    ) {
        Text(label, fontSize = 11.sp)
    }
}

@Composable
private fun PaneDivider(
    enabled: Boolean,
    onDrag: (Float) -> Unit,
    onDragFinished: () -> Unit,
) {
    val dragState = rememberDraggableState { delta ->
        if (enabled) onDrag(delta)
    }

    Box(
        modifier = Modifier
            .fillMaxWidth()
            .height(18.dp)
            .draggable(
                state = dragState,
                orientation = Orientation.Vertical,
                enabled = enabled,
                onDragStopped = { onDragFinished() },
            ),
        contentAlignment = Alignment.CenterStart,
    ) {
        Box(
            modifier = Modifier
                .fillMaxWidth()
                .height(1.dp)
                .background(if (enabled) Accent.copy(alpha = 0.45f) else PanelTertiary),
        )
        Card(
            colors = CardDefaults.cardColors(
                containerColor = if (enabled) PanelPrimary else PanelTertiary,
            ),
            shape = RoundedCornerShape(999.dp),
            modifier = Modifier.padding(start = 28.dp),
        ) {
            Text(
                text = "↕",
                color = if (enabled) Accent else TextMuted,
                modifier = Modifier.padding(horizontal = 8.dp, vertical = 1.dp),
                fontSize = 13.sp,
                fontWeight = FontWeight.ExtraBold,
            )
        }
    }
}

@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun RowScope.TerminalInputField(
    inputFieldVersion: Int,
    commandInput: TextFieldValue,
    onValueChange: (TextFieldValue) -> Unit,
    focusRequester: FocusRequester,
    bringIntoViewRequester: BringIntoViewRequester,
    keyboardController: androidx.compose.ui.platform.SoftwareKeyboardController?,
    onSubmit: () -> Unit,
) {
    val scope = rememberCoroutineScope()

    key(inputFieldVersion) {
        BasicTextField(
            value = commandInput,
            onValueChange = onValueChange,
            modifier = Modifier
                .weight(1f)
                .focusRequester(focusRequester)
                .bringIntoViewRequester(bringIntoViewRequester)
                .onFocusChanged { state ->
                    if (state.isFocused) {
                        keyboardController?.show()
                        scope.launch {
                            delay(150)
                            bringIntoViewRequester.bringIntoView()
                        }
                    }
                },
            singleLine = true,
            textStyle = TextStyle(
                color = TextPrimary,
                fontFamily = FontFamily.Monospace,
                fontSize = 15.sp,
                fontWeight = FontWeight.Normal,
            ),
            cursorBrush = SolidColor(Accent),
            keyboardOptions = KeyboardOptions(
                autoCorrect = false,
                keyboardType = KeyboardType.Ascii,
                imeAction = ImeAction.Done,
            ),
            keyboardActions = KeyboardActions(onDone = { onSubmit() }),
            decorationBox = { innerTextField ->
                Box(
                    modifier = Modifier
                        .fillMaxWidth()
                        .background(Color.Transparent)
                        .padding(vertical = 2.dp),
                    contentAlignment = Alignment.CenterStart,
                ) {
                    if (commandInput.text.isEmpty()) {
                        Text(
                            text = "Enter git command",
                            color = TextMuted.copy(alpha = 0.7f),
                            fontFamily = FontFamily.Monospace,
                            fontSize = 15.sp,
                        )
                    }
                    innerTextField()
                }
            },
        )
    }
}

@Composable
private fun LevelsPane(
    levels: List<Level>,
    currentLevelIndex: Int,
    completedLevels: Set<String>,
    onSelect: (Int) -> Unit,
) {
    Column(
        modifier = Modifier.fillMaxWidth(),
        verticalArrangement = Arrangement.spacedBy(6.dp),
    ) {
        levels.forEachIndexed { index, level ->
            Card(
                colors = CardDefaults.cardColors(
                    containerColor = if (index == currentLevelIndex) PanelTertiary else PanelPrimary,
                ),
                shape = RoundedCornerShape(10.dp),
            ) {
                TextButton(
                    onClick = { onSelect(index) },
                    modifier = Modifier.fillMaxWidth(),
                    colors = ButtonDefaults.textButtonColors(
                        contentColor = if (index == currentLevelIndex) Accent else TextSecondary,
                    ),
                ) {
                    Text(
                        text = buildString {
                            append(if (level.id in completedLevels) "✓ " else "• ")
                            append(level.title)
                        },
                        modifier = Modifier.fillMaxWidth(),
                    )
                }
            }
        }
    }
}

@Composable
private fun VisualPane(repo: RepoState) {
    val configuration = LocalConfiguration.current
    val isWide = configuration.screenWidthDp >= 840

    if (isWide) {
        Row(
            modifier = Modifier.fillMaxSize(),
            horizontalArrangement = Arrangement.spacedBy(12.dp),
        ) {
            InfoPaneCard(
                title = "Workspace",
                content = repo.files.joinToString("\n") {
                    "${if (it.staged) "[staged]" else "[file]  "} ${it.name}"
                }.ifBlank { "(empty)" },
                modifier = Modifier.weight(1f),
            )
            InfoPaneCard(
                title = "Branches",
                content = buildString {
                    appendLine("HEAD -> ${repo.headBranch}")
                    repo.branches.forEach { (name, _) -> appendLine(name) }
                }.trim(),
                modifier = Modifier.weight(1f),
            )
            InfoPaneCard(
                title = "Commits",
                content = repo.commits.reversed().joinToString("\n") { "${it.id} ${it.message}" }.ifBlank { "No commits yet" },
                modifier = Modifier.weight(1f),
            )
        }
    } else {
        Column(
            modifier = Modifier
                .fillMaxWidth(),
            verticalArrangement = Arrangement.spacedBy(10.dp),
        ) {
            InfoPaneCard(
                title = "Workspace",
                content = repo.files.joinToString("\n") {
                    "${if (it.staged) "[staged]" else "[file]  "} ${it.name}"
                }.ifBlank { "(empty)" },
            )
            InfoPaneCard(
                title = "Branches",
                content = buildString {
                    appendLine("HEAD -> ${repo.headBranch}")
                    repo.branches.forEach { (name, _) -> appendLine(name) }
                }.trim(),
            )
            InfoPaneCard(
                title = "Commits",
                content = repo.commits.reversed().joinToString("\n") { "${it.id} ${it.message}" }.ifBlank { "No commits yet" },
            )
        }
    }
}

@Composable
private fun ExercisePane(
    level: Level,
    visibleHint: String?,
    suggestionsExpanded: Boolean,
    onToggleSuggestions: () -> Unit,
    onHint: () -> Unit,
    onReset: () -> Unit,
) {
    Column(
        modifier = Modifier.fillMaxWidth(),
        verticalArrangement = Arrangement.spacedBy(12.dp),
    ) {
        Text(level.title, style = MaterialTheme.typography.titleLarge, color = TextPrimary)
        Text(level.description, color = TextSecondary)
        Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
            Button(
                onClick = onHint,
                colors = ButtonDefaults.buttonColors(
                    containerColor = Accent,
                    contentColor = Color.Black,
                ),
            ) {
                Text("Hint")
            }
            TextButton(
                onClick = onToggleSuggestions,
                colors = ButtonDefaults.textButtonColors(contentColor = Accent),
            ) {
                Text("Suggestions")
            }
            TextButton(
                onClick = onReset,
                colors = ButtonDefaults.textButtonColors(contentColor = Accent),
            ) {
                Text("Reset level")
            }
        }
        if (suggestionsExpanded && level.commandSuggestions.isNotEmpty()) {
            Text("Suggestions", color = TextPrimary, fontWeight = FontWeight.Bold)
            level.commandSuggestions.forEach { suggestion ->
                Text(suggestion, color = TextSecondary, fontFamily = FontFamily.Monospace)
            }
        }
        if (visibleHint != null) {
            Text("Hint", color = TextPrimary, fontWeight = FontWeight.Bold)
            Text(visibleHint, color = TextSecondary)
        }
    }
}

@Composable
private fun InfoPaneCard(
    title: String,
    content: String,
    modifier: Modifier = Modifier,
) {
    Card(
        modifier = modifier.fillMaxWidth(),
        colors = CardDefaults.cardColors(containerColor = PanelPrimary),
        shape = RoundedCornerShape(10.dp),
    ) {
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(8.dp),
            verticalArrangement = Arrangement.spacedBy(4.dp),
        ) {
            Text(title, color = TextPrimary, fontWeight = FontWeight.Bold)
            Text(
                content,
                color = TextSecondary,
                fontFamily = FontFamily.Monospace,
            )
        }
    }
}

@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun TerminalPane(
    output: List<String>,
    inputFieldVersion: Int,
    commandInput: TextFieldValue,
    onValueChange: (TextFieldValue) -> Unit,
    onRun: () -> Unit,
    onTab: () -> Unit,
    onHelp: () -> Unit,
    onCursorLeft: () -> Unit,
    onCursorRight: () -> Unit,
    onHistoryUp: () -> Unit,
    onHistoryDown: () -> Unit,
) {
    val configuration = LocalConfiguration.current
    val focusRequester = remember { FocusRequester() }
    val bringIntoViewRequester = remember { BringIntoViewRequester() }
    val focusManager = LocalFocusManager.current
    val keyboardController = LocalSoftwareKeyboardController.current
    val horizontalScroll = rememberScrollState()
    val outputVerticalScroll = rememberScrollState()
    val terminalMinWidth = 80.dp * 7.2f
    val terminalPaneMaxHeight = configuration.screenHeightDp.dp * 0.7f
    val reservedControlsHeight = 86.dp
    val maxOutputHeight = (terminalPaneMaxHeight - reservedControlsHeight).coerceAtLeast(120.dp)
    val outputLineHeight = 20.dp
    val desiredOutputHeight = (output.size.coerceAtLeast(6) * outputLineHeight.value).dp.coerceAtMost(maxOutputHeight)

    LaunchedEffect(inputFieldVersion) {
        focusRequester.requestFocus()
        keyboardController?.show()
        horizontalScroll.scrollTo(0)
    }

    LaunchedEffect(output.size) {
        outputVerticalScroll.animateScrollTo(outputVerticalScroll.maxValue)
    }

    LaunchedEffect(output.size, commandInput.text) {
        if (output.isNotEmpty() && commandInput.text.isEmpty()) {
            horizontalScroll.scrollTo(0)
        }
    }

    fun submitCommand() {
        focusManager.clearFocus(force = true)
        keyboardController?.hide()
        onRun()
    }

    Box(
        modifier = Modifier
            .fillMaxSize()
            .background(TerminalBackground, RoundedCornerShape(12.dp))
            .padding(8.dp)
            .horizontalScroll(horizontalScroll),
    ) {
        Column(
            modifier = Modifier
                .fillMaxHeight()
                .widthIn(min = terminalMinWidth),
            verticalArrangement = Arrangement.spacedBy(8.dp),
        ) {
            Box(
                modifier = Modifier
                    .heightIn(min = 120.dp, max = maxOutputHeight)
                    .height(desiredOutputHeight)
                    .fillMaxWidth()
                    .background(PanelPrimary, RoundedCornerShape(10.dp))
                    .padding(8.dp)
                    .verticalScroll(outputVerticalScroll),
            ) {
                Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
                    output.forEach { line ->
                        Text(
                            text = line,
                            color = if (line.startsWith("✔") || line.startsWith("🏁")) Success else TextSecondary,
                            fontFamily = FontFamily.Monospace,
                        )
                    }
                }
            }
            SpecialKeyBar(
                onTab = onTab,
                onHelp = onHelp,
                onCursorLeft = onCursorLeft,
                onCursorRight = onCursorRight,
                onHistoryUp = onHistoryUp,
                onHistoryDown = onHistoryDown,
            )
            Row(
                modifier = Modifier
                    .bringIntoViewRequester(bringIntoViewRequester)
                    .fillMaxWidth()
                    .widthIn(min = terminalMinWidth)
                    .background(PanelPrimary, RoundedCornerShape(8.dp))
                    .padding(horizontal = 8.dp, vertical = 3.dp),
                horizontalArrangement = Arrangement.spacedBy(8.dp),
                verticalAlignment = Alignment.CenterVertically,
            ) {
                Text(
                    text = "$",
                    color = Accent,
                    fontFamily = FontFamily.Monospace,
                    fontWeight = FontWeight.Bold,
                    fontSize = 15.sp,
                )
                TerminalInputField(
                    inputFieldVersion = inputFieldVersion,
                    commandInput = commandInput,
                    onValueChange = onValueChange,
                    focusRequester = focusRequester,
                    bringIntoViewRequester = bringIntoViewRequester,
                    keyboardController = keyboardController,
                    onSubmit = { submitCommand() },
                )
            }
        }
    }
}

@Composable
private fun SpecialKeyBar(
    onTab: () -> Unit,
    onHelp: () -> Unit,
    onCursorLeft: () -> Unit,
    onCursorRight: () -> Unit,
    onHistoryUp: () -> Unit,
    onHistoryDown: () -> Unit,
) {
    Row(
        modifier = Modifier.fillMaxWidth(),
        horizontalArrangement = Arrangement.spacedBy(6.dp),
    ) {
        TerminalKeyButton(label = "↹", onClick = onTab, modifier = Modifier.width(56.dp), fontFamily = FontFamily.Default)
        TerminalKeyButton(label = "?", onClick = onHelp, modifier = Modifier.width(40.dp), fontFamily = FontFamily.Default)
        TerminalKeyButton(label = "←", onClick = onCursorLeft, modifier = Modifier.width(40.dp))
        TerminalKeyButton(label = "→", onClick = onCursorRight, modifier = Modifier.width(40.dp))
        TerminalKeyButton(label = "↑", onClick = onHistoryUp, modifier = Modifier.width(40.dp))
        TerminalKeyButton(label = "↓", onClick = onHistoryDown, modifier = Modifier.width(40.dp))
    }
}

@Composable
private fun TerminalKeyButton(
    label: String,
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    fontFamily: FontFamily = FontFamily.Monospace,
) {
    Button(
        onClick = onClick,
        modifier = modifier.height(28.dp),
        contentPadding = PaddingValues(horizontal = 8.dp, vertical = 2.dp),
        colors = ButtonDefaults.buttonColors(
            containerColor = PanelSecondary,
            contentColor = TextPrimary,
        ),
    ) {
        Text(
            text = label,
            fontFamily = fontFamily,
            fontWeight = FontWeight.Bold,
            fontSize = 11.sp,
        )
    }
}

private fun movePane(layout: PaneLayout, paneId: PaneId, delta: Int): PaneLayout {
    val currentIndex = layout.order.indexOf(paneId)
    if (currentIndex == -1) return layout
    val targetIndex = (currentIndex + delta).coerceIn(0, layout.order.lastIndex)
    if (currentIndex == targetIndex) return layout

    val updatedOrder = layout.order.toMutableList()
    updatedOrder.removeAt(currentIndex)
    updatedOrder.add(targetIndex, paneId)
    return layout.copy(order = updatedOrder)
}

private fun resizePanes(
    layout: PaneLayout,
    upper: PaneId,
    lower: PaneId,
    dragFraction: Float,
): PaneLayout {
    if (upper in layout.collapsed || lower in layout.collapsed) return layout

    val minWeight = 0.6f
    val upperWeight = layout.weights[upper] ?: 1f
    val lowerWeight = layout.weights[lower] ?: 1f
    val scaledDelta = dragFraction.coerceIn(-0.75f, 0.75f)

    val newUpper = (upperWeight + scaledDelta).coerceAtLeast(minWeight)
    val newLower = (lowerWeight - scaledDelta).coerceAtLeast(minWeight)

    return layout.copy(
        weights = layout.weights + mapOf(
            upper to newUpper,
            lower to newLower,
        )
    )
}

private fun recommendedPaneHeights(
    level: Level,
    levelCount: Int,
    outputLineCount: Int,
    screenHeightDp: Int,
    suggestionsVisible: Boolean,
    visibleHint: String?,
): Map<PaneId, Int> {
    val screenHeight = screenHeightDp.coerceAtLeast(560)
    val levelsHeight = (56 + levelCount * 44).coerceAtLeast(160)
    val descriptionLines = (level.description.length / 42).coerceAtLeast(2) + 2
    val suggestionsLines = if (suggestionsVisible) level.commandSuggestions.size + 1 else 0
    val hintLines = visibleHint?.let { (it.length / 42).coerceAtLeast(1) + 1 } ?: 0
    val exerciseHeight = (120 + descriptionLines * 24 + suggestionsLines * 22 + hintLines * 22).coerceAtLeast(180)
    val terminalHeight = (170 + outputLineCount.coerceAtLeast(1) * 20).coerceAtMost((screenHeight * 0.7f).toInt())
    val visualHeight = (screenHeight * 0.24f).toInt().coerceAtLeast(170)

    return mapOf(
        PaneId.EXERCISE to exerciseHeight,
        PaneId.TERMINAL to terminalHeight,
        PaneId.VISUAL to visualHeight,
        PaneId.LEVELS to levelsHeight,
    )
}

private fun recommendedPaneWeights(
    heights: Map<PaneId, Int>,
): Map<PaneId, Float> {
    fun weightFor(height: Int): Float = (height / 180f).coerceAtLeast(0.6f)

    return heights.mapValues { (_, height) -> weightFor(height) }
}

private fun recommendedWorkspaceHeight(
    paneLayout: PaneLayout,
    recommendedHeights: Map<PaneId, Int>,
): androidx.compose.ui.unit.Dp {
    val dividerHeight = 18 * (paneLayout.order.size - 1)
    val collapsedPaneHeight = 44 * paneLayout.collapsed.size
    val expandedHeight = paneLayout.order
        .filterNot { it in paneLayout.collapsed }
        .sumOf { pane -> recommendedHeights[pane] ?: 0 }
    return (expandedHeight + dividerHeight + collapsedPaneHeight).dp
}

private fun commonPrefix(values: List<String>): String {
    if (values.isEmpty()) return ""
    var prefix = values.first()
    values.drop(1).forEach { value ->
        while (!value.startsWith(prefix) && prefix.isNotEmpty()) {
            prefix = prefix.dropLast(1)
        }
    }
    return prefix
}
