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

export const useModalStore = defineStore('modal', () => {
  const api = useApi()

  const currentImageId = ref(null)
  const current = ref(null)
  const { loading, error, run } = useAsyncAction({ errorAs: 'message' })
  // Tag mutations interpolate the image id into the URL after an
  // await; without an inflight token, a fast prev/next can route the
  // DELETE/POST to the wrong image AND the response to the wrong
  // chip rail. Audit 2026-06-02.
  const inflight = useInflightToken()

  // Scoped playlist. When set, prev/next cycles within THIS ordered id array —
  // the current gallery filter (GalleryView) or a post's images (PostCard) — so
  // the modal walks exactly what the user was looking at, not a global order.
  // When null, prev/next falls back to current.value.neighbors (the
  // /api/gallery/image/<id> global neighbours).
  const playlistIds = ref(null)
  const playlistIndex = ref(0)

  async function open (id, opts = {}) {
    // Cancel any in-flight tag mutation or reloadTags from the
    // previous image so its late response can't apply to this one.
    inflight.cancel()
    currentImageId.value = id
    current.value = null  // cleared upfront so it stays null on error
    // Update post-scoped state if caller passed it; otherwise clear so
    // the next open() from gallery context uses neighbors mode.
    if (opts.playlistIds != null) {
      playlistIds.value = opts.playlistIds
      playlistIndex.value = opts.playlistIds.indexOf(id)
      if (playlistIndex.value < 0) playlistIndex.value = 0
    } else if (opts.clearPlaylist !== false && playlistIds.value != null) {
      playlistIds.value = null
      playlistIndex.value = 0
    }
    const t = inflight.claim()
    await run(async () => {
      const body = await api.get(`/api/gallery/image/${id}`)
      if (!t.isCurrent()) return
      current.value = body
    })
  }

  async function close () {
    inflight.cancel()
    currentImageId.value = null
    current.value = null
    error.value = null
    playlistIds.value = null
    playlistIndex.value = 0
  }

  async function goPrev () {
    if (playlistIds.value != null) {
      if (playlistIndex.value > 0) {
        const newIdx = playlistIndex.value - 1
        const newId = playlistIds.value[newIdx]
        playlistIndex.value = newIdx
        await open(newId, { playlistIds: playlistIds.value })
      }
      return
    }
    if (current.value && current.value.neighbors?.prev_id) {
      await open(current.value.neighbors.prev_id)
    }
  }

  async function goNext () {
    if (playlistIds.value != null) {
      if (playlistIndex.value < playlistIds.value.length - 1) {
        const newIdx = playlistIndex.value + 1
        const newId = playlistIds.value[newIdx]
        playlistIndex.value = newIdx
        await open(newId, { playlistIds: playlistIds.value })
      }
      return
    }
    if (current.value && current.value.neighbors?.next_id) {
      await open(current.value.neighbors.next_id)
    }
  }

  async function reloadTags () {
    // Capture image id at call-time. After an await, currentImageId
    // may have advanced via prev/next navigation; without capture, the
    // GET would target the new image and write its tags onto a chip
    // rail the user didn't open. Audit 2026-06-02.
    const imageId = currentImageId.value
    if (!imageId) return
    const t = inflight.claim()
    const tags = await api.get(`/api/images/${imageId}/tags`)
    if (!t.isCurrent()) return
    // Only commit if the modal is still showing this image — guards
    // against close() / nav clearing current between the await and now.
    if (current.value && currentImageId.value === imageId) {
      current.value.tags = tags
    }
  }

  async function removeTag (tagId) {
    const imageId = currentImageId.value
    if (!imageId) return
    const prev = current.value.tags
    // Optimistic UI: drop the chip immediately.
    current.value.tags = current.value.tags.filter(t => t.id !== tagId)
    // Split the two POSTs so a dismiss failure (secondary side-effect)
    // doesn't roll back the successful DELETE — previously the catch
    // unconditionally restored the chip rail even when only the
    // dismiss had failed, so the UI lied until refresh. Audit 2026-06-02.
    try {
      await api.delete(`/api/images/${imageId}/tags/${tagId}`)
    } catch (e) {
      // Real failure: roll back, surface, rethrow.
      if (current.value && currentImageId.value === imageId) {
        current.value.tags = prev
      }
      toast({ text: `Failed to remove tag: ${e.message}`, type: 'error' })
      throw e
    }
    // DELETE landed. The dismiss is fire-and-best-effort — log on
    // failure but DON'T roll back the chip rail; the tag is gone
    // server-side regardless.
    try {
      await api.post(`/api/images/${imageId}/suggestions/dismiss`, {
        body: { tag_id: tagId },
      })
    } catch (e) {
      toast({
        text: `Tag removed, but failed to dismiss suggestion: ${e.message}`,
        type: 'warning',
      })
    }
  }

  async function addExistingTag (tagId) {
    const imageId = currentImageId.value
    if (!imageId) return
    await api.post(`/api/images/${imageId}/tags`, {
      body: { tag_id: tagId, source: 'manual' },
    })
    await reloadTags()
  }

  async function createAndAdd ({ name, kind, fandom_id = null }) {
    // Capture imageId so the post-create reloadTags / addExistingTag
    // flow stays bound to the image the user clicked on, even if
    // they navigated during the /api/tags POST.
    const imageId = currentImageId.value
    if (!imageId) return
    const tag = await api.post('/api/tags', { body: { name, kind, fandom_id } })
    // Audit 2026-06-02: a kind='fandom' created here used to be
    // invisible to FandomPicker until a full page reload — its load
    // gates on fandomCache.length, so a non-empty cache skips the
    // refetch and the new fandom never appears. Push it into the
    // cache directly so the next open sees it.
    if (kind === 'fandom') {
      const { useTagStore } = await import('./tags.js')
      const tagStore = useTagStore()
      tagStore.fandomCache.push({
        id: tag.id, name: tag.name, kind: 'fandom',
        fandom_id: null, fandom_name: null, image_count: 0,
      })
    }
    if (currentImageId.value !== imageId) return  // navigated away
    await addExistingTag(tag.id)
    // Returned so TagPanel can match the created tag against a pending
    // suggestion and record the acceptance (manual add == accept, 2026-07-03).
    return tag
  }

  const isOpen = computed(() => currentImageId.value !== null)
  const canPrev = computed(() => {
    if (playlistIds.value != null) return playlistIndex.value > 0
    return current.value?.neighbors?.prev_id != null
  })
  const canNext = computed(() => {
    if (playlistIds.value != null) {
      return playlistIndex.value < (playlistIds.value.length - 1)
    }
    return current.value?.neighbors?.next_id != null
  })

  return {
    currentImageId, current, loading, error,
    playlistIds, playlistIndex,
    isOpen, canPrev, canNext,
    open, close, goPrev, goNext,
    reloadTags, removeTag, addExistingTag, createAndAdd,
  }
})
