package com.ponzischeme89.memby.data.analytics

import com.ponzischeme89.memby.data.model.GatewayRowEvent
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.TimeZone

/**
 * Collects what the viewer actually looked at, one row at a time.
 *
 * Three signals, cheapest to strongest:
 *
 *  - **impression** — the row was composed, so it was on screen at least briefly.
 *  - **focus** — the remote landed on it, with how long it stayed. This is the number
 *    worth reading: it separates "a row scrolled past" from "a row someone browsed".
 *  - **select** — something was opened from it.
 *
 * Events buffer in memory and are flushed in batches, because a D-pad generates focus
 * changes far faster than anything should generate HTTP requests. Nothing here retries or
 * persists: losing a batch to a crash costs a little telemetry and nothing else.
 */
class RowAnalytics(
    private val now: () -> Long = System::currentTimeMillis,
    private val maxBuffered: Int = 200,
) {
    private val lock = Any()
    private val buffer = ArrayList<GatewayRowEvent>()
    private val impressed = HashSet<String>()
    private val impressedItems = HashSet<String>()

    private var focusedRowId: String? = null
    private var focusedRowKind: String = ""
    private var focusStartedAt: Long = 0

    /** Records that a row was drawn. Repeats are ignored until [reset]. */
    fun rowImpression(rowId: String, rowKind: String, visibleItemIds: List<String> = emptyList()) {
        synchronized(lock) {
            if (impressed.add(rowId)) {
                add(GatewayRowEvent(rowId = rowId, rowKind = rowKind, event = EVENT_IMPRESSION, occurredAt = timestamp()))
            }
            visibleItemIds.filter(String::isNotBlank).forEach { itemId ->
                if (impressedItems.add("$rowId:$itemId")) {
                    add(
                        GatewayRowEvent(
                            rowId = rowId,
                            rowKind = rowKind,
                            event = EVENT_IMPRESSION,
                            itemId = itemId,
                            occurredAt = timestamp(),
                        ),
                    )
                }
            }
        }
    }

    /**
     * Records focus landing on [rowId]. Moving between cards inside one row extends that
     * row's dwell rather than starting a new measurement — the viewer is still reading
     * the same strip.
     */
    fun rowFocused(rowId: String, rowKind: String, itemId: String) {
        synchronized(lock) {
            if (focusedRowId == rowId) return
            closeOpenFocus()
            focusedRowId = rowId
            focusedRowKind = rowKind
            focusStartedAt = now()
            // The impression may not have fired if the row was already on screen when
            // this session's collector was created.
            if (impressed.add(rowId)) {
                add(GatewayRowEvent(rowId = rowId, rowKind = rowKind, event = EVENT_IMPRESSION, occurredAt = timestamp()))
            }
            lastFocusedItemId = itemId
        }
    }

    /** Records something being opened from a row. */
    fun rowSelected(rowId: String, rowKind: String, itemId: String) {
        synchronized(lock) {
            add(
                GatewayRowEvent(
                    rowId = rowId,
                    rowKind = rowKind,
                    event = EVENT_SELECT,
                    itemId = itemId,
                    occurredAt = timestamp(),
                ),
            )
        }
    }

    /**
     * Closes the open focus measurement — call when leaving the home screen, or before a
     * flush, so dwell is not lost while the viewer sits on one row.
     */
    fun endFocus() {
        synchronized(lock) { closeOpenFocus() }
    }

    /** Returns everything buffered and clears it. */
    fun drain(): List<GatewayRowEvent> = synchronized(lock) {
        if (buffer.isEmpty()) return emptyList()
        val events = buffer.toList()
        buffer.clear()
        events
    }

    fun hasPending(): Boolean = synchronized(lock) { buffer.isNotEmpty() }

    /** Forgets which rows have been seen, e.g. after a profile switch. */
    fun reset() {
        synchronized(lock) {
            buffer.clear()
            impressed.clear()
            impressedItems.clear()
            focusedRowId = null
        }
    }

    private var lastFocusedItemId: String = ""

    private fun closeOpenFocus() {
        val rowId = focusedRowId ?: return
        val dwell = (now() - focusStartedAt).coerceAtLeast(0)
        focusedRowId = null
        // Sub-second glances are D-pad travel, not attention. Dropping them keeps the
        // numbers meaningful and the batches small.
        if (dwell < MIN_DWELL_MS) return
        add(
            GatewayRowEvent(
                rowId = rowId,
                rowKind = focusedRowKind,
                event = EVENT_FOCUS,
                itemId = lastFocusedItemId,
                dwellMs = dwell,
                occurredAt = timestamp(),
            ),
        )
    }

    /** Caller already holds the lock. */
    private fun add(event: GatewayRowEvent) {
        // Drop oldest rather than grow without bound: if flushes are failing, recent
        // engagement is the more useful half to keep.
        if (buffer.size >= maxBuffered) buffer.removeAt(0)
        buffer.add(event)
    }

    private fun timestamp(): String = iso8601.get()!!.format(Date(now()))

    companion object {
        const val EVENT_IMPRESSION = "impression"
        const val EVENT_FOCUS = "focus"
        const val EVENT_SELECT = "select"

        /** Below this, a row was passed through rather than looked at. */
        const val MIN_DWELL_MS = 400L

        // SimpleDateFormat is not thread-safe and minSdk 23 rules out java.time.
        private val iso8601 = object : ThreadLocal<SimpleDateFormat>() {
            override fun initialValue(): SimpleDateFormat =
                SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).apply {
                    timeZone = TimeZone.getTimeZone("UTC")
                }
        }
    }
}
