<template>
  <div class="fc-filterbar-wrap fc-chrome-continues">
   <div class="fc-filterbar">
    <v-autocomplete
      v-model="selected"
      :items="searchItems"
      :loading="searchLoading"
      item-title="name" item-value="value"
      no-filter clearable hide-details density="compact" variant="outlined"
      placeholder="Filter by tag or artist…"
      prepend-inner-icon="mdi-filter-variant"
      class="fc-filterbar__search"
      @update:search="onSearch"
      @update:model-value="onPick"
    >
      <template #item="{ props: itemProps, internalItem }">
        <v-list-item v-bind="itemProps" :title="internalItem.raw.name">
          <template #prepend>
            <v-icon size="small">{{ iconFor(internalItem.raw) }}</v-icon>
          </template>
          <template #subtitle>
            {{ internalItem.raw.kind === 'artist' ? 'artist'
              : (internalItem.raw.fandom_name ? `character · ${internalItem.raw.fandom_name}` : internalItem.raw.kind) }}
          </template>
        </v-list-item>
      </template>
    </v-autocomplete>

    <div class="fc-filterbar__chips">
      <!-- Include tag chips: click the body to flip to exclude, ✕ to remove
           (#6 light editor — one model, two editors). -->
      <v-chip
        v-for="id in store.filter.tag_ids" :key="`t${id}`"
        size="small" closable :color="chipColor(id)" variant="tonal"
        class="fc-filterbar__tagchip"
        title="Click to exclude this tag instead"
        @click="toggleTagPolarity(id)"
        @click:close="removeTag(id)"
      >{{ store.tagLabels[id] || `#${id}` }}</v-chip>
      <!-- Exclude tag chips (red, minus). Click body to flip back to include. -->
      <v-chip
        v-for="id in store.filter.tag_exclude" :key="`x${id}`"
        size="small" closable color="error" variant="tonal"
        class="fc-filterbar__tagchip"
        title="Excluded — click to include instead"
        @click="toggleTagPolarity(id)"
        @click:close="removeExclude(id)"
      ><v-icon start size="x-small">mdi-minus</v-icon>{{ store.tagLabels[id] || `#${id}` }}</v-chip>
      <!-- Advanced OR-groups can't render as flat chips; one affordance opens
           the builder where they live. -->
      <v-chip
        v-if="orGroupCount"
        size="small" color="accent" variant="tonal"
        prepend-icon="mdi-filter-cog-outline"
        title="Open the advanced tag filter"
        @click="advancedOpen = true"
      >{{ orGroupCount }} OR-group{{ orGroupCount > 1 ? 's' : '' }}</v-chip>
      <v-chip
        v-if="store.filter.artist_id"
        size="small" closable color="accent" variant="tonal"
        prepend-icon="mdi-account"
        @click:close="clearArtist"
      >{{ store.artistLabel || `Artist #${store.filter.artist_id}` }}</v-chip>
      <v-chip
        v-if="store.filter.similar_to"
        size="small" closable color="accent" variant="tonal"
        prepend-icon="mdi-image-multiple"
        @click:close="clearSimilar"
      >Similar to #{{ store.filter.similar_to }}</v-chip>
    </div>

    <v-spacer />

    <v-btn-toggle
      :model-value="store.filter.media_type ?? 'all'"
      density="compact" mandatory variant="outlined" divided
      @update:model-value="(v) => setMedia(v === 'all' ? null : v)"
    >
      <v-btn value="all" size="small">All</v-btn>
      <v-btn value="image" size="small">Images</v-btn>
      <v-btn value="video" size="small">Videos</v-btn>
    </v-btn-toggle>

    <!-- Sort is meaningless in similar-mode (results are distance-ranked). -->
    <v-select
      v-if="!store.filter.similar_to"
      :model-value="store.filter.sort"
      :items="SORTS"
      density="compact" hide-details variant="outlined"
      class="fc-filterbar__sort"
      @update:model-value="setSort"
    />

    <v-btn
      :color="advancedActive ? 'accent' : undefined"
      :variant="advancedActive ? 'tonal' : 'text'"
      size="small" prepend-icon="mdi-filter-cog-outline"
      @click="advancedOpen = true"
    >Advanced{{ advancedCount ? ` (${advancedCount})` : '' }}</v-btn>

    <v-btn
      :color="refineOpen ? 'accent' : undefined"
      :variant="refineOpen || hasRefineFilters ? 'tonal' : 'text'"
      size="small"
      :append-icon="refineOpen ? 'mdi-chevron-up' : 'mdi-chevron-down'"
      @click="toggleRefine"
    >Refine{{ refineCount ? ` (${refineCount})` : '' }}</v-btn>

    <v-btn
      v-if="hasActiveFilters" variant="text" size="small"
      prepend-icon="mdi-close" @click="clearAll"
    >Clear</v-btn>
   </div>

   <GalleryFacetPanel v-if="refineOpen" />

   <v-dialog v-model="advancedOpen" max-width="640" scrollable>
     <TagQueryBuilder
       v-if="advancedOpen"
       :include="store.filter.tag_ids"
       :or-groups="store.filter.tag_or"
       :exclude="store.filter.tag_exclude"
       :labels="store.tagLabels"
       @apply="onAdvancedApply"
       @close="advancedOpen = false"
     />
   </v-dialog>
  </div>
</template>

<script setup>
import { computed, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { useApi } from '../../composables/useApi.js'
import { cloneFilter, filterToQuery, useGalleryStore } from '../../stores/gallery.js'
import { useTagStore } from '../../stores/tags.js'
import GalleryFacetPanel from './GalleryFacetPanel.vue'
import TagQueryBuilder from './TagQueryBuilder.vue'

const store = useGalleryStore()
const tagStore = useTagStore()
const api = useApi()
const router = useRouter()

// posted_* sort by earliest publish across ALL of an image's posts (original
// post date); newest/oldest sort by the primary post's date, else download date.
const SORTS = [
  { title: 'Newest post date', value: 'posted_new' },
  { title: 'Oldest post date', value: 'posted_old' },
  { title: 'Newest added', value: 'newest' },
  { title: 'Oldest added', value: 'oldest' },
]

const selected = ref(null)
const searchItems = ref([])
const searchLoading = ref(false)
let debounce = null

// The faceted-refine sub-filters (platform / curation flags / date range).
const refineCount = computed(() => {
  const f = store.filter
  return (f.platform ? 1 : 0) + (f.untagged ? 1 : 0) + (f.no_artist ? 1 : 0)
    + (f.include_hidden ? 1 : 0) + (f.date_from ? 1 : 0) + (f.date_to ? 1 : 0)
})
const hasRefineFilters = computed(() => refineCount.value > 0)

// The structured tag filter beyond plain includes (OR-groups + excludes) —
// drives the Advanced button's active state + its count badge.
const advancedOpen = ref(false)
const orGroupCount = computed(() => store.filter.tag_or.length)
const advancedCount = computed(
  () => store.filter.tag_or.length + store.filter.tag_exclude.length,
)
const advancedActive = computed(() => advancedOpen.value || advancedCount.value > 0)

const hasActiveFilters = computed(() =>
  store.filter.tag_ids.length > 0 ||
  store.filter.tag_exclude.length > 0 ||
  store.filter.tag_or.length > 0 ||
  store.filter.artist_id != null ||
  store.filter.media_type != null ||
  store.filter.sort !== 'posted_new' ||
  store.filter.similar_to != null ||
  hasRefineFilters.value
)

// Auto-open the panel when refine filters are present in the URL (deep-link /
// back button). The parent applies the query in its onMounted — after this
// child has set up — so watch for the transition rather than reading the
// initial (still-default) filter state.
const refineOpen = ref(false)
watch(hasRefineFilters, (v) => { if (v) refineOpen.value = true })
function toggleRefine() {
  // The panel fetches facets itself on mount (and refetches on filter change);
  // opening it is enough.
  refineOpen.value = !refineOpen.value
}

function iconFor(raw) {
  if (raw.kind === 'artist') return 'mdi-account'
  return { character: 'mdi-account-circle', fandom: 'mdi-book-open-page-variant',
           series: 'mdi-bookshelf' }[raw.kind] || 'mdi-tag'
}
function chipColor(id) {
  // Tag chips use the same per-kind palette as the rest of the app; we only
  // know the kind from the autocomplete pick, so fall back to a neutral tone.
  return tagStore.colorFor(pickedKind.value[id] || 'general')
}
const pickedKind = ref({})

function onSearch(q) {
  if (debounce) clearTimeout(debounce)
  if (!q || !q.trim()) { searchItems.value = []; return }
  debounce = setTimeout(async () => {
    searchLoading.value = true
    try {
      const [tags, artists] = await Promise.all([
        api.get('/api/tags/autocomplete', { params: { q, limit: 10 } }),
        api.get('/api/artists/autocomplete', { params: { q, limit: 10 } }),
      ])
      searchItems.value = [
        ...(artists || []).map((a) => ({
          kind: 'artist', id: a.id, name: a.name, value: `artist:${a.id}`,
        })),
        ...(tags || []).map((t) => ({
          kind: t.kind, id: t.id, name: t.name, value: `tag:${t.id}`,
          fandom_name: t.fandom_name,
        })),
      ]
    } catch {
      searchItems.value = []
    } finally {
      searchLoading.value = false
    }
  }, 250)
}

function onPick(value) {
  const item = searchItems.value.find((i) => i.value === value)
  selected.value = null
  searchItems.value = []
  if (!item) return
  if (item.kind === 'artist') {
    store.noteArtistLabel(item.name)
    pushFilter((n) => { n.artist_id = item.id })
  } else {
    store.noteTagLabel(item.id, item.name)
    pickedKind.value = { ...pickedKind.value, [item.id]: item.kind }
    pushFilter((n) => { if (!n.tag_ids.includes(item.id)) n.tag_ids.push(item.id) })
  }
}

function removeTag(id) {
  pushFilter((n) => { n.tag_ids = n.tag_ids.filter((t) => t !== id) })
}
function removeExclude(id) {
  pushFilter((n) => { n.tag_exclude = n.tag_exclude.filter((t) => t !== id) })
}
// Flip a tag between include and exclude in place (light-editor toggle). A tag
// is only ever in one of the two lists.
function toggleTagPolarity(id) {
  pushFilter((n) => {
    if (n.tag_ids.includes(id)) {
      n.tag_ids = n.tag_ids.filter((t) => t !== id)
      if (!n.tag_exclude.includes(id)) n.tag_exclude.push(id)
    } else {
      n.tag_exclude = n.tag_exclude.filter((t) => t !== id)
      if (!n.tag_ids.includes(id)) n.tag_ids.push(id)
    }
  })
}
// The advanced builder hands back the whole tag model at once.
function onAdvancedApply({ tag_ids, tag_or, tag_exclude, labels }) {
  for (const [id, name] of Object.entries(labels || {})) {
    store.noteTagLabel(Number(id), name)
  }
  pushFilter((n) => {
    n.tag_ids = tag_ids
    n.tag_or = tag_or
    n.tag_exclude = tag_exclude
  })
  advancedOpen.value = false
}
function clearArtist() {
  store.noteArtistLabel(null)
  pushFilter((n) => { n.artist_id = null })
}
function clearSimilar() { pushFilter((n) => { n.similar_to = null }) }
function setMedia(m) { pushFilter((n) => { n.media_type = m }) }
function setSort(s) { pushFilter((n) => { n.sort = s }) }
function clearAll() { router.push({ name: 'gallery', query: {} }) }

// Single write path: clone the current filter, mutate, serialize to the URL.
// The route watcher in GalleryView applies it to the store and reloads.
// cloneFilter/filterToQuery are shared with GalleryFacetPanel so the refine
// sub-filters survive a bar push and vice versa.
function pushFilter(mutate) {
  const n = cloneFilter(store.filter)
  mutate(n)
  router.push({ name: 'gallery', query: filterToQuery(n) })
}
</script>

<style scoped>
/* The whole chrome (bar row + expandable refine panel) is one sticky,
   frosted block pinned directly under the 64px TopNav and continuous with it. */
.fc-filterbar-wrap {
  position: sticky;
  top: var(--fc-nav-h, 64px); /* pins at the nav's real measured bottom (#1481) */
  z-index: 5;
  /* Attach to the TopNav: cancel the v-container's top padding (pt-2 = 8px)
     so the bar sits flush at 64px even at scroll 0 — without this it detaches
     and a gap shows through when scrolled to the top. */
  margin-top: -8px;
  margin-bottom: 12px;
  /* The frost itself (obsidian fade + blur) is the shared .fc-chrome-continues
     primitive: it CONTINUES the TopNav's fade from the seam alpha to transparent
     rather than re-darkening, so the nav + bar read as one gradient (operator
     2026-07-13). This block only owns the sticky positioning now. */
}
.fc-filterbar {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  padding: 10px 12px;
}
/* The inputs/toggles float on the haze: still translucent, but a touch more
   opaque than the bar/nav so the controls stay legible. */
.fc-filterbar-wrap :deep(.v-field),
.fc-filterbar-wrap :deep(.v-btn-group) {
  background-color: rgba(20, 23, 26, 0.72);
}
/* Media toggle (All / Images / Videos) as ONE cohesive segmented control.
   FC's global VBtn { rounded: 'pill' } default made Vuetify 4 pill-round each
   SEGMENT individually, so the rounded ends collided at the joins — the shapes
   landed awkwardly on the button edges (operator 2026-07-13). Square the inner
   segments (over the pill utility's !important) and clip the group to a single
   8px outline (matches the chips/tiles rounding elsewhere in the app). Radius
   only — no height change, so the bar height and nav offset are untouched. */
.fc-filterbar-wrap :deep(.v-btn-toggle) {
  border-radius: 8px;
  overflow: hidden;
}
.fc-filterbar-wrap :deep(.v-btn-toggle .v-btn) {
  border-radius: 0 !important;
}
.fc-filterbar__search { max-width: 320px; min-width: 200px; }
.fc-filterbar__chips { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
/* The tag chips' bodies toggle include/exclude — signal they're clickable. */
.fc-filterbar__tagchip { cursor: pointer; }
.fc-filterbar__tagchip:focus-visible {
  outline: 2px solid rgb(var(--v-theme-accent));
  outline-offset: 1px;
}
.fc-filterbar__sort { max-width: 150px; }

/* Phones: the search's 200px min-width jams the wrapping bar. Give search its
   own full-width row and let sort grow; everything else wraps under it. */
@media (max-width: 600px) {
  .fc-filterbar { gap: 8px; }
  .fc-filterbar__search { min-width: 100%; max-width: none; }
  .fc-filterbar__sort { max-width: none; flex: 1 1 auto; }
}
</style>
