package com.ponzischeme89.memby.data

import com.ponzischeme89.memby.data.model.BaseItem

/**
 * The direct-to-Emby copy of the gateway's `mergeContinueWatching`.
 *
 * The rule exists twice on purpose, like the subtitle rule does: with no gateway there is
 * nobody to ask, and Continue Watching must not hold different cards depending on whether
 * the container is up. `ContinueWatchingTest` and `continue_watching_test.go` are
 * deliberately parallel — change one and change the other.
 *
 * Resume items and Next Up episodes are one row because they answer one question. Both
 * lists arrive most-recently-watched first, so this is a merge of two sorted lists and
 * never a sort of their union: Emby's order within each is the useful part. What places a
 * Next Up episode is when its *series* was last watched, since the episode itself is
 * unwatched and carries no date of its own.
 */
internal fun mergeContinueWatching(
    resume: List<BaseItem>,
    nextUp: List<BaseItem>,
    seriesLastPlayed: Map<String, String>,
): List<BaseItem> {
    val seenItems = mutableSetOf<String>()
    val seenSeries = mutableSetOf<String>()
    val inProgress = resume.map { item ->
        seenItems += item.id
        item.continueSeriesKey()?.let { seenSeries += it }
        item to item.continuePlayedAt(seriesLastPlayed)
    }
    // A series being watched right now is represented by the episode it is part-way
    // through, not by the one after it.
    val upNext = nextUp
        .filter { item ->
            val series = item.continueSeriesKey()
            item.id !in seenItems && (series == null || seenSeries.add(series))
        }
        .map { item -> item to item.continuePlayedAt(seriesLastPlayed) }

    val merged = ArrayList<BaseItem>(inProgress.size + upNext.size)
    var left = 0
    var right = 0
    while (left < inProgress.size && right < upNext.size) {
        val (nextItem, nextAt) = upNext[right]
        val (currentItem, currentAt) = inProgress[left]
        // An undated card never displaces a dated one, and falls back to the resume half:
        // that is the list which is definitely in progress.
        if (nextAt != null && (currentAt == null || nextAt > currentAt)) {
            merged += nextItem
            right++
        } else {
            merged += currentItem
            left++
        }
    }
    while (left < inProgress.size) merged += inProgress[left++].first
    while (right < upNext.size) merged += upNext[right++].first
    return merged
}

private fun BaseItem.continueSeriesKey(): String? = seriesId?.takeIf(String::isNotBlank)

private fun BaseItem.continuePlayedAt(seriesLastPlayed: Map<String, String>): String? =
    normalizePlayedAt(userData?.lastPlayedDate)
        ?: continueSeriesKey()?.let { normalizePlayedAt(seriesLastPlayed[it]) }

/**
 * Emby writes UTC timestamps with .NET's seven fractional digits
 * (`2026-08-06T21:04:05.1234567Z`), so trimming to whole seconds makes them fixed-width
 * and directly comparable as strings — `java.time` is not available at this minSdk.
 * Anything that is not a recognisable timestamp is undated rather than mis-ordered.
 */
internal fun normalizePlayedAt(value: String?): String? {
    val trimmed = value?.trim().orEmpty()
    if (trimmed.length < 19) return null
    val stamp = trimmed.take(19)
    val shaped = stamp[4] == '-' && stamp[7] == '-' && stamp[10] == 'T' &&
        stamp[13] == ':' && stamp[16] == ':'
    if (!shaped) return null
    val digits = stamp.filterIndexed { index, _ -> index !in setOf(4, 7, 10, 13, 16) }
    if (!digits.all(Char::isDigit)) return null
    // Emby's "never played" sentinel. A real date it is not, and treating it as one would
    // sort a card by a year nobody watched anything in.
    if (stamp.startsWith("0001-01-01")) return null
    return stamp
}
