package dev.ulfrx.recipe.ui.components.calendar

import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.draggable
import androidx.compose.foundation.gestures.rememberDraggableState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
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.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.layout
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.lerp
import dev.ulfrx.recipe.ui.components.glass.GlassSurface
import dev.ulfrx.recipe.ui.theme.RecipeGlassStyle
import dev.ulfrx.recipe.ui.theme.RecipeTheme
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.datetime.LocalDate

enum class CalendarPillExpandDirection {
    /** Pill anchored at the bottom; calendar slides into view from above (planner pattern). */
    Up,

    /** Pill anchored at the top; calendar grows downward beneath it (in-sheet editor pattern). */
    Down,
    ;

    /** Sign convention: positive drag/velocity along this axis opens the pill. */
    val openingSign: Float
        get() =
            when (this) {
                Up -> -1f
                Down -> 1f
            }
}

@Composable
fun CalendarPill(
    expanded: Boolean,
    onExpandedChange: (Boolean) -> Unit,
    selectedDate: LocalDate,
    today: LocalDate,
    onSelectDate: (LocalDate) -> Unit,
    modifier: Modifier = Modifier,
    label: String = "",
    collapsedContent: (@Composable RowScope.() -> Unit)? = null,
    trailing: (@Composable () -> Unit)? = null,
    dayState: (LocalDate) -> DayState = { DayState() },
    pillHeight: Dp = 48.dp,
    locale: CalendarLocale = CalendarLocale.PL,
    expandDirection: CalendarPillExpandDirection = CalendarPillExpandDirection.Up,
    tint: Color = RecipeTheme.colors.surfaceGlass,
    glass: Boolean = true,
) {
    val scope = rememberCoroutineScope()
    val expansion = remember { PillExpansion(initial = if (expanded) 1f else 0f) }

    LaunchedEffect(expanded) {
        expansion.animateTo(scope, target = if (expanded) 1f else 0f)
    }

    val progress = expansion.progress
    val cornerRadius = pillHeight / 2 * (1f - progress) + EXPANDED_CORNER_RADIUS * progress
    val pillInset = RecipeTheme.spacing.lg + RecipeTheme.spacing.xs
    val pillHeightPx = with(LocalDensity.current) { pillHeight.toPx() }
    val dragState =
        rememberDraggableState { delta ->
            expansion.dragBy(
                delta = delta,
                range = (expansion.fullHeightPx - pillHeightPx).coerceAtLeast(1f),
                direction = expandDirection,
            )
        }

    PillSurface(
        glass = glass,
        tint = tint,
        cornerRadius = cornerRadius,
        glassStyle = if (expanded) RecipeTheme.glass.panel else RecipeTheme.glass.dock,
        modifier =
            modifier.draggable(
                state = dragState,
                orientation = Orientation.Vertical,
                onDragStarted = { expansion.cancelSettle() },
                onDragStopped = { velocity ->
                    val openTarget = releaseTarget(expansion.progress, velocity, expandDirection)
                    val range = (expansion.fullHeightPx - pillHeightPx).coerceAtLeast(1f)
                    val initialVelocity = expandDirection.openingSign * velocity / range
                    expansion.animateTo(scope, if (openTarget) 1f else 0f, initialVelocity = initialVelocity)
                    if (openTarget != expanded) onExpandedChange(openTarget)
                },
            ),
    ) {
        Box(modifier = Modifier.fillMaxWidth()) {
            CompositionLocalProvider(LocalCalendarInteractive provides expanded) {
                Box(
                    modifier =
                        Modifier
                            .fillMaxWidth()
                            .expandingHeight(progress, pillHeight, expansion, expandDirection)
                            .alpha(progress),
                ) {
                    SwipeableCalendar(
                        selectedDate = selectedDate,
                        today = today,
                        mode = CalendarMode.Month,
                        onSelectDate = onSelectDate,
                        onModeChange = {},
                        onVisibleAnchorChange = {},
                        dayState = dayState,
                        expandable = false,
                        locale = locale,
                        modifier = Modifier.fillMaxWidth().padding(vertical = RecipeTheme.spacing.lg),
                    )
                }
            }

            val rowAlpha = (1f - progress / PILL_CONTENT_FADE_END).coerceIn(0f, 1f)
            if (rowAlpha > 0f) {
                val pillRowAlignment =
                    when (expandDirection) {
                        CalendarPillExpandDirection.Up -> Alignment.BottomCenter
                        CalendarPillExpandDirection.Down -> Alignment.TopCenter
                    }
                Box(
                    modifier =
                        Modifier
                            .fillMaxWidth()
                            .align(pillRowAlignment)
                            .alpha(rowAlpha),
                ) {
                    PillRow(
                        label = label,
                        collapsedContent = collapsedContent,
                        trailing = trailing,
                        height = pillHeight,
                        horizontalInset = pillInset,
                    )
                }
            }
        }
    }
}

/**
 * Surface wrapper for the pill. Glass mode is the default and matches the
 * planner pattern where the pill sits over a varied app-shell backdrop and
 * refraction earns its keep. The flat mode is for in-sheet contexts where the
 * backdrop is mostly a solid colour — refraction has nothing meaningful to
 * refract and only adds visual noise.
 */
@Composable
private fun PillSurface(
    glass: Boolean,
    tint: Color,
    cornerRadius: Dp,
    glassStyle: RecipeGlassStyle,
    modifier: Modifier,
    content: @Composable BoxScope.() -> Unit,
) {
    if (glass) {
        GlassSurface(
            modifier = modifier,
            cornerRadius = cornerRadius,
            glassStyle = glassStyle,
            content = content,
        )
    } else {
        val colors = RecipeTheme.colors
        val shape = RoundedCornerShape(cornerRadius)
        Box(
            modifier =
                modifier
                    .clip(shape)
                    .background(tint)
                    .border(width = FlatBorderWidth, color = colors.borderCard, shape = shape),
            content = content,
        )
    }
}

@Composable
private fun PillRow(
    label: String,
    collapsedContent: (@Composable RowScope.() -> Unit)?,
    trailing: (@Composable () -> Unit)?,
    height: Dp,
    horizontalInset: Dp,
) {
    Row(
        modifier =
            Modifier
                .fillMaxWidth()
                .height(height)
                .padding(horizontal = horizontalInset),
        horizontalArrangement = Arrangement.spacedBy(RecipeTheme.spacing.sm),
        verticalAlignment = Alignment.CenterVertically,
    ) {
        if (collapsedContent != null) {
            collapsedContent()
        } else {
            BasicText(
                text = label,
                style = RecipeTheme.typography.body.copy(color = RecipeTheme.colors.content),
                maxLines = 1,
                overflow = TextOverflow.Ellipsis,
                modifier = Modifier.weight(1f),
            )
            trailing?.invoke()
        }
    }
}

/**
 * Measures the calendar at its full intrinsic height, reports it to [expansion]
 * so drag knows the range, then lays out at the lerped height. The placement
 * anchor flips with [direction]: anchoring the calendar's bottom edge makes it
 * slide in from above (pill at bottom); anchoring the top edge makes the
 * calendar reveal downward (pill at top).
 */
private fun Modifier.expandingHeight(
    progress: Float,
    pillHeight: Dp,
    expansion: PillExpansion,
    direction: CalendarPillExpandDirection,
): Modifier =
    this.layout { measurable, constraints ->
        val placeable =
            measurable.measure(constraints.copy(minHeight = 0, maxHeight = Constraints.Infinity))
        expansion.reportFullHeight(placeable.height)
        val pillHeightPx = pillHeight.roundToPx()
        val height = lerp(pillHeightPx, placeable.height, progress).coerceIn(pillHeightPx, placeable.height)
        layout(placeable.width, height) {
            val placementY =
                when (direction) {
                    CalendarPillExpandDirection.Up -> height - placeable.height
                    CalendarPillExpandDirection.Down -> 0
                }
            placeable.place(0, placementY)
        }
    }

/**
 * Single source of truth for pill drag/settle state. Holds [progress] (0 =
 * collapsed, 1 = expanded) and tracks [target] so external [expanded] changes
 * that match an in-flight settle become no-ops — no flag, no race.
 */
@Stable
private class PillExpansion(
    initial: Float,
) {
    var progress by mutableFloatStateOf(initial)
        private set
    var fullHeightPx by mutableIntStateOf(0)
        private set

    private var target: Float = initial
    private var settleJob: Job? = null

    fun dragBy(
        delta: Float,
        range: Float,
        direction: CalendarPillExpandDirection,
    ) {
        settleJob?.cancel()
        progress = (progress + direction.openingSign * delta / range).coerceIn(0f, 1f)
        target = progress
    }

    fun animateTo(
        scope: CoroutineScope,
        target: Float,
        initialVelocity: Float = 0f,
    ) {
        if (this.target == target && settleJob?.isActive == true) return
        this.target = target
        settleJob?.cancel()
        settleJob =
            scope.launch {
                Animatable(progress)
                    .also { it.updateBounds(0f, 1f) }
                    .animateTo(
                        targetValue = target,
                        animationSpec =
                            spring(
                                dampingRatio = Spring.DampingRatioNoBouncy,
                                stiffness = Spring.StiffnessMediumLow,
                            ),
                        initialVelocity = initialVelocity,
                    ) { progress = value }
            }
    }

    fun cancelSettle() {
        settleJob?.cancel()
    }

    fun reportFullHeight(height: Int) {
        if (fullHeightPx != height) fullHeightPx = height
    }
}

private fun releaseTarget(
    progress: Float,
    velocity: Float,
    direction: CalendarPillExpandDirection,
): Boolean {
    val openingVelocity = direction.openingSign * velocity
    return when {
        openingVelocity >= FLING_VELOCITY -> true
        openingVelocity <= -FLING_VELOCITY -> false
        else -> progress >= 0.5f
    }
}

private const val FLING_VELOCITY = 60f
private const val PILL_CONTENT_FADE_END = 0.35f
private val EXPANDED_CORNER_RADIUS = 28.dp
private val FlatBorderWidth = 1.dp
