import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { useApi } from '../composables/useApi.js'
import { preloadImage } from '../utils/preloadImage.js'

// Buffered producer/consumer so the cascade cadence is decoupled from fetch
// latency (operator-flagged 2026-06-04). The OLD pipeline trickled each batch
// right after its fetch and bet the next round-trip would finish inside the
// ~240ms trickle window; when a fetch ran long (TABLESAMPLE hits random,
// sometimes-cold blocks; RTT jitter) the animation starved and the view
// "burped" out a clump of images. Now:
//   - PRODUCER (_fill): races ahead fetching batches into `queue` up to a
//     target depth, refilling whenever the queue dips below BUFFER_MIN. It
//     also kicks off the image PRELOAD for each queued item so decoding
//     pipelines ahead of the reveal.
//   - CONSUMER (_drain): pops ONE item, WAITS for its thumbnail to be fully
//     decoded, then reveals it — at most one per CADENCE_MS. Because the
//     producer preloads ahead, the decode-wait is usually already satisfied,
//     so the reveal stays evenly paced without idling the network.
// The decode-gate is what makes the showcase's signature cascade land each
// tile fully-loaded (the flip-in animates a real image, never a gray
// placeholder); the single-item reveal keeps it strictly one-at-a-time
// (operator-flagged 2026-06-04). The very first image still waits on the first
// fetch + decode (a cold TABLESAMPLE is a separate, query-side concern);
// everything after it is buffer-smoothed.
const PAGE = 3
const CADENCE_MS = 160         // floor between fully-loaded reveals (doubled
                              // 2026-06-04 — slower, more deliberate cadence)
const PRIME = 6                // items buffered before the drain starts
const BUFFER_TARGET = 30       // producer tops the queue up to this
const BUFFER_MIN = 12          // ...and refills once the queue dips below this
const INITIAL_COUNT = 60       // cascade length on load / shuffle
const SCROLL_COUNT = 15        // cascade length per infinite-scroll demand
// Showcase is endless by design (random sample); an unlucky all-duplicate
// batch must be retried — only a genuinely empty API response is exhaustion.
const FETCH_RETRY_CAP = 8


function _sleep(ms) { return new Promise((r) => setTimeout(r, ms)) }


export const useShowcaseStore = defineStore('showcase', () => {
  const api = useApi()
  const images = ref([])
  const loading = ref(false)
  const error = ref(null)
  const exhausted = ref(false)
  const seen = new Set()

  // Internal buffer (not reactive — the consumer is what feeds the UI via
  // images.value).
  let queue = []
  // id -> Promise that settles when the thumbnail is paint-ready. Started by
  // the producer so decoding runs ahead of the reveal; awaited by the consumer
  // so no tile is shown before its image is loaded.
  let _preloads = new Map()
  // Sequence token: shuffle / re-mount bumps it so in-flight producers and
  // the drain bail instead of interleaving two runs into one images list.
  let _seq = 0
  let _filling = false
  let _draining = false

  function _preload(item) {
    if (!_preloads.has(item.id)) _preloads.set(item.id, preloadImage(item.thumbnail_url))
    return _preloads.get(item.id)
  }

  // One batch, retried while the random sample comes back all-duplicates.
  // Returns the fresh items, or null when the API is genuinely empty.
  async function _fetchFresh(mySeq) {
    for (let attempt = 0; attempt < FETCH_RETRY_CAP; attempt++) {
      if (mySeq !== _seq) return []
      const body = await api
        .get('/api/showcase', { params: { limit: PAGE } })
        .catch((e) => {
          error.value = error.value || (e.message || String(e))
          return null
        })
      if (mySeq !== _seq) return []
      const items = (body && body.images) || []
      if (items.length === 0) return null
      const fresh = items.filter((i) => !seen.has(i.id))
      fresh.forEach((f) => seen.add(f.id))
      if (fresh.length) return fresh
    }
    return null  // retry cap hit → tiny library, treat as exhausted
  }

  // Producer: top the buffer up to `target`. Single-flight.
  async function _fill(mySeq, target) {
    if (_filling) return
    _filling = true
    try {
      while (mySeq === _seq && !exhausted.value && queue.length < target) {
        const batch = await _fetchFresh(mySeq)
        if (mySeq !== _seq) return
        if (batch === null) { exhausted.value = true; return }
        queue.push(...batch)
        batch.forEach(_preload)  // pipeline decoding ahead of the reveal
      }
    } finally {
      _filling = false
    }
  }

  // Consumer: show `count` items at a fixed cadence, topping up the buffer as
  // it drains. Single-flight so the initial cascade and scroll appends can't
  // interleave.
  async function _drain(mySeq, count) {
    if (_draining) return
    _draining = true
    loading.value = true
    try {
      for (let shown = 0; shown < count && mySeq === _seq; shown++) {
        if (!exhausted.value && queue.length < BUFFER_MIN) {
          _fill(mySeq, BUFFER_TARGET)  // topup, no await
        }
        let guard = 0
        while (queue.length === 0 && !exhausted.value && mySeq === _seq) {
          await _sleep(20)
          if (++guard > 500) break  // 10s starvation safety net
        }
        if (mySeq !== _seq) return
        if (queue.length === 0) return  // exhausted + empty
        const item = queue.shift()
        await _preload(item)  // reveal only once the thumbnail is fully decoded
        if (mySeq !== _seq) return
        images.value.push(item)
        await _sleep(CADENCE_MS)
      }
    } finally {
      if (mySeq === _seq) {
        _draining = false
        loading.value = false
      }
    }
  }

  async function loadInitial() {
    _seq += 1
    const mySeq = _seq
    images.value = []
    queue = []
    _preloads = new Map()
    seen.clear()
    exhausted.value = false
    error.value = null
    _filling = false
    _draining = false
    loading.value = true
    try {
      // Prime a small buffer before the cascade starts so it doesn't starve
      // at the front; the drain's own topup grows it to BUFFER_TARGET.
      await _fill(mySeq, PRIME)
      if (mySeq !== _seq) return
      if (queue.length === 0 && exhausted.value) return  // empty library
      await _drain(mySeq, INITIAL_COUNT)
    } finally {
      if (mySeq === _seq) loading.value = false
    }
  }

  async function fetchPage() {
    // Infinite-scroll demand — append another cascade of SCROLL_COUNT.
    if (_draining || exhausted.value) return
    await _drain(_seq, SCROLL_COUNT)
  }

  async function shuffle() {
    await loadInitial()
  }

  const hasMore = computed(() => !exhausted.value)
  const isEmpty = computed(
    () => !loading.value && images.value.length === 0 && error.value === null
  )

  return { images, loading, error, hasMore, isEmpty, fetchPage, shuffle, loadInitial }
})
