<template>
  <div class="flex flex-col max-w-screen md:max-w-full overflow-hidden h-full">
    <!-- ─────────────────── Sticky Header (search + toggles) ─────────────────── -->
    <div
      ref="stickyBar"
      class="z-40 flex flex-col gap-2 relative max-h-full bg-theme-950/70 backdrop-blur-md pb-2 border-b border-theme-900/50"
    >
      <!-- Row 1 : Search bar + Filters / Sort toggles -->
      <div class="flex flex-col md:flex-row gap-2 items-center">
        <!-- Search -->
        <div class="relative flex w-full md:flex-1 h-9 shrink-0">
          <input
            v-model="searchTerm"
            type="text"
            :placeholder="$root.translations.search_characters"
            class="w-full h-full m-0 py-0 pl-10 pr-10 rounded-md bg-theme-950/50 text-sm font-medium text-theme-200 placeholder-theme-500 outline-none border border-theme-900/80 focus:border-theme-800/80  transition-all duration-200 box-border appearance-none"
            @keyup.enter="debouncedSearch"
          />
          <span
            class="absolute left-3 top-1/2 -translate-y-1/2 ph ph-magnifying-glass text-theme-400 text-[15px] pointer-events-none"
          />
          <button
            v-if="searchTerm"
            class="absolute right-1.5 top-1/2 -translate-y-1/2 w-6 h-6 rounded-md hover:bg-theme-800/80 text-theme-500 hover:text-theme-300 transition-colors flex items-center justify-center"
            @click="clearSearch"
          >
            <span class="ph ph-x text-sm" />
          </button>
        </div>
        <div
          class="flex h-9 justify-between md:justify-end items-center relative gap-2 w-full md:w-auto shrink-0"
        >

          <!-- Shuffle button -->
          <button
            v-if="!compact"
            @click="getRandomCharacter"
            :disabled="shuffleLoading"
            class="shuffle-button group shrink-0 relative overflow-hidden rounded-md font-semibold transition-all select-none duration-200 h-9 w-10 md:w-auto flex items-center justify-center border-t-2 border-b-2 border-t-theme-900 border-b-theme-940 shadow-theme-950/50 bg-gradient-to-b from-theme-920 to-theme-940 text-theme-300 active:bg-gradient-to-t active:border-t-theme-950 active:border-b-theme-900 hover:from-violet-600/20 hover:to-fuchsia-600/20 hover:border-t-violet-500/50 hover:border-b-fuchsia-500/50 hover:text-violet-300 px-6"
          >
            <!-- Animated background gradient -->
            <div class="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
              <div class="absolute inset-0 bg-gradient-to-r from-violet-500/10 via-fuchsia-500/10 to-violet-500/10 animate-gradient-x"></div>
            </div>
            <!-- Sparkle particles -->
            <div class="absolute inset-0 overflow-hidden pointer-events-none">
              <div class="sparkle sparkle-1"></div>
              <div class="sparkle sparkle-2"></div>
              <div class="sparkle sparkle-3"></div>
              <div class="sparkle sparkle-4"></div>
            </div>
            <!-- Button content -->
            <div class="relative flex items-center justify-center gap-2 md:gap-2">
              <i
                class="ph ph-shuffle text-lg transition-transform duration-300"
                :class="[
                  shuffleLoading ? 'animate-shuffle-spin' : 'group-hover:animate-shuffle-wiggle'
                ]"
              ></i>
            </div>
            <!-- Glow effect on hover -->
            <div class="absolute inset-0 rounded-md opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none shadow-[inset_0_0_20px_rgba(139,92,246,0.3)]"></div>
          </button>
                    <!-- Filter toggle -->
          <BaseButton
            @click="openFiltersModal"
            type="secondary"
            icon="ph-funnel"
            :class="['h-9', compact ? 'w-9 px-0' : 'flex-1 md:flex-initial']"
          >
            <span
              v-if="activeFilterCount"
              class="text-xs rounded-full bg-rose-500/20 text-rose-300"
            >
              {{ activeFilterCount }}
            </span>
          </BaseButton>
          <!-- Sort selector -->
          <fancy-selector
            v-if="show_sort"
            v-model="sortOption"
            :value="sortOption"
            :options="sortOptions"
            class="min-w-0 md:min-w-72 h-9 flex-1 w-full md:flex-initial"
            @change="updateSort"
          />
        </div>
      </div>
    </div>

    <!-- ─────────────────── Character grid ─────────────────── -->
    <section
      ref="scrollContainer"
      class="relative z-30"
      :class="containerClasses"
      @scroll.passive="handleScroll"
    >
      <!-- Loading indicator -->
      <div
        v-if="loading && !hasRenderableContent"
        class="col-span-full flex flex-col items-center justify-center py-10"
      >
        <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-theme-400"></div>
        <p class="mt-4 text-theme-500">{{ $root.translations.loading_characters }}</p>
      </div>

      <!-- Loading indicator for load more -->
      <div
        v-else-if="loading && hasRenderableContent"
        class="col-span-full flex justify-center py-4"
      >
        <div class="animate-spin rounded-full h-8 w-8 border-b-2 border-theme-400"></div>
      </div>

      <!-- Sectioned layout (new `top` response) -->
      <template v-if="hasSections">
        <div
          v-for="(section, idx) in filteredSections"
          :key="section.id || idx"
          class="browser-section col-span-full shrink-0 rounded-lg"
          :style="sectionTheme(idx, section)"
        >
          <div class="browser-section-header rounded-md mb-2">
            <div class="flex items-start justify-between gap-4">
              <div class="min-w-0">
                <h3 class="browser-section-title">
                  {{ section.title || section.id || 'Section' }}
                </h3>
                <p v-if="section.description" class="browser-section-desc hidden md:block">
                  {{ section.description }}
                </p>
              </div>

              <div class="flex items-center gap-2 flex-shrink-0">
                <span
                  v-if="sectionLoading[section.id]"
                  class="browser-section-pill subtle"
                >
                  <span class="animate-spin rounded-full h-3 w-3 border-b-2 border-theme-300" />
                  {{ $root.translations.loading || 'Loading...' }}
                </span>

                <span class="browser-section-pill">
                  {{ (section.characters && section.characters.length) || 0 }}
                  {{ $root.translations.characters || 'Characters' }}
                </span>


              </div>
            </div>
          </div>

          <div :class="sectionGridClasses" @scroll.passive="handleAnimatedAvatarScrollActivity">
            <!-- Per-section loading state (important for Explore incremental rendering) -->
            <div
              v-if="sectionLoading[section.id] && (!section.characters || !section.characters.length)"
              :class="horizontalScroll ? 'flex-shrink-0 w-full flex justify-center py-10' : 'col-span-full flex justify-center py-10'"
            >
              <div class="animate-spin rounded-full h-8 w-8 border-b-2 border-theme-400"></div>
            </div>

            <component
              v-for="character in section.characters"
              :is="bigview ? 'character-big-card' : 'character-list-item'"
              :key="character.id"
              :character="character"
              :autoplay-animated-avatar="isAnimatedAvatarSelected(character)"
              :class="horizontalScroll ? 'snap-start flex-shrink-0 w-80' : ''"
              @open-character="open_character"
              @open-options="openOptions"
              @send-report="send_report"
            />

            <div
              v-if="!sectionLoading[section.id] && (!section.characters || !section.characters.length)"
              class="col-span-full text-center py-10"
            >
              <span class="ph ph-sparkle text-3xl text-theme-700" />
              <p class="mt-2 text-theme-500">
                {{ $root.translations.no_characters_found || 'No characters found' }}
              </p>
            </div>
          </div>

          <!-- Per-section load more (explore) -->
          <div
            v-if="section.pagination && section.pagination.has_next"
            class="mt-4 flex justify-center"
          >
            <BaseButton
              @click="loadMoreSection(section.id)"
              type="secondary"
              size="small"
              :disabled="!!sectionLoading[section.id]"
              :label="
                sectionLoading[section.id]
                  ? ($root.translations.loading || 'Loading...')
                  : ($root.translations.load_more || 'Load more')
              "
            />
          </div>
        </div>
      </template>

      <!-- Flat list layout (legacy `characters` response) -->
      <template v-else>
        <component
          v-for="character in filteredCharacters"
          :is="bigview ? 'character-big-card' : 'character-list-item'"
          :key="character.id"
          :character="character"
          :autoplay-animated-avatar="isAnimatedAvatarSelected(character)"
          :class="horizontalScroll ? 'snap-start flex-shrink-0 w-80' : ''"
          @open-character="open_character"
          @open-options="openOptions"
          @send-report="send_report"
        />

        <div v-if="showGlobalEmptyState" class="col-span-full text-center py-10">
          <span class="ph ph-ghost text-4xl text-theme-600" />
          <p class="mt-2 text-theme-500">
            {{
              searchTerm || activeFilterCount
                ? $root.translations.no_characters_match_query
                : $root.translations.no_characters_found
            }}
          </p>
        </div>
      </template>
    </section>

    <!-- ─────────────────── Load More button ─────────────────── -->
    <div v-if="showLoadMore" class="text-center py-3">
      <BaseButton
        @click="loadMore"
        type="secondary"
        size="small"
        :label="$root.translations.load_more"
      />
    </div>
  </div>
</template>

<script>
import FancySelector from "./fancy-selector.vue";
import CharacterListItem from "./CharacterListItem.vue";
import CharacterBigCard from "./CharacterBigCard.vue";
import BaseButton from "./BaseButton.vue";
import ToggleSwitch from "./ToggleSwitch.vue";

const ANIMATED_AVATAR_SCROLL_SETTLE_MS = 160;

export default {
  name: "Browser",
  components: {
    FancySelector,
    CharacterListItem,
    CharacterBigCard,
    BaseButton,
    ToggleSwitch,
  },
  props: {
    search: { type: String, default: "" },

    // Optional hard filter that should be applied on the `top` endpoint
    // (used by /tag/:tag pages for SEO-friendly tag landing pages)
    tags: { type: Array, default: () => [] },

    columns: { type: Number, default: 1 },
    itemsPerPage: { type: Number, default: 10 },
    default_sort: { type: String, default: "explore" },
    show_sort: { type: Boolean, default: true },
    bigview: { type: Boolean, default: false },
    horizontalScroll: { type: Boolean, default: false },
    open_message_options: { type: Boolean, default: false },
    compact: { type: Boolean, default: false },
  },
  data() {
    return {
      searchTerm: this.search,
      sortOption: this.default_sort,
      activeFilters: {
        gender: [],
        tags: [],
        imageStyle: [],
        hasImage: false,
        hasExampleDialogs: false,
      },
      sortOptions: [
        { label: this.$root.translations.sort_newest, value: "newest" },
        { label: this.$root.translations.sort_oldest, value: "oldest" },
        {
          label: this.$root.translations.sort_most_popular,
          value: "most_popular",
        },
        { label: "Explore", value: "explore" },
        {
          label: this.$root.translations.sort_most_interesting,
          value: "most_interesting",
        },
      ],

      // Legacy (flat) response support
      top: [],
      filteredCharacters: [],

      // New sectioned response support
      topSections: [],
      filteredSections: [],

      // Explore: per-section pagination state
      sectionPages: {},
      sectionLoading: {},

      // Used to ignore stale in-flight explore requests when the user changes sort/search.
      exploreRequestToken: 0,

      currentPage: 1,
      loading: false,
      allDataLoaded: false,
      debounceTimer: null,
      shuffleLoading: false,
      animatedAvatarIds: new Set(),
      animatedAvatarSelectionFrame: null,
      animatedAvatarResizeHandler: null,
    };
  },
  computed: {
    activeFilterCount() {
      return (
        this.activeFilters.gender.length +
        this.activeFilters.imageStyle.length +
        (this.activeFilters.hasImage ? 1 : 0) +
        (this.activeFilters.hasExampleDialogs ? 1 : 0)
      );
    },

    hasSections() {
      return Array.isArray(this.filteredSections) && this.filteredSections.length > 0;
    },

    isExploreMode() {
      return this.sortOption === "explore" && !this.searchTerm.trim();
    },

    containerClasses() {
      // When rendering sections we always want a vertical scroller with stacked blocks.
      if (this.hasSections) {
        return "flex-1 py-3 gap-6 flex flex-col overflow-y-auto";
      }

      return [
        "flex-1 py-3 gap-3",
        this.horizontalScroll
          ? "flex max-w-full flex-nowrap overflow-x-auto snap-x"
          : "grid overflow-y-auto",
        this.bigview
          ? "grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
          : "grid-cols-1",
      ];
    },

    sectionGridClasses() {
      return [
        "relative z-10 gap-3",
        this.horizontalScroll
          ? "flex max-w-full flex-nowrap overflow-x-auto snap-x"
          : "grid",
        this.bigview
          ? "grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
          : "grid-cols-1",
      ];
    },

    hasRenderableContent() {
      if (this.hasSections) return this.filteredSections.length > 0;
      return this.filteredCharacters.length > 0;
    },

    totalRenderedCharacters() {
      if (this.hasSections) return this.flattenSections(this.filteredSections).length;
      return this.filteredCharacters.length;
    },

    showGlobalEmptyState() {
      // Only show the global empty state for the flat/legacy mode.
      return !this.loading && !this.hasSections && !this.filteredCharacters.length;
    },

    showLoadMore() {
      // In sectioned (explore) mode we show per-section load more buttons.
      if (this.hasSections) return false;

      // Legacy (flat list) load more button.
      return !this.loading && !this.allDataLoaded && this.totalRenderedCharacters > 0;
    },
  },
  watch: {
    searchTerm() {
      this.debouncedSearch();
    },
    search(v) {
      this.searchTerm = v;
    },
    tags: {
      deep: true,
      handler() {
        // If the consumer changes the external tag filter, refresh.
        if (!this.searchTerm || !this.searchTerm.trim()) {
          this.get_top(true);
        }
      },
    },
  },
  mounted() {
    // sortOption is already initialized from default_sort
    this._animatedAvatarLastScrollAt = 0;
    this._animatedAvatarSettleTimer = null;
    this._animatedAvatarDataVersion = 1;
    this._animatedAvatarSyncedVersion = 1;
    this.animatedAvatarResizeHandler = () => this.scheduleAnimatedAvatarSelection();
    window.addEventListener("resize", this.animatedAvatarResizeHandler, { passive: true });
    this._visibleAnimatedAvatarElements = new Set();
    this._observedAnimatedAvatarElements = new Set();
    if (typeof IntersectionObserver !== "undefined") {
      this._animatedAvatarObserver = new IntersectionObserver((entries) => {
        for (const entry of entries) {
          if (entry.isIntersecting) this._visibleAnimatedAvatarElements.add(entry.target);
          else this._visibleAnimatedAvatarElements.delete(entry.target);
        }
        this.scheduleAnimatedAvatarSelection();
      }, { root: this.$refs.scrollContainer, threshold: 0 });
    }
    this.get_top(true);
    this.syncAnimatedAvatarElements();
    this._animatedAvatarSyncedVersion = this._animatedAvatarDataVersion;
    this.scheduleAnimatedAvatarSelection();
  },
  updated() {
    if (this._animatedAvatarDataVersion !== this._animatedAvatarSyncedVersion) {
      this._animatedAvatarSyncedVersion = this._animatedAvatarDataVersion;
      this.syncAnimatedAvatarElements();
    }
    this.scheduleAnimatedAvatarSelection();
  },
  beforeUnmount() {
    if (this.animatedAvatarSelectionFrame !== null) {
      cancelAnimationFrame(this.animatedAvatarSelectionFrame);
      this.animatedAvatarSelectionFrame = null;
    }
    if (this._animatedAvatarSettleTimer) {
      clearTimeout(this._animatedAvatarSettleTimer);
      this._animatedAvatarSettleTimer = null;
    }
    if (this._scrollRaf) {
      cancelAnimationFrame(this._scrollRaf);
      this._scrollRaf = null;
    }
    if (this.animatedAvatarResizeHandler) {
      window.removeEventListener("resize", this.animatedAvatarResizeHandler);
    }
    if (this._animatedAvatarObserver) this._animatedAvatarObserver.disconnect();
    this._visibleAnimatedAvatarElements?.clear();
    this._observedAnimatedAvatarElements?.clear();
  },
  methods: {
    // Explore sections must match backend CharacterSorter::$exploreSections keys.
    // (We intentionally keep this explicit so the UI can request sections individually.)
    exploreSectionKeys() {
      return [
        "structured_imgen",
        "top_characters",
        "experienced_picks",
        "top_male",
        "top_female",
        "rising_stars",
        "known_creators",
        "old_but_gold",
        "most_favorited",
        "trending_tags",
        "hidden_gems",
        "official_spotlight",
        "deep_conversations",
        "most_liked",
        "polarizing",
        "popular_collections",
      ];
    },

    explorePerPage() {
      // Historically explore has been 20 per section.
      // Keep it independent from itemsPerPage (used by non-explore endpoints).
      return 20;
    },

    // ─── section theming ──────────────────────────────────────────
    sectionTheme(index) {
      const themes = [
        { a: "139,92,246", b: "236,72,153" }, // violet → fuchsia
        { a: "34,211,238", b: "59,130,246" }, // cyan → blue
        { a: "52,211,153", b: "16,185,129" }, // emerald → green
        { a: "251,191,36", b: "249,115,22" }, // amber → orange
        { a: "244,63,94", b: "239,68,68" }, // rose → red
        { a: "168,85,247", b: "45,212,191" }, // purple → teal
        { a: "250,204,21", b: "14,165,233" }, // yellow → sky
        { a: "99,102,241", b: "236,72,153" }, // indigo → pink
      ];

      const t = themes[index % themes.length];
      return {
        "--accent-a": `rgba(${t.a}, 0.18)`,
        "--accent-b": `rgba(${t.b}, 0.18)`,
        "--accent-border": `rgba(${t.a}, 0.35)`,
        "--accent-text-a": `rgba(${t.a}, 1)`,
        "--accent-text-b": `rgba(${t.b}, 1)`,
      };
    },

    isAnimatedAvatarSelected(character) {
      return Boolean(
        this.bigview &&
        character &&
        character.animated_avatar &&
        this.animatedAvatarIds.has(String(character.id))
      );
    },

    animatedAvatarLimit() {
      const width = window.innerWidth || 1024;
      if (width < 640) return 2;
      if (width < 1024) return 4;
      return 6;
    },

    handleAnimatedAvatarScrollActivity() {
      this._animatedAvatarLastScrollAt = performance.now();
      this.scheduleAnimatedAvatarSelection();
    },

    scheduleAnimatedAvatarSelection() {
      if (!this.bigview || this.animatedAvatarSelectionFrame !== null) return;
      this.animatedAvatarSelectionFrame = requestAnimationFrame(() => {
        this.animatedAvatarSelectionFrame = null;
        this.updateAnimatedAvatarSelection();
      });
    },

    syncAnimatedAvatarElements() {
      if (!this.bigview || !this.$refs.scrollContainer || !this._animatedAvatarObserver) return;

      const elements = new Set(
        this.$refs.scrollContainer.querySelectorAll('[data-animated-avatar="true"]')
      );
      for (const element of elements) {
        if (this._observedAnimatedAvatarElements.has(element)) continue;
        this._observedAnimatedAvatarElements.add(element);
        this._animatedAvatarObserver.observe(element);
      }
      for (const element of this._observedAnimatedAvatarElements) {
        if (elements.has(element)) continue;
        this._animatedAvatarObserver.unobserve(element);
        this._observedAnimatedAvatarElements.delete(element);
        this._visibleAnimatedAvatarElements.delete(element);
      }
    },

    updateAnimatedAvatarSelection() {
      if (!this.bigview || !this.$refs.scrollContainer) return;

      const container = this.$refs.scrollContainer;
      const containerRect = container.getBoundingClientRect();
      if (!containerRect.width || !containerRect.height) return;
      const centerX = containerRect.left + containerRect.width / 2;
      const centerY = containerRect.top + containerRect.height / 2;
      const candidates = this._animatedAvatarObserver
        ? Array.from(this._visibleAnimatedAvatarElements)
        : Array.from(container.querySelectorAll('[data-animated-avatar="true"]'));
      const ranked = candidates
        .map((element) => {
          const rect = element.getBoundingClientRect();
          // Ignore cards fully outside the viewport before ranking distances.
          if (
            rect.bottom <= containerRect.top ||
            rect.top >= containerRect.bottom ||
            rect.right <= containerRect.left ||
            rect.left >= containerRect.right
          ) return null;
          const dx = rect.left + rect.width / 2 - centerX;
          const dy = rect.top + rect.height / 2 - centerY;
          return { id: element.dataset.characterId, distance: dx * dx + dy * dy };
        })
        .filter(Boolean)
        .filter((candidate) => candidate.id)
        .sort((a, b) => a.distance - b.distance)
        .slice(0, this.animatedAvatarLimit());

      const nextIds = new Set(ranked.map((candidate) => candidate.id));
      const changed = nextIds.size !== this.animatedAvatarIds.size ||
        [...nextIds].some((id) => !this.animatedAvatarIds.has(id));
      if (!changed) return;

      const sinceScroll = performance.now() - this._animatedAvatarLastScrollAt;
      if (sinceScroll < ANIMATED_AVATAR_SCROLL_SETTLE_MS) {
        if (this._animatedAvatarSettleTimer) return;
        this._animatedAvatarSettleTimer = setTimeout(() => {
          this._animatedAvatarSettleTimer = null;
          this.updateAnimatedAvatarSelection();
        }, ANIMATED_AVATAR_SCROLL_SETTLE_MS - sinceScroll + 40);
        return;
      }

      this.animatedAvatarIds = nextIds;
    },

    // ─── shuffle / random character ───────────────────────────────
    async getRandomCharacter() {
      if (this.shuffleLoading) return;
      this.shuffleLoading = true;
      try {
        const res = await this.$root.handleRequest("random_character", {});
        if (res.success && res.data && res.data.data && res.data.data.character) {
          this.open_character(res.data.data.character.id);
        } else if (res.success && res.data && res.data.data && res.data.data.id) {
          this.open_character(res.data.data.id);
        } else {
          // fallback to local random
          const pool = this.hasSections
            ? this.flattenSections(this.filteredSections)
            : this.filteredCharacters;

          if (pool.length > 0) {
            const randomIndex = Math.floor(Math.random() * pool.length);
            const randomChar = pool[randomIndex];
            this.open_character(randomChar.id);
          } else {
            this.$root.showNotification &&
              this.$root.showNotification({
                type: "error",
                message:
                  this.$root.translations.no_characters_found ||
                  "No characters found",
              });
          }
        }
      } catch (error) {
        console.error("Error fetching random character:", error);
        const pool = this.hasSections
          ? this.flattenSections(this.filteredSections)
          : this.filteredCharacters;

        if (pool.length > 0) {
          const randomIndex = Math.floor(Math.random() * pool.length);
          const randomChar = pool[randomIndex];
          this.open_character(randomChar.id);
        }
      } finally {
        this.shuffleLoading = false;
      }
    },

    // ─── replaced filter open with modal ───────────────────────────
    openFiltersModal() {
      this.$root.openModal("SearchFiltersModal", {
        initialFilters: {
          gender: this.activeFilters.gender[0] || "any",
          style: this.activeFilters.imageStyle[0] || "any",
          tags: this.activeFilters.tags || [],
        },
        onApply: (newFilters) => {
          // Normalize modal filters
          this.activeFilters = {
            gender: newFilters.gender === "any" ? [] : [newFilters.gender],
            imageStyle: newFilters.style === "any" ? [] : [newFilters.style],
            tags: newFilters.tags || [],
            hasImage: false,
            hasExampleDialogs: false,
          };
          // Automatically build a search term if tags exist
          const tagQuery = this.activeFilters.tags.map((t) => t.name).join(" ");
          this.searchTerm = tagQuery || this.searchTerm;
          this.applyFilters();
        },
      });
    },

    applyFilters() {
      const tagNames = this.activeFilters.tags.map((t) => t.name);
      if (tagNames.length) {
        const query = tagNames.join(" ");
        this.searchTerm = query;
        this.search_character(query, true);
      } else if (this.searchTerm.trim()) {
        this.search_character(this.searchTerm, true);
      } else {
        this.get_top(true);
      }
    },

    debouncedSearch() {
      clearTimeout(this.debounceTimer);
      this.debounceTimer = setTimeout(() => {
        if (this.searchTerm.trim()) {
          this.search_character(this.searchTerm, true);
        } else {
          this.get_top(true);
        }
      }, 300);
    },

    clearSearch() {
      this.searchTerm = "";
      this.get_top(true);
    },

    updateSort(option) {
      this.sortOption = option;
      if (this.searchTerm) this.search_character(this.searchTerm, true);
      else this.get_top(true);
    },

    // ─── data helpers ─────────────────────────────────────────────
    flattenSections(sections) {
      if (!Array.isArray(sections)) return [];
      return sections.reduce((acc, s) => {
        const chars = Array.isArray(s.characters) ? s.characters : [];
        acc.push(...chars);
        return acc;
      }, []);
    },

    normalizeCharacters(chars) {
      if (!Array.isArray(chars)) return [];
      return chars.filter((c) => c && c.name !== undefined);
    },

    mergeSections(existingSections, incomingSections) {
      const existingById = new Map(
        (existingSections || []).map((s) => [s.id, s])
      );

      const order = (existingSections || []).map((s) => s.id);

      for (const incoming of incomingSections) {
        if (!incoming || !incoming.id) continue;

        const normalizedIncoming = {
          ...incoming,
          characters: this.normalizeCharacters(incoming.characters),
        };

        const existing = existingById.get(incoming.id);
        if (!existing) {
          existingById.set(incoming.id, normalizedIncoming);
          order.push(incoming.id);
          continue;
        }

        const seen = new Set(
          (existing.characters || []).map((c) => String(c.id))
        );
        const mergedCharacters = [...(existing.characters || [])];
        for (const c of normalizedIncoming.characters) {
          const key = String(c.id);
          if (seen.has(key)) continue;
          seen.add(key);
          mergedCharacters.push(c);
        }

        existingById.set(incoming.id, {
          ...existing,
          ...normalizedIncoming,
          characters: mergedCharacters,
        });
      }

      return order.map((id) => existingById.get(id)).filter(Boolean);
    },

    computeAllDataLoadedFromSections(sections) {
      if (!Array.isArray(sections) || sections.length === 0) return false;

      // All sections must have pagination.has_next === false.
      return sections.every((s) => {
        // If pagination is missing, treat it as not-yet-loaded (important for incremental loading).
        if (!s || !s.pagination) return false;
        if (typeof s.pagination.has_next === "boolean") return !s.pagination.has_next;
        return true;
      });
    },

    humanizeExploreSectionKey(key) {
      if (!key) return "Section";
      return key
        .toString()
        .replace(/_/g, " ")
        .replace(/\b\w/g, (m) => m.toUpperCase());
    },

    refreshFiltered() {
      this._animatedAvatarDataVersion = (this._animatedAvatarDataVersion || 0) + 1;
      if (this.topSections.length) {
        this.filteredSections = this.applyFiltersToSections(this.topSections);
        // keep flat list updated too (used by some UI logic)
        this.filteredCharacters = this.flattenSections(this.filteredSections);
      } else {
        this.filteredCharacters = this.applyFiltersToCharacters(this.top);
        this.filteredSections = [];
      }
    },

    applyFiltersToSections(sections) {
      return (sections || []).map((s) => ({
        ...s,
        characters: this.applyFiltersToCharacters(this.normalizeCharacters(s.characters)),
      }));
    },

    handleCharactersResponse(data, initial) {
      if (!data) return;

      // New shape: { sections: [...] }
      if (Array.isArray(data.sections)) {
        const incomingSections = data.sections
          .filter((s) => s && s.id)
          .map((s) => ({
            ...s,
            characters: this.normalizeCharacters(s.characters),
          }));

        this.topSections = initial
          ? incomingSections
          : this.mergeSections(this.topSections, incomingSections);

        // Keep legacy list in sync (not rendered when hasSections=true, but used elsewhere).
        this.top = this.flattenSections(this.topSections);

        this.allDataLoaded = this.computeAllDataLoadedFromSections(this.topSections);
        this.refreshFiltered();
        return;
      }

      // Legacy shape: { characters: [...] }
      const chars = this.normalizeCharacters(data.characters);
      this.topSections = [];
      this.top = initial ? chars : [...this.top, ...chars];
      if (chars.length < this.itemsPerPage) this.allDataLoaded = true;
      this.refreshFiltered();
    },

    // ─── data-fetching ──────────────────────────────
    normalizedTags() {
      const raw = Array.isArray(this.tags) ? this.tags : [];
      // support both strings and {name: string} objects
      return raw
        .map((t) => (t && typeof t === "object" ? t.name : t))
        .map((t) => (t || "").toString().trim())
        .filter(Boolean);
    },

    async get_top(initial = false) {
      if (initial) {
        this.top = [];
        this.topSections = [];
        this.filteredCharacters = [];
        this.filteredSections = [];
        this.currentPage = 1;
        this.allDataLoaded = false;

        this.sectionPages = {};
        this.sectionLoading = {};

        // Cancel any pending explore requests from a previous view.
        this.exploreRequestToken++;
      }

      this.loading = true;

       // Explore: fetch each section individually.
      if (this.isExploreMode) {
        // IMPORTANT: do NOT await here. We want sections to render as soon as each
        // request resolves, rather than waiting for all of them.
        this.get_explore_sections(initial);
        this.loading = false;
        return;
      }

      const tags = this.normalizedTags();
      const res = await this.$root.handleRequest("top", {
        sort: this.sortOption,
        page: this.currentPage,
        language: this.$root.language || 'en',
        per_page: this.itemsPerPage,
        ...(tags.length ? { tags } : {}),
      });

      if (res.success) {
        this.handleCharactersResponse(res.data, initial);
      }

      this.loading = false;
    },

    async get_explore_sections(initial = false) {
      const keys = this.exploreSectionKeys();

      // Token used to ignore stale responses if sort/search changes.
      const token = ++this.exploreRequestToken;

      // Initialize per-section pages + loading state.
      const nextPages = { ...(this.sectionPages || {}) };
      const nextLoading = { ...(this.sectionLoading || {}) };
      for (const k of keys) {
        nextPages[k] = 1;
        nextLoading[k] = true;
      }
      this.sectionPages = nextPages;
      this.sectionLoading = nextLoading;

      // Render placeholders immediately (so the UI shows section shells right away).
      // Each placeholder will be replaced/merged as its request finishes.
      const placeholders = keys.map((k) => ({
        id: k,
        title: this.humanizeExploreSectionKey(k),
        description: "",
        characters: [],
        pagination: null,
      }));

      this.topSections = initial
        ? placeholders
        : this.mergeSections(this.topSections, placeholders);

      this.top = this.flattenSections(this.topSections);
      this.allDataLoaded = false;
      this.refreshFiltered();

      // Fire requests with limited concurrency so mobile connections aren't
      // saturated by ~16 simultaneous explore requests.
      const loadSection = async (sectionKey) => {
        try {
          const res = await this.$root.handleRequest("top", {
            sort: "explore",
            section: sectionKey,
            page: 1,
            per_page: this.explorePerPage(),
            language: this.$root.language || "en",
          });

          // Ignore stale results.
          if (token !== this.exploreRequestToken) return;

          if (res && res.success && res.data && Array.isArray(res.data.sections) && res.data.sections[0]) {
            this.topSections = this.mergeSections(this.topSections, [res.data.sections[0]]);
            this.top = this.flattenSections(this.topSections);
            this.allDataLoaded = this.computeAllDataLoadedFromSections(this.topSections);
            this.refreshFiltered();
          }
        } catch (e) {
          // Silent fail per-section; other sections should continue to load.
          console.error("Explore section load failed:", sectionKey, e);
        } finally {
          // Only update state if still current.
          if (token !== this.exploreRequestToken) return;
          this.sectionLoading = { ...this.sectionLoading, [sectionKey]: false };
        }
      };

      const concurrency = Math.min(4, keys.length);
      let cursor = 0;
      const worker = async () => {
        while (cursor < keys.length) {
          await loadSection(keys[cursor++]);
        }
      };
      await Promise.all(Array.from({ length: concurrency }, worker));
    },

    async loadMoreSection(sectionId) {
      if (!sectionId) return;
      if (this.sectionLoading[sectionId]) return;

      // Find current section state
      const section = (this.topSections || []).find((s) => s && s.id === sectionId);
      const hasNext = section?.pagination?.has_next;
      if (hasNext === false) return;

      const nextPage = (this.sectionPages[sectionId] || 1) + 1;
      this.sectionLoading = { ...this.sectionLoading, [sectionId]: true };

      try {
        const res = await this.$root.handleRequest("top", {
          sort: "explore",
          section: sectionId,
          page: nextPage,
          per_page: this.explorePerPage(),
          language: this.$root.language || "en",
        });

        if (res && res.success && res.data && Array.isArray(res.data.sections) && res.data.sections[0]) {
          this.sectionPages = { ...this.sectionPages, [sectionId]: nextPage };
          this.topSections = this.mergeSections(this.topSections, [res.data.sections[0]]);
          this.top = this.flattenSections(this.topSections);
          this.allDataLoaded = this.computeAllDataLoadedFromSections(this.topSections);
          this.refreshFiltered();
        }
      } finally {
        this.sectionLoading = { ...this.sectionLoading, [sectionId]: false };
      }
    },

    async search_character(query, initial = false) {
      if (initial) {
        this.top = [];
        this.topSections = [];
        this.filteredCharacters = [];
        this.filteredSections = [];
        this.currentPage = 1;
        this.allDataLoaded = false;

        // Cancel any pending explore requests.
        this.exploreRequestToken++;
      }

      this.loading = true;
      const res = await this.$root.handleRequest("search", {
        query,
        sort: this.sortOption,
        page: this.currentPage,
        per_page: this.itemsPerPage,
      });

      if (res.success) {
        this.handleCharactersResponse(res.data, initial);
      }

      this.loading = false;
    },

    async loadMore() {
      if (this.loading || this.allDataLoaded) return;
      this.currentPage++;
      if (this.searchTerm.trim()) await this.search_character(this.searchTerm);
      else await this.get_top();
    },

    applyFiltersToCharacters(characters) {
      return (characters || []).filter((c) => {
        if (
          this.activeFilters.gender.length &&
          !this.activeFilters.gender.includes(c.gender)
        )
          return false;

        if (this.activeFilters.imageStyle.length) {
          const style = c.imgen?.image_style ? "realistic" : "anime";
          if (!this.activeFilters.imageStyle.includes(style)) return false;
        }

        if (this.activeFilters.hasImage && !c.avatar) return false;

        if (
          this.activeFilters.hasExampleDialogs &&
          (!c.mes_example || !c.mes_example.trim())
        )
          return false;

        return true;
      });
    },

    handleScroll({ target }) {
      this.handleAnimatedAvatarScrollActivity();
      if (this.allDataLoaded || this.loading) return;

      // In explore/section mode, pagination is per-section.
      // We intentionally do not auto-load on scroll here; each section has its own Load More button.
      if (this.hasSections) return;

      // Throttle layout reads to at most once per animation frame.
      if (this._scrollRaf) cancelAnimationFrame(this._scrollRaf);
      this._scrollRaf = requestAnimationFrame(() => {
        this._scrollRaf = null;
        const useHorizontal = this.horizontalScroll && !this.hasSections;
        const nearEnd = useHorizontal
          ? target.scrollLeft + target.clientWidth >= target.scrollWidth - 20
          : target.scrollTop + target.clientHeight >= target.scrollHeight - 20;
        if (nearEnd) this.loadMore();
      });
    },

    // ─── events ───────────────────────────────────────────────────
    open_character(character) {
      this.$store.commit("character_modal", { id: character });
      this.$root.openModal("character");
    },
    openOptions(id) {},
    send_report(id) {
      this.$store.commit("reporting_data", { id });
      this.$root.openModal("report");
    },
  },
};
</script>

<style scoped>
.snap-x {
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
}
.snap-start {
  scroll-snap-align: start;
}

/* ──────────────────────────────────────────────────────────────
   Sectioned layout styling
   ────────────────────────────────────────────────────────────── */
.browser-section {
  position: relative;
  padding: 14px;
  border: 1px solid rgba(255, 255, 255, 0.06);
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.03) 0%, rgba(0, 0, 0, 0) 100%);
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.45);
  overflow: hidden;
}

.browser-section::before {
  content: "";
  position: absolute;
  inset: 0;
  background:
    radial-gradient(circle at 12% 18%, var(--accent-a), transparent 55%),
    radial-gradient(circle at 88% 22%, var(--accent-b), transparent 55%),
    radial-gradient(circle at 50% 100%, rgba(255, 255, 255, 0.04), transparent 60%);
  opacity: 0.75;
  pointer-events: none;
}

.browser-section-header {
  position: relative;
  z-index: 10;
  padding: 10px 10px 12px 10px;
}

.browser-section-title {
  font-weight: 700;
  letter-spacing: -0.01em;
  font-size: 1.05rem;
  line-height: 1.2;
  color: rgba(226, 232, 240, 0.92); /* close to text-theme-200 */
}

.browser-section-desc {
  margin-top: 4px;
  font-size: 0.85rem;
  line-height: 1.25;
  color: rgba(148, 163, 184, 0.9); /* close to text-theme-400 */
}

.browser-section-pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 10px;
  font-size: 0.75rem;
  font-weight: 700;
  color: rgba(226, 232, 240, 0.9);
  border: 1px solid rgba(255, 255, 255, 0.08);
  background: rgba(2, 6, 23, 0.35);
}

.browser-section-pill.subtle {
  color: rgba(148, 163, 184, 0.95);
  background: rgba(0, 0, 0, 0.15);
}

/* Loading spinner animation */
@keyframes spin {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}
.animate-spin {
  animation: spin 1s linear infinite;
}

/* Shuffle button animations */
@keyframes shuffle-wiggle {
  0%,
  100% {
    transform: rotate(0deg) scale(1);
  }
  25% {
    transform: rotate(-15deg) scale(1.1);
  }
  50% {
    transform: rotate(15deg) scale(1.1);
  }
  75% {
    transform: rotate(-10deg) scale(1.05);
  }
}

@keyframes shuffle-spin {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}

@keyframes gradient-x {
  0%,
  100% {
    background-position: 0% 50%;
  }
  50% {
    background-position: 100% 50%;
  }
}

@keyframes sparkle {
  0%,
  100% {
    opacity: 0;
    transform: scale(0) translate(0, 0);
  }
  50% {
    opacity: 1;
    transform: scale(1) translate(var(--tx), var(--ty));
  }
}

.animate-shuffle-wiggle {
  animation: shuffle-wiggle 0.6s ease-in-out infinite;
}

.animate-shuffle-spin {
  animation: shuffle-spin 0.8s linear infinite;
}

.animate-gradient-x {
  background-size: 200% 200%;
  animation: gradient-x 2s ease infinite;
}

/* Sparkle particles */
.sparkle {
  position: absolute;
  width: 4px;
  height: 4px;
  background: radial-gradient(
    circle,
    rgba(139, 92, 246, 1) 0%,
    rgba(192, 132, 252, 0.8) 50%,
    transparent 100%
  );
  
  opacity: 0;
  pointer-events: none;
}

.shuffle-button:hover .sparkle {
  animation: sparkle 1.5s ease-in-out infinite;
}

.sparkle-1 {
  top: 20%;
  left: 10%;
  --tx: 5px;
  --ty: -10px;
  animation-delay: 0s;
}

.sparkle-2 {
  top: 60%;
  right: 15%;
  --tx: -8px;
  --ty: 5px;
  animation-delay: 0.3s;
}

.sparkle-3 {
  bottom: 25%;
  left: 25%;
  --tx: 3px;
  --ty: 8px;
  animation-delay: 0.6s;
}

.sparkle-4 {
  top: 30%;
  right: 25%;
  --tx: -5px;
  --ty: -5px;
  animation-delay: 0.9s;
}

.shuffle-button:hover .sparkle-1 {
  animation: sparkle 1.5s ease-in-out infinite 0s;
}

.shuffle-button:hover .sparkle-2 {
  animation: sparkle 1.5s ease-in-out infinite 0.3s;
}

.shuffle-button:hover .sparkle-3 {
  animation: sparkle 1.5s ease-in-out infinite 0.6s;
}

.shuffle-button:hover .sparkle-4 {
  animation: sparkle 1.5s ease-in-out infinite 0.9s;
}

/* Pulse glow on hover */
@keyframes pulse-glow {
  0%,
  100% {
    box-shadow: 0 0 5px rgba(139, 92, 246, 0.3),
      inset 0 0 10px rgba(139, 92, 246, 0.1);
  }
  50% {
    box-shadow: 0 0 20px rgba(139, 92, 246, 0.5),
      inset 0 0 20px rgba(139, 92, 246, 0.2);
  }
}

.shuffle-button:hover {
  animation: pulse-glow 2s ease-in-out infinite;
}

.shuffle-button:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

.shuffle-button:disabled:hover {
  animation: none;
}

.shuffle-button:disabled:hover .sparkle {
  animation: none;
}

.shuffle-button:active:not(:disabled) {
  transform: scale(0.95);
}
</style>
