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

// Category display order: people first, general last.
// 'artist' (FC-2d-vii-c) and 'copyright' (2026-06-01) retired — only
// character and general surface as suggestion categories now.
export const CATEGORY_ORDER = ['character', 'general']
export const CATEGORY_LABELS = {
  character: 'Character',
  general: 'General'
}

export const useSuggestionsStore = defineStore('suggestions', () => {
  const api = useApi()
  const byCategory = ref({})       // { category: [suggestion, ...] } — panel (≥ threshold)
  // The typed tag-input dropdown searches the model's FULL prediction set for
  // the image (min=0, down to the store floor) so low-confidence actions/
  // features can be picked in canonical formatting (operator-asked 2026-06-09).
  const allByCategory = ref({})
  const { loading, error, run } = useAsyncAction({ errorAs: 'message' })
  let currentImageId = null
  const inflightAll = useInflightToken()
  // Audit 2026-06-02: this store had no inflight guard — a late
  // /suggestions response from a prior image could overwrite
  // byCategory while currentImageId pointed at a new one, and
  // accept() dereferenced currentImageId AFTER an awaited POST so
  // the subsequent /suggestions/accept could apply A's chosen tag
  // to image B (and push it to the allowlist). Both fixed below
  // by capturing imageId at call-time and gating writes on the token.
  const inflight = useInflightToken()

  async function load(imageId) {
    // Cancel any in-flight load from the previous image so its late
    // response can't overwrite this image's byCategory.
    inflight.cancel()
    currentImageId = imageId
    byCategory.value = {}  // cleared upfront so it stays empty on error
    const t = inflight.claim()
    await run(async () => {
      const body = await api.get(`/api/images/${imageId}/suggestions`)
      if (!t.isCurrent()) return
      byCategory.value = body.by_category || {}
    })
  }

  // Load the full prediction set for the dropdown (separate inflight guard so a
  // late response from a prior image can't overwrite the current one's list).
  async function loadAll(imageId) {
    inflightAll.cancel()
    allByCategory.value = {}
    const t = inflightAll.claim()
    try {
      const body = await api.get(`/api/images/${imageId}/suggestions`, {
        params: { min: 0 },
      })
      if (!t.isCurrent()) return
      allByCategory.value = body.by_category || {}
    } catch {
      // Dropdown is best-effort — the panel surfaces load errors.
    }
  }

  // A stable identity for a suggestion across the two lists (panel vs dropdown
  // are separate fetches, so object identity differs): tag id when known, else
  // the raw display key.
  function _keyOf(s) {
    return s.canonical_tag_id != null
      ? `id:${s.canonical_tag_id}`
      : `raw:${s.category}:${(s.display_name || '').toLowerCase()}`
  }

  // Drop an accepted/dismissed suggestion from BOTH the panel list and the
  // dropdown's full list so it can't reappear in either surface.
  function _dropEverywhere(suggestion) {
    const key = _keyOf(suggestion)
    const cat = suggestion.category
    for (const map of [byCategory.value, allByCategory.value]) {
      const list = map[cat]
      if (list) map[cat] = list.filter(s => _keyOf(s) !== key)
    }
  }

  async function accept(suggestion) {
    // Capture imageId so a mid-flight prev/next can't reroute the
    // accept POST to a different image AND push the tag to that
    // image's allowlist.
    const imageId = currentImageId
    if (imageId == null) return
    // Raw tags (creates_new_tag) have no canonical_tag_id; the backend's
    // accept endpoint needs a tag_id, so for raw tags we create the tag
    // first via the existing /api/tags endpoint, then accept by id.
    let tagId = suggestion.canonical_tag_id
    if (tagId == null) {
      const created = await api.post('/api/tags', {
        body: { name: suggestion.display_name, kind: suggestion.category }
      })
      tagId = created.id
    }
    await api.post(`/api/images/${imageId}/suggestions/accept`, {
      body: { tag_id: tagId }
    })
    // Only drop from THIS image's category list — if the user navigated,
    // the new image has its own suggestions and this drop would corrupt them.
    if (currentImageId === imageId) {
      _dropEverywhere(suggestion)
    }
    toast({
      text: `Tagged: ${suggestion.display_name}`,
      type: 'success'
    })
  }

  async function aliasAccept(suggestion, canonicalTagId) {
    const imageId = currentImageId
    if (imageId == null) return
    // The alias MUST be stored under the raw model key — resolution looks up the
    // raw prediction key, not the normalized display name. Sending display_name
    // (the old bug) stored an alias that never resolved, so the prediction kept
    // reappearing unaliased. raw_name is null only for centroid hits, which
    // can't be aliased (the UI hides the action for them).
    const aliasString = suggestion.raw_name ?? suggestion.display_name
    await api.post(`/api/images/${imageId}/suggestions/alias`, {
      body: {
        alias_string: aliasString,
        alias_category: suggestion.category,
        canonical_tag_id: canonicalTagId
      }
    })
    if (currentImageId === imageId) {
      _dropEverywhere(suggestion)
    }
    toast({
      text: `Aliased & tagged: ${suggestion.display_name}`,
      type: 'success'
    })
  }

  // Remove the alias behind an aliased suggestion (the raw prediction reverts to
  // its unaliased form on reload). The canonical tag stays applied if it was
  // accepted — this only undoes the model-key→tag mapping.
  async function removeAlias(suggestion) {
    const imageId = currentImageId
    if (imageId == null || suggestion.raw_name == null) return
    await api.delete(
      `/api/aliases/${encodeURIComponent(suggestion.raw_name)}/${encodeURIComponent(suggestion.category)}`
    )
    if (currentImageId === imageId) {
      await load(imageId)
      await loadAll(imageId)
    }
    toast({ text: `Alias removed: ${suggestion.display_name}`, type: 'success' })
  }

  async function dismiss(suggestion) {
    const imageId = currentImageId
    if (imageId == null) return
    // Dismiss needs a tag_id; raw tags have none, so dismissing a raw
    // suggestion just hides it client-side (nothing to persist a rejection
    // against until the tag exists).
    if (suggestion.canonical_tag_id != null) {
      await api.post(`/api/images/${imageId}/suggestions/dismiss`, {
        body: { tag_id: suggestion.canonical_tag_id }
      })
    }
    if (currentImageId === imageId) {
      _dropEverywhere(suggestion)
    }
  }

  return {
    byCategory, allByCategory, loading, error,
    load, loadAll, accept, aliasAccept, removeAlias, dismiss
  }
})
