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

// Explore (#94): anchor on an image, walk its visual neighbours (pgvector
// SigLIP via /api/gallery/similar), keep an in-memory breadcrumb of the walked
// path. The ROUTE (`/explore/:imageId`) is the source of truth for the anchor;
// the view calls anchorOn() on every param change and the store reconciles the
// trail. The store ALSO acts as a TagPanel "host" (current/currentImageId +
// tag CRUD over the anchor) so the Explore workspace reuses the modal's tag
// rail verbatim for modal-parity tagging while rabbit-holing.
const NEIGHBOR_LIMIT = 40   // a wider pool → more variety to browse + jump into

export const useExploreStore = defineStore('explore', () => {
  const api = useApi()

  const anchor = ref(null)       // /api/gallery/image/<id> payload
  const neighbors = ref([])      // [{id, thumbnail_url, ...}]
  const breadcrumb = ref([])     // [{id, thumbnail_url}] walked path
  // Index of the current anchor within breadcrumb — browser-style back/forward.
  // The trail keeps its forward branch when you step back (so ← / → can move
  // through visited items); a NEW walk off a back-step truncates that branch.
  const cursor = ref(-1)
  const loading = ref(false)
  const error = ref(null)
  // Reach (#1476): how far the walk reaches past the anchor's immediate cluster.
  // 0 = nearest (can get stuck in a dense signature); ~0.4 default mixes in
  // mid-far escape routes so the walk diversifies without hitting "Random image".
  const reach = ref(0.4)

  const inflight = useInflightToken()

  async function anchorOn (id) {
    const numId = Number(id)
    if (!Number.isInteger(numId) || numId <= 0) return
    inflight.cancel()
    const t = inflight.claim()
    loading.value = true
    error.value = null
    try {
      const detail = await api.get(`/api/gallery/image/${numId}`)
      if (!t.isCurrent()) return
      anchor.value = detail
      _reconcileTrail(numId, detail.thumbnail_url)
      // Videos / not-yet-embedded images have no neighbours — leave the grid
      // empty and let the view explain why (anchor.has_embedding === false).
      if (detail.has_embedding) {
        const body = await api.get('/api/gallery/similar', {
          // exclude_wip: keep work-in-progress out of the Explore rabbit-hole
          // (the gallery's own "similar" button still shows it) — operator 2026-07-08.
          // reach + exclude_ids (#1476): reach past the dense cluster + never re-serve
          // an already-walked image, so the walk keeps moving instead of getting stuck.
          params: {
            similar_to: numId, limit: NEIGHBOR_LIMIT, exclude_wip: 1,
            reach: reach.value,
            exclude_ids: breadcrumb.value.map((c) => c.id).join(','),
          },
        })
        if (!t.isCurrent()) return
        neighbors.value = body.images || []
      } else {
        neighbors.value = []
      }
    } catch (e) {
      if (t.isCurrent()) { error.value = e.message; neighbors.value = [] }
    } finally {
      if (t.isCurrent()) loading.value = false
    }
  }

  // The route is the source of truth; this reconciles the trail + cursor to it.
  // Revisiting an id already on the path (← / →, a crumb click, or a loop) just
  // MOVES the cursor there — the forward branch is preserved so → can return to
  // it. A genuinely new image off a back-step truncates the stale forward branch
  // (browser semantics), then appends and points the cursor at the new tip.
  function _reconcileTrail (id, thumbnailUrl) {
    const idx = breadcrumb.value.findIndex((c) => c.id === id)
    if (idx >= 0) {
      cursor.value = idx
      return
    }
    const base = cursor.value < breadcrumb.value.length - 1
      ? breadcrumb.value.slice(0, cursor.value + 1)
      : breadcrumb.value
    breadcrumb.value = [...base, { id, thumbnail_url: thumbnailUrl }]
    cursor.value = breadcrumb.value.length - 1
  }

  // ← target: the previous crumb (null at the start of the trail).
  function backTarget () {
    return cursor.value > 0 ? breadcrumb.value[cursor.value - 1].id : null
  }

  // → target: after a ←, walk forward through the already-visited trail
  // (browser-style). Otherwise jump to a varied neighbour to keep the
  // rabbit-hole going — null if neither exists.
  function forwardTarget () {
    if (cursor.value >= 0 && cursor.value < breadcrumb.value.length - 1) {
      return breadcrumb.value[cursor.value + 1].id
    }
    if (!neighbors.value.length) return null
    // Prefer UNVISITED neighbours so → opens something new instead of landing on
    // a crumb (which snaps the cursor back into the trail — the "loops back"
    // report). Fall back to the full set only if every neighbour's been seen.
    const seen = new Set(breadcrumb.value.map((c) => c.id))
    const pool = neighbors.value.filter((n) => !seen.has(n.id))
    const cands = pool.length ? pool : neighbors.value
    // The list is already pHash-deduped + MMR-diversified server-side (it spans
    // clusters, not 40 near-dupes), so a plain random pick gives real variance —
    // no need to skip the nearest slice the way the raw nearest-list required.
    return cands[Math.floor(Math.random() * cands.length)].id
  }

  function reset () {
    inflight.cancel()
    anchor.value = null
    neighbors.value = []
    breadcrumb.value = []
    cursor.value = -1
    error.value = null
    loading.value = false
  }

  // Change how far the walk reaches and re-fetch the current anchor's neighbours
  // with the new setting (the anchor + trail are unchanged — only the grid varies).
  function setReach (v) {
    reach.value = Math.max(0, Math.min(1, Number(v)))
    const id = anchor.value?.id
    if (id != null) anchorOn(id)
  }

  // --- TagPanel "host" surface ---------------------------------------------
  // The anchor IS the current image (same /api/gallery/image/<id> payload the
  // modal uses), so these mirror the modal store's tag-CRUD, targeting the
  // anchor. Kept separate from the modal store so the audited overlay flow is
  // untouched; the id is captured at call-time so a fast walk can't misroute a
  // mutation to the next anchor (same guard as the modal store).
  const current = computed(() => anchor.value)
  const currentImageId = computed(() => anchor.value?.id ?? null)

  async function reloadTags () {
    const id = anchor.value?.id
    if (!id) return
    const tags = await api.get(`/api/images/${id}/tags`)
    if (anchor.value && anchor.value.id === id) anchor.value.tags = tags
  }

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

  async function removeTag (tagId) {
    const id = anchor.value?.id
    if (!id) return
    const prev = anchor.value.tags
    anchor.value.tags = (anchor.value.tags || []).filter((t) => t.id !== tagId)
    try {
      await api.delete(`/api/images/${id}/tags/${tagId}`)
    } catch (e) {
      if (anchor.value && anchor.value.id === id) anchor.value.tags = prev
      toast({ text: `Failed to remove tag: ${e.message}`, type: 'error' })
      throw e
    }
    // The dismiss is best-effort — the tag is gone server-side regardless.
    try {
      await api.post(`/api/images/${id}/suggestions/dismiss`, {
        body: { tag_id: tagId },
      })
    } catch (e) {
      toast({
        text: `Tag removed, but failed to dismiss suggestion: ${e.message}`,
        type: 'warning',
      })
    }
  }

  async function createAndAdd ({ name, kind, fandom_id = null }) {
    const id = anchor.value?.id
    if (!id) return
    const tag = await api.post('/api/tags', { body: { name, kind, fandom_id } })
    if (kind === 'fandom') {
      // Mirror the modal store: a freshly-created fandom must land in the cache
      // so FandomPicker sees it without a reload.
      const { useTagStore } = await import('./tags.js')
      useTagStore().fandomCache.push({
        id: tag.id, name: tag.name, kind: 'fandom',
        fandom_id: null, fandom_name: null, image_count: 0,
      })
    }
    if (anchor.value?.id !== id) return  // walked away mid-create
    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
  }

  // No overlay to dismiss in the Explore workspace — the chip-body "show me
  // more of this tag" navigation just routes away. Present so TagPanel's
  // host.close?.() is a no-op here.
  function close () {}

  return {
    anchor, neighbors, breadcrumb, cursor, loading, error, NEIGHBOR_LIMIT, reach,
    anchorOn, reset, backTarget, forwardTarget, setReach,
    // host surface
    current, currentImageId,
    reloadTags, addExistingTag, removeTag, createAndAdd, close,
  }
})
