package com.fabledsword.minstrel.player.ui

import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SwipeToDismissBox
import androidx.compose.material3.SwipeToDismissBoxValue
import androidx.compose.material3.Text
import androidx.compose.material3.rememberSwipeToDismissBoxState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.semantics.CustomAccessibilityAction
import androidx.compose.ui.semantics.customActions
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import com.composables.icons.lucide.Lucide
import com.composables.icons.lucide.Music
import com.composables.icons.lucide.Trash2
import com.composables.icons.lucide.Volume2
import com.fabledsword.minstrel.models.TrackRef
import com.fabledsword.minstrel.shared.formatDuration
import com.fabledsword.minstrel.shared.widgets.LikeButton
import com.fabledsword.minstrel.shared.widgets.ServerImage
import com.fabledsword.minstrel.theme.LocalActionColors
import kotlin.math.roundToInt

/*
 * A single queue row, split out of QueueScreen.kt when swipe-to-remove (#2435)
 * pushed that file past detekt's TooManyFunctions limit. The seam is real and
 * not just a way to satisfy the analyzer: the row now carries two gestures, a
 * swipe background, and its own accessibility surface, which is more behaviour
 * than the screen that lists it. `internal` rather than `private` only because
 * QueueList (still in QueueScreen.kt) is the caller.
 */

@Suppress("LongParameterList") // Compose row wiring — layout + queue callbacks, not logic.
@Composable
internal fun QueueRow(
    track: TrackRef,
    index: Int,
    queueSize: Int,
    isCurrent: Boolean,
    liked: Boolean,
    onClick: () -> Unit,
    onToggleLike: () -> Unit,
    onRemove: () -> Unit,
    onMove: (Int, Int) -> Unit,
) {
    var dragOffsetY by remember { mutableFloatStateOf(0f) }
    var rowHeightPx by remember { mutableIntStateOf(0) }
    val highlight = if (isCurrent) {
        MaterialTheme.colorScheme.primary.copy(alpha = HIGHLIGHT_ALPHA)
    } else {
        Color.Transparent
    }
    // Swipe left to remove, replacing the X button (#2395 follow-up). Only
    // end-to-start is enabled: a right-swipe has no meaning here, and leaving it
    // live would delete tracks on a mis-aimed gesture in either direction.
    val dismissState = rememberSwipeToDismissBoxState(
        confirmValueChange = { value ->
            if (value == SwipeToDismissBoxValue.EndToStart) {
                onRemove()
                true
            } else {
                false
            }
        },
    )
    SwipeToDismissBox(
        state = dismissState,
        enableDismissFromStartToEnd = false,
        backgroundContent = { RemoveSwipeBackground() },
        // The reorder lift lives out here so a row being dragged vertically
        // carries its swipe container with it rather than sliding out of one.
        modifier = Modifier
            .onSizeChanged { rowHeightPx = it.height }
            .zIndex(if (dragOffsetY != 0f) 1f else 0f)
            .graphicsLayer { translationY = dragOffsetY },
    ) {
        QueueRowContent(
            track = track,
            index = index,
            queueSize = queueSize,
            isCurrent = isCurrent,
            liked = liked,
            highlight = highlight,
            rowHeightPx = rowHeightPx,
            onClick = onClick,
            onToggleLike = onToggleLike,
            onRemove = onRemove,
            onMove = onMove,
            onDragOffset = { dragOffsetY = it },
        )
    }
}

@Suppress("LongParameterList") // Compose row wiring — layout + queue callbacks, not logic.
@Composable
private fun QueueRowContent(
    track: TrackRef,
    index: Int,
    queueSize: Int,
    isCurrent: Boolean,
    liked: Boolean,
    highlight: Color,
    rowHeightPx: Int,
    onClick: () -> Unit,
    onToggleLike: () -> Unit,
    onRemove: () -> Unit,
    onMove: (Int, Int) -> Unit,
    onDragOffset: (Float) -> Unit,
) {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            // Opaque: this sits ON TOP of the red remove background, so a
            // transparent row would show the fill through it at rest.
            .background(MaterialTheme.colorScheme.surface)
            .background(highlight)
            .clickable(onClick = onClick)
            .queueReorderActions(
                index = index,
                queueSize = queueSize,
                onMove = onMove,
                onRemove = onRemove,
            )
            .padding(horizontal = 16.dp, vertical = 12.dp),
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.spacedBy(12.dp),
    ) {
        // The album art IS the grab surface (#2395). The grip icon it replaces
        // cost ~36dp of every row's width — icon plus its 12dp gap — on the
        // narrowest surface in the app, competing with the title for space.
        QueueRowThumbnail(
            track = track,
            dragModifier = Modifier.queueReorderDrag(
                index = index,
                queueSize = queueSize,
                rowHeightPx = rowHeightPx,
                onOffsetChange = onDragOffset,
                onMove = onMove,
            ),
        )
        if (isCurrent) {
            Icon(
                Lucide.Volume2,
                contentDescription = "Now playing",
                tint = MaterialTheme.colorScheme.primary,
            )
        }
        QueueRowText(track = track, isCurrent = isCurrent, modifier = Modifier.weight(1f))
        if (track.durationSec > 0) {
            Text(
                text = formatDuration(track.durationSec),
                style = MaterialTheme.typography.bodySmall,
                color = MaterialTheme.colorScheme.onSurfaceVariant,
            )
        }
        LikeButton(liked = liked, onToggle = onToggleLike)
    }
}

/**
 * What the row slides off to reveal: the destructive colour with a trash glyph,
 * pinned to the trailing edge because that is the edge the swipe uncovers.
 *
 * Oxblood (LocalActionColors.destructive), NOT colorScheme.error. The design
 * system keeps those apart deliberately — an error is a failure that already
 * happened, a destructive action is one about to happen — and using the error
 * colour here would dress an intentional gesture as a fault report.
 */
@Composable
private fun RemoveSwipeBackground() {
    val actions = LocalActionColors.current
    Box(
        modifier = Modifier
            .fillMaxSize()
            .background(actions.destructive)
            .padding(horizontal = 24.dp),
        contentAlignment = Alignment.CenterEnd,
    ) {
        Row(
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.spacedBy(8.dp),
        ) {
            Icon(Lucide.Trash2, contentDescription = null, tint = actions.onAction)
            Text(
                text = "Remove",
                style = MaterialTheme.typography.labelLarge,
                color = actions.onAction,
            )
        }
    }
}

/**
 * Screen-reader reordering and removal for a queue row.
 *
 * Both gestures this row now relies on — long-press-drag to reorder, swipe to
 * remove — are touch-only and unavailable under TalkBack, and each replaced a
 * control that a screen reader COULD find (the grip's "Reorder track", the X's
 * "Remove from queue"). Without these actions the row would have lost both
 * capabilities for anyone not using touch. They're the Android counterpart to
 * the web row's ArrowUp/ArrowDown keys and its still-present X button.
 */
private fun Modifier.queueReorderActions(
    index: Int,
    queueSize: Int,
    onMove: (Int, Int) -> Unit,
    onRemove: () -> Unit,
): Modifier = semantics {
    customActions = listOf(
        CustomAccessibilityAction("Move up") {
            if (index > 0) { onMove(index, index - 1); true } else false
        },
        CustomAccessibilityAction("Move down") {
            if (index < queueSize - 1) { onMove(index, index + 1); true } else false
        },
        CustomAccessibilityAction("Remove from queue") { onRemove(); true },
    )
}

/**
 * Reorder-drag behaviour for a queue row, applied to whatever element is the
 * grab surface — the album art, since #2395 removed the grip icon.
 *
 * Uses **detectDragGesturesAfterLongPress**, not detectDragGestures, and that
 * is the load-bearing detail. The grip was a small target, so a plain drag
 * gesture on it never competed with anything. A 48dp thumbnail is a large
 * chunk of every row, and with a plain drag detector any vertical pan starting
 * on artwork would be swallowed as a row-reorder instead of scrolling the
 * queue — the list would feel broken precisely where it's easiest to touch.
 * Long-press-then-drag separates the two: pan scrolls, long-press reorders,
 * tap still plays (the detector doesn't consume a plain tap, so it falls
 * through to the row's clickable).
 */
private fun Modifier.queueReorderDrag(
    index: Int,
    queueSize: Int,
    rowHeightPx: Int,
    onOffsetChange: (Float) -> Unit,
    onMove: (Int, Int) -> Unit,
): Modifier = composed {
    // Mirrors the web queue: the row follows the finger during a drag, then on
    // release we translate the accumulated offset into a row delta and reorder.
    var offset by remember { mutableFloatStateOf(0f) }
    pointerInput(index, queueSize, rowHeightPx) {
        detectDragGesturesAfterLongPress(
            onDrag = { change, dragAmount ->
                change.consume()
                offset += dragAmount.y
                onOffsetChange(offset)
            },
            onDragEnd = {
                val delta = if (rowHeightPx > 0) (offset / rowHeightPx).roundToInt() else 0
                val target = (index + delta).coerceIn(0, queueSize - 1)
                if (target != index) onMove(index, target)
                offset = 0f
                onOffsetChange(0f)
            },
            onDragCancel = {
                offset = 0f
                onOffsetChange(0f)
            },
        )
    }
}

@Composable
private fun QueueRowThumbnail(track: TrackRef, dragModifier: Modifier = Modifier) {
    Box(
        modifier = Modifier
            .size(48.dp)
            .clip(RoundedCornerShape(4.dp))
            .background(MaterialTheme.colorScheme.surfaceVariant)
            .then(dragModifier),
        contentAlignment = Alignment.Center,
    ) {
        ServerImage(
            url = track.coverUrl,
            contentDescription = null,
            modifier = Modifier.size(48.dp),
        ) {
            Icon(
                Lucide.Music,
                contentDescription = null,
                tint = MaterialTheme.colorScheme.onSurfaceVariant,
            )
        }
    }
}

@Composable
private fun QueueRowText(track: TrackRef, isCurrent: Boolean, modifier: Modifier = Modifier) {
    Column(modifier = modifier) {
        Text(
            text = track.title,
            style = MaterialTheme.typography.bodyLarge,
            color = MaterialTheme.colorScheme.onSurface,
            fontWeight = if (isCurrent) FontWeight.Medium else FontWeight.Normal,
            maxLines = 1,
            overflow = TextOverflow.Ellipsis,
        )
        val subtitle = queueSubtitle(track)
        if (subtitle.isNotEmpty()) {
            Text(
                text = subtitle,
                style = MaterialTheme.typography.bodySmall,
                color = MaterialTheme.colorScheme.onSurfaceVariant,
                maxLines = 1,
                overflow = TextOverflow.Ellipsis,
            )
        }
    }
}

/** "Artist · Album" — collapses gracefully when either is missing. */
private fun queueSubtitle(track: TrackRef): String = listOf(track.artistName, track.albumTitle)
    .filter { it.isNotEmpty() }
    .joinToString(" · ")

private const val HIGHLIGHT_ALPHA = 0.12f
