import { defineStore } from 'pinia'
import { ref } from 'vue'
import { useApi } from '../composables/useApi.js'
import { useAsyncAction } from '../composables/useAsyncAction.js'
import { useInflightToken } from '../composables/useInflightToken.js'
import { useSourcesStore } from './sources.js'

export const useDownloadsStore = defineStore('downloads', () => {
  const api = useApi()

  const events = ref([])
  const cursor = ref(null)
  const hasMore = ref(true)
  const filter = ref({
    status: null, source_id: null, artist_id: null,
    from_date: null, to_date: null,
  })
  const selected = ref(null)
  const { loading, error, run } = useAsyncAction()
  const stats = ref({ pending: 0, running: 0, ok: 0, error: 0, skipped: 0 })
  const activity = ref({ hours: 24, buckets: [] })
  const failing = ref([])
  // Running + queued events, fetched independent of the feed's filter so
  // the "active now" panel always reflects what's happening regardless of
  // how the operator has filtered the historical list below.
  const activeEvents = ref([])
  // Filter changes (applyFilter) and rapid pagination can interleave
  // responses; without an inflight guard the late response from a
  // prior filter overwrites the current view. Audit 2026-06-02.
  const inflight = useInflightToken()

  function _params(extra = {}) {
    const out = { limit: 50, ...extra }
    if (filter.value.status) out.status = filter.value.status
    if (filter.value.source_id != null) out.source_id = filter.value.source_id
    if (filter.value.artist_id != null) out.artist_id = filter.value.artist_id
    return out
  }

  async function loadFirst() {
    const t = inflight.claim()
    await run(async () => {
      const body = await api.get('/api/downloads', { params: _params() })
      if (!t.isCurrent()) return
      events.value = body
      cursor.value = body.length ? body[body.length - 1].id : null
      hasMore.value = body.length === 50
    })
  }

  async function loadMore() {
    if (!hasMore.value || cursor.value == null) return
    const t = inflight.claim()
    await run(async () => {
      const body = await api.get('/api/downloads', { params: _params({ before: cursor.value }) })
      if (!t.isCurrent()) return
      events.value.push(...body)
      cursor.value = body.length ? body[body.length - 1].id : cursor.value
      hasMore.value = body.length === 50
    })
  }

  async function loadOne(id) {
    selected.value = await api.get(`/api/downloads/${id}`)
    return selected.value
  }

  // Open the detail modal for the most recent DownloadEvent of a given
  // source. Used by the failing-sources rollup's "Logs" button so the
  // operator can troubleshoot without leaving the Downloads tab to find
  // the row (operator-flagged 2026-06-01).
  async function loadLastForSource(sourceId) {
    const events = await api.get('/api/downloads', {
      params: { source_id: sourceId, limit: 1 },
    })
    if (!events.length) {
      selected.value = null
      return null
    }
    return await loadOne(events[0].id)
  }

  async function applyFilter(patch) {
    // Drop any in-flight loadFirst/loadMore from the previous filter
    // so its late response doesn't overwrite this filter's results.
    inflight.cancel()
    filter.value = { ...filter.value, ...patch }
    await loadFirst()
  }

  function closeDetail() {
    selected.value = null
  }

  async function loadStats(windowHours = 24) {
    stats.value = await api.get('/api/downloads/stats', { params: { window_hours: windowHours } })
    return stats.value
  }

  async function loadActivity(hours = 24) {
    activity.value = await api.get('/api/downloads/activity', { params: { hours } })
    return activity.value
  }

  async function loadFailing() {
    failing.value = await api.get('/api/sources', { params: { failing: true } })
    return failing.value
  }

  // --- Failing-source retries (shared by the Subscriptions needs-attention
  // card and the Downloads maintenance menu — one implementation for both
  // surfaces). Data-only: callers own the toasts, per this store's style.
  // A single deliberate retry forces past cooldown; BULK retries keep
  // cooldown enforcement ON so N failing sources on one platform don't all
  // retry into the very rate limit the cooldown is preventing.
  async function retrySource(source) {
    const sourcesStore = useSourcesStore()
    try {
      await sourcesStore.checkNow(source.id, { force: true })
      return 'queued'
    } catch (e) {
      if (e?.body?.download_event_id) return 'already_running'
      throw e
    } finally {
      await Promise.all([loadFailing(), loadStats(24)])
    }
  }

  async function retryAllFailing(sources) {
    const sourcesStore = useSourcesStore()
    const tally = { ok: 0, deferred: 0, conflict: 0 }
    try {
      for (const s of sources) {
        try {
          const body = await sourcesStore.checkNow(s.id)
          if (body?.status === 'deferred') tally.deferred += 1
          else tally.ok += 1
        } catch (e) {
          if (e?.body?.download_event_id) tally.conflict += 1
        }
      }
    } finally {
      await Promise.all([loadFailing(), loadStats(24)])
    }
    return tally
  }

  async function loadActive() {
    const [running, pending] = await Promise.all([
      api.get('/api/downloads', { params: { status: 'running', limit: 50 } }),
      api.get('/api/downloads', { params: { status: 'pending', limit: 50 } }),
    ])
    activeEvents.value = [...running, ...pending]
    return activeEvents.value
  }

  // POSTs to the download-recovery sweep endpoint (fire-and-forget — the
  // Beat schedule also runs it every 5 min). The caller should refresh
  // failing/stats a few seconds after this resolves to see swept rows.
  async function recoverStalled() {
    return api.post('/api/downloads/recover-stalled')
  }

  return {
    events, cursor, hasMore, filter, selected, loading, error, stats,
    activity, failing, activeEvents,
    loadFirst, loadMore, loadOne, loadLastForSource, applyFilter,
    closeDetail, loadStats,
    loadActivity, loadFailing, loadActive, recoverStalled,
    retrySource, retryAllFailing,
  }
})
