<template>
  <div :class="['app-root', themeClass, densityClass, layoutClass]" :style="rootStyle">
    <header class="app-header">
        <div class="flex w-full items-center justify-between gap-4 px-6 py-4">
          <div class="flex items-center gap-3">
            <div class="h-10 w-10 rounded-full bg-[var(--accent)] text-black flex items-center justify-center vintage-shadow">
              <i class="fa-solid fa-wave-square"></i>
            </div>
            <div>
              <div class="section-title text-lg">PriceWatch</div>
              <div class="label">Vintage control deck</div>
    </div>
  </div>
  <transition name="scrape-log">
    <div v-if="scrapeLog.visible" ref="scrapeLogRef" class="scrape-log-bar z-40">
      <div
        v-for="entry in scrapeLog.messages"
        :key="entry.id"
        class="scrape-log-line"
      >
        <span class="scrape-log-time">[{{ entry.time }}]</span>
        <span class="scrape-log-icon">{{ LOG_ICONS[entry.level] }}</span>
        <span class="scrape-log-text">{{ entry.text }}</span>
      </div>
    </div>
  </transition>
          <div class="flex items-center gap-3 header-meta">
            <div class="label">FE v{{ frontendVersion }}</div>
            <div class="label">BE v{{ backendVersion }}</div>
          </div>
          <div class="relative flex-1 max-w-[320px]">
            <input
              class="input pr-10"
              placeholder="Rechercher"
              v-model="searchQuery"
            />
            <i class="fa-solid fa-magnifying-glass absolute right-3 top-3 text-[var(--muted)]"></i>
          </div>
          <div class="header-actions">
            <button
              class="icon-btn add-product-btn"
              title="Ajouter un produit"
              aria-label="Ajouter un produit"
              @click="openAddProduct"
            >
              <i class="fa-solid fa-plus"></i>
            </button>
            <div class="view-toggle-group" role="group" aria-label="Modes de vue">
              <button
                class="icon-btn header-icon"
                title="Vue cartes"
                :class="{ 'active-view': viewMode === 'cards' }"
                aria-label="Vue cartes"
                @click="viewMode = 'cards'"
              >
                <i class="fa-solid fa-border-all"></i>
              </button>
              <button
                class="icon-btn header-icon"
                title="Vue tableau"
                :class="{ 'active-view': viewMode === 'table' }"
                aria-label="Vue tableau"
                @click="viewMode = 'table'"
              >
                <i class="fa-solid fa-table"></i>
              </button>
            </div>
            <button class="icon-btn header-icon" title="Parametres" aria-label="Paramètres" @click="toggleSettings">
              <i class="fa-solid fa-gear"></i>
            </button>
            <button class="icon-btn header-icon" title="Recharger" aria-label="Recharger" @click="reloadPage">
              <i class="fa-solid fa-rotate"></i>
            </button>
            <button class="icon-btn header-icon" title="Theme" aria-label="Basculer mode clair/sombre" @click="cycleTheme">
              <i class="fa-solid fa-circle-half-stroke"></i>
            </button>
          </div>
          <div class="pill">{{ apiStatus }}</div>
        </div>
    </header>

    <div class="flex w-full gap-6 px-6 py-6">
      <aside class="sidebar">
        <div class="panel p-4 vintage-shadow">
          <div class="section-title text-base mb-3">Filtres & tri</div>
          <div class="flex items-center justify-between mb-2">
            <div class="label">Affichés {{ filteredProducts.length }} / {{ totalProducts }} produits</div>
            <button class="icon-btn" title="Réinitialiser" aria-label="Réinitialiser les filtres" @click="resetFilters">
              <i class="fa-solid fa-retweet"></i>
            </button>
          </div>
          <div class="flex flex-wrap gap-2 mb-3">
            <button
              v-for="chip in activeFilterChips"
              :key="chip.key"
              class="filter-chip"
              :title="chip.tooltip"
              type="button"
              @click="removeFilter(chip.key)"
            >
              {{ chip.label }}
              <i class="fa-solid fa-xmark ml-1"></i>
            </button>
          </div>
          <div class="space-y-3">
            <div>
              <div class="label">Store</div>
              <select class="input" v-model="filters.store">
                <option value="all">Tous</option>
                <option v-for="store in stores" :key="store.id" :value="store.id">
                  {{ store.name }}
                </option>
              </select>
            </div>
            <div>
              <div class="label">Stock</div>
              <select class="input" v-model="filters.stock">
                <option value="all">Tous</option>
                <option value="in_stock">En stock</option>
                <option value="out_of_stock">Rupture</option>
                <option value="unknown">Inconnu</option>
              </select>
            </div>
            <div class="grid grid-cols-2 gap-2">
              <div>
                <div class="label">Prix min</div>
                <input class="input" type="number" v-model.number="filters.priceMin" placeholder="Min" />
              </div>
              <div>
                <div class="label">Prix max</div>
                <input class="input" type="number" v-model.number="filters.priceMax" placeholder="Max" />
              </div>
            </div>
            <div>
              <div class="label">Tri</div>
              <select class="input" v-model="filters.sort">
                <option value="updated">Derniere maj</option>
                <option value="price">Prix actuel</option>
                <option value="delta">Variation</option>
                <option value="stock">Stock</option>
              </select>
            </div>
            <div>
              <div class="label">Ordre du tri</div>
              <select class="input" v-model="filters.sortOrder">
                <option value="desc">Décroissant</option>
                <option value="asc">Croissant</option>
              </select>
            </div>
          </div>
        </div>

        <div class="panel mt-4 p-4 vintage-shadow">
          <div class="section-title text-base mb-3">Comparaison</div>
          <div class="flex items-center justify-between">
            <div class="label">{{ compareIds.length }} selectionnes</div>
            <button class="icon-btn" title="Activer comparaison" @click="compareMode = !compareMode">
              <i class="fa-solid fa-code-compare"></i>
            </button>
          </div>
          <div class="label mt-3">Split view 2-4 produits</div>
        </div>
      </aside>

      <main class="flex-1">
        <div v-if="apiState.error" class="panel p-3 mb-4">
          <div class="label">Erreur API: {{ apiState.error }}</div>
        </div>

        <section v-if="compareMode" class="compare-grid">
          <div v-for="product in compareProducts" :key="product.id" class="card p-4">
            <div class="flex items-center justify-between">
              <div>
                <div class="section-title text-base">{{ product.title }}</div>
                <div class="label">{{ storeLabel(product.storeId) }}</div>
              </div>
              <div class="pill">{{ product.stockStatus }}</div>
            </div>
            <div class="mt-3 text-xl">{{ formatPrice(product.price, product.currency) }}</div>
            <div class="label">Delta: {{ product.delta }}%</div>
            <div class="mt-3 h-20 rounded-lg bg-[var(--surface-2)]"></div>
          </div>
        </section>

        <section v-else-if="viewMode === 'cards'" class="product-grid">
          <template v-for="product in filteredProducts" :key="product.id">
            <div
              class="card card-hover relative group"
              :class="{ 'card-accent': product.delta < 0 }"
              role="button"
              tabindex="0"
              @click="openProductDetails(product.id)"
              @keydown.enter.prevent="openProductDetails(product.id)"
              @keydown.space.prevent="openProductDetails(product.id)"
              @mouseenter="handleCardHover(product, $event)"
              @mouseleave="scheduleHideHistory(0)"
              @focusin="handleCardHover(product, $event)"
              @focusout="scheduleHideHistory(0)"
            >
              <template v-for="historyData in [productHistorySnapshots.get(product.id)]">
                <div class="card-thumbnail">
                  <picture v-if="product.imageWebp || product.imageJpg">
                    <source v-if="product.imageWebp" :srcset="product.imageWebp" type="image/webp" />
                    <source v-if="product.imageJpg" :srcset="product.imageJpg" type="image/jpeg" />
                    <img
                      :src="product.imageJpg || placeholderImage"
                      class="card-media-image"
                      :class="`card-media-${settings.value.imageMode ?? 'contain'}`"
                      alt="image produit"
                      loading="lazy"
                    />
                  </picture>
                  <img
                    v-else
                    :src="placeholderImage"
                    class="card-media-image"
                    :class="`card-media-${settings.value.imageMode ?? 'contain'}`"
                    alt="image indisponible"
                    loading="lazy"
                  />
                </div>

                <div class="card-identity">
                  <div class="store-icon">
                    <img v-if="storeLogo(product.storeId)" :src="storeLogo(product.storeId)" alt="" />
                    <span v-else class="label text-[10px]">{{ storeInitials(product.storeId) }}</span>
                  </div>
                  <div class="card-identity-text">
                    <div class="section-title text-base card-title" :title="product.title">
                      {{ product.title }}
                    </div>
                    <div class="label text-xs">{{ storeLabel(product.storeId) }}</div>
                  </div>
                </div>

                <div class="card-price-history">
                  <div class="history-price-grid">
                    <div class="history-panel" :style="{ color: historyData?.trendColor || 'var(--muted)' }">
                      <MiniLineChart
                        :points="historyData?.points ?? []"
                        :height="160"
                        :formatY="(value) => formatShortPrice(value, product.currency)"
                        :formatX="formatHistoryDateLabel"
                        :yTicks="4"
                        :xTicks="4"
                      />
                      <div class="chart-period-label">{{ chartPeriodLabel }}</div>
                    </div>
                    <div class="price-panel">
                      <div class="price-main">{{ formatShortPrice(product.price, product.currency) }}</div>
                      <div v-if="product.msrp" class="price-msrp">{{ formatShortPrice(product.msrp, product.currency) }}</div>
                      <div
                        v-if="product.discountAmount !== null && product.discountPercent !== null"
                        class="price-discount"
                      >
                        {{ formatDiscount(product.discountAmount, product.discountPercent) }}
                      </div>
                      <div class="stock-line" :class="statusClass(product.stockStatus)">
                        {{ statusLabel(product.stockStatus) }}
                      </div>
                    </div>
                  </div>
                  <div class="history-summary">
                    <span>
                      Min :
                      {{ historyData?.min !== null ? formatShortPrice(historyData.min, product.currency) : "—" }}
                    </span>
                    <span>
                      Max :
                      {{ historyData?.max !== null ? formatShortPrice(historyData.max, product.currency) : "—" }}
                    </span>
                    <span class="history-trend">
                      Tendance :
                      <span
                        class="trend-pill"
                        :style="{ color: historyData?.trendColor || 'var(--muted)' }"
                      >
                        {{ historyData?.trendIcon || "→" }} {{ historyData?.trendLabel || "—" }}
                      </span>
                      <span class="trend-delta">{{ historyData?.trendDeltaLabel || "—" }}</span>
                    </span>
                  </div>
                </div>

                <div class="card-update label text-xs">
                  Dernière mise à jour :
                  {{ formatRelativeTimeAgo(historyData?.lastTimestamp ?? parseHistoryTimestamp(product.updatedAt)) }}
                  ({{ storeLabel(product.storeId) }})
                </div>

                <div class="card-toolbar">
                  <button
                    class="icon-btn primary-action"
                    title="Rafraîchir"
                    aria-label="Rafraîchir"
                    @click.stop="refreshProduct(product.id)"
                  >
                    <i class="fa-solid fa-rotate"></i>
                  </button>
                  <div class="secondary-actions">
                    <button
                      class="icon-btn"
                      title="Comparer"
                      aria-label="Comparer le produit"
                      @click.stop="toggleCompare(product.id)"
                    >
                      <i class="fa-solid" :class="compareIds.includes(product.id) ? 'fa-square-check' : 'fa-square'"></i>
                    </button>
                    <button class="icon-btn" title="Editer" aria-label="Modifier" @click.stop="selectProduct(product.id)">
                      <i class="fa-solid fa-pen"></i>
                    </button>
                    <button class="icon-btn" title="Supprimer" aria-label="Supprimer le produit" @click.stop="deleteProduct(product.id)">
                      <i class="fa-solid fa-trash"></i>
                    </button>
                    <button class="icon-btn" title="Ouvrir" aria-label="Ouvrir dans un nouvel onglet" @click.stop="openProductUrl(product)">
                      <i class="fa-solid fa-up-right-from-square"></i>
                    </button>
                  </div>
                </div>
              </template>
            </div>
          </template>
        </section>

        <section v-else class="panel p-4">
          <div class="section-title text-base mb-3">Tableau dense</div>
          <div class="overflow-auto">
            <table class="w-full text-sm">
              <thead class="text-left text-[var(--muted)]">
                <tr>
                  <th class="pb-2">Produit</th>
                  <th class="pb-2">Store</th>
                  <th class="pb-2">Prix</th>
                  <th class="pb-2">Delta</th>
                  <th class="pb-2">Stock</th>
                  <th class="pb-2">Actions</th>
                </tr>
              </thead>
              <tbody>
                <tr v-for="product in filteredProducts" :key="product.id" class="border-t border-white/5">
                  <td class="py-3">{{ product.title }}</td>
                  <td class="py-3">{{ storeLabel(product.storeId) }}</td>
                  <td class="py-3">{{ formatPrice(product.price, product.currency) }}</td>
                  <td class="py-3">{{ product.delta }}%</td>
                  <td class="py-3">{{ product.stockStatus }}</td>
                  <td class="py-3">
                    <div class="flex items-center gap-2">
                      <button class="icon-btn" title="Detail" @click="selectProduct(product.id)">
                        <i class="fa-solid fa-eye"></i>
                      </button>
                      <button class="icon-btn" title="Comparer" @click="toggleCompare(product.id)">
                        <i class="fa-solid" :class="compareIds.includes(product.id) ? 'fa-square-check' : 'fa-square'" ></i>
                      </button>
                      <button class="icon-btn" title="Supprimer" @click="deleteProduct(product.id)">
                        <i class="fa-solid fa-trash"></i>
                      </button>
                      <button class="icon-btn" title="Relancer le scrap" @click="refreshProduct(product.id)">
                        <i class="fa-solid fa-rotate"></i>
                      </button>
                      <button class="icon-btn" title="Ouvrir dans un nouvel onglet" @click="openProductUrl(product)">
                        <i class="fa-solid fa-up-right-from-square"></i>
                      </button>
                    </div>
                  </td>
                </tr>
              </tbody>
            </table>
          </div>
        </section>
      </main>
    </div>

    <div v-if="settingsOpen" class="fixed inset-0 bg-black/40 z-50" @click.self="toggleSettings">
      <div class="absolute right-0 top-0 h-full w-full max-w-md panel p-6 overflow-auto">
        <div class="flex items-center justify-between mb-4">
          <div class="section-title text-lg">Parametres</div>
          <button class="icon-btn" title="Fermer" @click="toggleSettings">
            <i class="fa-solid fa-xmark"></i>
          </button>
        </div>

        <div class="space-y-6">
          <section>
            <div class="section-title text-sm mb-2">Apparence</div>
            <div class="space-y-3">
              <div>
                <div class="label">Theme</div>
                <select class="input" v-model="theme">
                  <option value="gruvbox-dark">Gruvbox Dark</option>
                  <option value="gruvbox-light">Gruvbox Light</option>
                  <option value="monokai-dark">Monokai Dark</option>
                  <option value="monokai-light">Monokai Light</option>
                </select>
              </div>
              <div>
                <div class="label">Densite</div>
                <select class="input" v-model="density">
                  <option value="dense">Dense</option>
                  <option value="comfort">Confort</option>
                </select>
              </div>
              <div>
                <div class="label">Layout</div>
                <select class="input" v-model="layoutMode">
                  <option value="auto">Auto</option>
                  <option value="compact">Compact</option>
                  <option value="wide">Large</option>
                </select>
              </div>
              <div>
                <div class="label">Police titre</div>
                <select class="input" v-model="settings.fontTitle">
                  <option v-for="font in fontPresets" :key="font.id" :value="font.id">
                    {{ font.label }}
                  </option>
                </select>
              </div>
              <div>
                <div class="label">Police corps</div>
                <select class="input" v-model="settings.fontBody">
                  <option v-for="font in fontPresets" :key="font.id" :value="font.id">
                    {{ font.label }}
                  </option>
                </select>
              </div>
              <div>
                <div class="label">Police mono</div>
                <select class="input" v-model="settings.fontMono">
                  <option v-for="font in fontPresets" :key="font.id" :value="font.id">
                    {{ font.label }}
                  </option>
                </select>
              </div>
              <div>
                <div class="label">Taille texte</div>
                <div class="flex items-center gap-2">
                  <input class="input" type="range" min="12" max="20" step="1" v-model.number="settings.fontSize" />
                  <div class="label min-w-[44px] text-right">{{ settings.fontSize }}px</div>
                </div>
              </div>
              <div>
                <div class="label">Taille icones</div>
                <select class="input" v-model="settings.iconSize">
                  <option value="sm">Compact</option>
                  <option value="md">Normal</option>
                  <option value="lg">Large</option>
                </select>
              </div>
              <div>
                <div class="label flex items-center justify-between">
                  <span>Taille icône boutique</span>
                  <span class="text-xs label">{{ storeIconDisplay }}</span>
                </div>
                <input
                  class="input"
                  type="range"
                  min="16"
                  max="40"
                  v-model.number="storeIconSize"
                />
              </div>
              <div>
                <div class="label flex items-center justify-between">
                  <span>Ratio des vignettes</span>
                  <span class="text-xs label">{{ cardRatioLabel }}</span>
                </div>
                <input
                  class="input"
                  type="range"
                  min="0.75"
                  max="1.3"
                  step="0.05"
                  v-model.number="settings.cardRatio"
                />
                <div class="text-xs label flex justify-between mt-1">
                  <span>Compact</span>
                  <span>Standard</span>
                  <span>Étendu</span>
                </div>
                <div class="label text-xs mt-2">{{ cardColumnsLabel }}</div>
              </div>
              <div>
                <div class="label flex items-center justify-between">
                  <span>Hauteur image produit</span>
                  <span class="text-xs label">{{ cardImageHeightLabel }}</span>
                </div>
                <input
                  class="input"
                  type="range"
                  min="120"
                  max="220"
                  step="5"
                  v-model.number="settings.cardImageHeight"
                />
              </div>
              <div>
                <div class="label">Mode image</div>
                <select class="input" v-model="settings.imageMode">
                  <option v-for="mode in IMAGE_MODES" :key="mode.value" :value="mode.value">
                    {{ mode.label }}
                  </option>
                </select>
              </div>
              <div>
                <div class="label">Période du graphique</div>
                <select class="input" v-model.number="settings.chartPeriod">
                  <option v-for="option in CHART_PERIOD_OPTIONS" :key="option.value" :value="option.value">
                    {{ option.label }}
                  </option>
                </select>
              </div>
              <div>
                <div class="label">Durée barre logs (ms)</div>
                <input class="input" type="number" min="500" v-model.number="settings.logDuration" />
              </div>
              <div class="grid grid-cols-2 gap-2">
                <div>
                  <div class="label">Seuil baisse (%)</div>
                  <input class="input" type="number" step="0.1" v-model.number="settings.trendThresholdDown" />
                </div>
                <div>
                  <div class="label">Seuil hausse (%)</div>
                  <input class="input" type="number" step="0.1" v-model.number="settings.trendThresholdUp" />
                </div>
              </div>
            </div>
          </section>

          <section>
            <div class="section-title text-sm mb-2">Donnees</div>
            <div class="space-y-3">
              <div>
                <div class="label">Frequence rafraichissement</div>
                <input class="input" v-model.number="settings.refreshMinutes" type="number" min="1" />
              </div>
              <div>
                <div class="label">Limite produits page</div>
                <input class="input" v-model.number="settings.pageSize" type="number" min="10" />
              </div>
              <div>
                <div class="label">Preferer WebP</div>
                <select class="input" v-model="settings.preferWebp">
                  <option :value="true">Oui</option>
                  <option :value="false">Non</option>
                </select>
              </div>
            </div>
          </section>

          <section>
            <div class="section-title text-sm mb-2">API</div>
            <div class="space-y-3">
              <div>
                <div class="label">Base URL</div>
                <input class="input" v-model="settings.apiBase" placeholder="/api" />
              </div>
              <div>
                <div class="label">Token</div>
                <input class="input" type="password" v-model="settings.apiToken" placeholder="Bearer token" />
              </div>
              <button class="input text-left" @click="fetchProducts">
                Recharger les donnees
              </button>
            </div>
          </section>

          <section>
            <div class="section-title text-sm mb-2">Stores</div>
            <div class="space-y-2">
              <div v-for="store in stores" :key="store.id" class="space-y-2 border border-white/5 rounded-lg p-2">
                <div class="label">Nom</div>
                <input class="input" v-model="store.name" />
                <div class="label">Domaine</div>
                <input class="input" v-model="store.domain" />
                <div class="flex justify-end">
                  <button class="icon-btn" title="Supprimer" @click="removeStore(store.id)">
                    <i class="fa-solid fa-trash"></i>
                  </button>
                </div>
              </div>
              <div class="mt-3">
                <div class="label">Ajouter un store</div>
                <div class="flex gap-2">
                  <input class="input" v-model="storeDraftName" placeholder="Nom" />
                  <input class="input" v-model="storeDraftDomain" placeholder="Domaine" />
                  <button class="icon-btn" title="Ajouter" @click="addStore">
                    <i class="fa-solid fa-plus"></i>
                  </button>
                </div>
              </div>
            </div>
          </section>

          <section>
            <div class="section-title text-sm mb-2">Debug</div>
            <div class="space-y-2">
            <div class="flex items-center justify-between">
              <div class="label">Afficher erreurs scraping</div>
              <input type="checkbox" v-model="settings.showDebug" />
            </div>
            <div class="flex items-center justify-between">
              <div class="label">Animations</div>
              <input type="checkbox" v-model="settings.enableAnimations" />
            </div>
            <div class="flex items-center justify-between">
              <div class="label">Historique au survol</div>
              <input type="checkbox" v-model="settings.showHistoryHover" />
            </div>
          </div>
        </section>

          <section>
            <div class="section-title text-sm mb-2">Maintenance</div>
            <div class="space-y-2">
              <button class="input text-left" @click="applySettingsAndReload">
                Appliquer et recharger
              </button>
              <button class="input text-left" @click="reloadPage">
                Recharger la page
              </button>
              <button class="input text-left" @click="clearCacheAndReload">
                Vider le cache et recharger
              </button>
            </div>
          </section>
        </div>
      </div>
    </div>

    <div v-if="addProductOpen" class="fixed inset-0 bg-black/40 z-50 flex items-center justify-center p-4" @click.self="closeAddProduct">
      <div class="w-full max-w-2xl panel p-6 overflow-auto">
        <div class="flex items-center justify-between mb-4">
          <div class="section-title text-lg">Ajouter un produit</div>
          <button class="icon-btn" title="Fermer" @click="closeAddProduct">
            <i class="fa-solid fa-xmark"></i>
          </button>
        </div>
        <div class="space-y-3">
          <input class="input" v-model="addProductDraft.url" placeholder="URL" />
          <select class="input" v-model="addProductDraft.usePlaywright">
            <option :value="false">HTTP uniquement</option>
            <option :value="true">Playwright fallback</option>
          </select>
          <button class="input text-left" @click="previewAddProduct" :disabled="!addProductDraft.url || addProductState.loading">
            {{ addProductState.loading ? "Analyse en cours..." : "Ajouter" }}
          </button>
          <div v-if="addProductState.error" class="label">Erreur: {{ addProductState.error }}</div>

          <div v-if="addProductState.snapshot" class="panel p-3 space-y-3">
            <div class="section-title text-sm">Donnees scrappees</div>
            <div class="label">Store: {{ addProductState.snapshot.source }}</div>
            <div class="label">Reference: {{ addProductState.snapshot.reference || "n/a" }}</div>
            <div class="label">Titre: {{ addProductState.snapshot.title || "n/a" }}</div>
            <div class="flex items-start gap-3">
              <picture>
                <source :srcset="addProductImagePreview" type="image/webp" />
                <source :srcset="addProductImagePreview" type="image/jpeg" />
                <img
                  :src="addProductImagePreview"
                  class="h-20 w-20 rounded-lg object-cover border border-white/10"
                  alt="vignette"
                  loading="lazy"
                />
              </picture>
              <div class="flex-1">
                <div class="label">Images: {{ addProductImagesCount }}</div>
                <div v-if="addProductImageThumbs.length" class="mt-3 grid grid-cols-4 gap-2">
                  <button
                    v-for="(image, index) in addProductImageThumbs"
                    :key="`${image}-${index}`"
                    type="button"
                    class="image-toggle"
                    :class="{ selected: selectedImagesSet.has(index) }"
                    @click="toggleAddProductImage(index)"
                  >
                    <picture>
                      <source :srcset="image" type="image/webp" />
                      <img
                        :src="image"
                        class="h-16 w-full object-cover rounded-md"
                        alt="miniature"
                        loading="lazy"
                      />
                    </picture>
                  </button>
                </div>
              </div>
            </div>
            <div class="label">Prix: {{ formatPrice(addProductState.snapshot.price, addProductState.snapshot.currency) }}</div>
            <div class="label">Prix conseille: {{ formatPrice(addProductState.snapshot.msrp, addProductState.snapshot.currency) }}</div>
            <div class="label">Stock: {{ addProductState.snapshot.stock_status || "n/a" }}</div>
            <div class="label">Categorie: {{ addProductState.snapshot.category || "n/a" }}</div>
            <div class="label">Description: {{ addProductState.snapshot.description || "n/a" }}</div>
            <div class="label">Caracteristiques: {{ addProductSpecsCount }}</div>
            <div v-if="addProductSpecEntries.length === 0" class="label">n/a</div>
            <div v-else class="space-y-1">
              <div v-for="[key, value] in addProductSpecEntries" :key="key" class="label">
                {{ key }}: {{ value }}
              </div>
            </div>
            <button class="input text-left" @click="commitAddProduct">
              Enregistrer
            </button>
          </div>
        </div>
      </div>
    </div>


    <div
      v-if="activeProduct"
      class="fixed inset-0 bg-black/40 z-50 flex items-center justify-center p-4"
      @click.self="activeProductId = null"
    >
        <div
        ref="detailModalRef"
        class="w-full max-w-6xl detail-dialog panel detail-popup flex flex-col overflow-hidden"
        role="dialog"
        :aria-label="`Détails produit ${activeProduct.title || ''}`"
      >
        <div class="detail-modal-header">
          <div>
            <div class="section-title text-lg">
              Nom :
              <span class="detail-title" :title="activeProduct.title">{{ activeProduct.title || "Sans titre" }}</span>
            </div>
            <div class="label">
              {{ storeLabel(activeProduct.storeId) }} • {{ activeProduct.reference || "Réf. inconnue" }}
            </div>
          </div>
          <button
            class="icon-btn"
            title="Fermer"
            aria-label="Fermer le panneau détail"
            @click="activeProductId = null"
            type="button"
          >
            <i class="fa-solid fa-xmark"></i>
          </button>
        </div>
        <div class="detail-content-area flex-1 overflow-hidden">
          <div class="detail-columns grid lg:grid-cols-[1.1fr,0.9fr] gap-6 h-full">
            <div class="space-y-4 detail-left overflow-hidden">
              <section class="detail-card detail-summary-card space-y-4">
                <div class="section-title text-sm">Résumé</div>
                <picture class="detail-summary-image bg-[var(--surface-2)] rounded-xl border border-white/10 overflow-hidden">
                  <source :srcset="activeProduct.imageWebp" type="image/webp" />
                  <source :srcset="activeProduct.imageJpg" type="image/jpeg" />
                  <img
                    :src="activeProduct.imageJpg"
                    class="w-full h-full object-contain rounded-xl"
                    alt="image produit"
                    loading="lazy"
                  />
                </picture>
                <div class="label detail-summary-title" :title="activeProduct.title">{{ activeProduct.title || "Sans titre" }}</div>
                <div class="label">Stock actuel: {{ statusLabel(activeProduct.stockStatus) }}</div>
              </section>
              <section class="detail-card detail-tabs-card space-y-3">
                <div class="detail-tabs" role="tablist">
                  <button
                    class="detail-tab-button"
                    :class="{ active: detailTab === 'description' }"
                    :disabled="!detailHasDescription"
                    role="tab"
                    @click="detailTab = 'description'"
                    :aria-selected="detailTab === 'description'"
                    type="button"
                  >
                    Description
                  </button>
                  <button
                    class="detail-tab-button"
                    :class="{ active: detailTab === 'specs' }"
                    :disabled="!detailHasSpecs"
                    role="tab"
                    @click="detailTab = 'specs'"
                    :aria-selected="detailTab === 'specs'"
                    type="button"
                  >
                    Caractéristiques
                  </button>
                </div>
                <div class="detail-tab-panel max-h-[20rem] overflow-auto">
                  <div v-if="detailTab === 'description'">
                    <div v-if="detailHasDescription" class="label detail-text" :title="activeProduct.description">
                      {{ activeProduct.description }}
                    </div>
                    <div v-else class="label detail-empty">Aucune description.</div>
                  </div>
                  <div v-else>
                    <div v-if="detailHasSpecs" class="detail-specs space-y-1">
                      <div
                        v-for="(entry, index) in detailSpecEntries"
                        :key="`${entry[0]}-${index}`"
                        class="label"
                      >
                        <span class="font-bold">{{ entry[0] }}</span>: {{ entry[1] }}
                      </div>
                    </div>
                    <div v-else class="label detail-empty">Aucune caractéristique.</div>
                  </div>
                </div>
              </section>
            </div>
            <div class="space-y-4 detail-right overflow-hidden">
              <section class="detail-card price-card space-y-2">
                <div class="section-title text-sm">Prix</div>
                <div class="detail-price-row">
                  <div class="detail-price-value">{{ formatPrice(activeProduct.price, activeProduct.currency) }}</div>
                  <div class="label detail-price-updated">Dernière mise à jour: {{ activeProduct.updatedAtLabel }}</div>
                </div>
                <div class="label">Prix conseillé: {{ formatPrice(activeProduct.msrp, activeProduct.currency) }}</div>
                <div class="label">Réduction: {{ formatDiscount(activeProduct.discountAmount, activeProduct.discountPercent) }}</div>
              </section>
              <section class="detail-card source-card space-y-2">
                <div class="section-title text-sm">Source</div>
                <div class="flex flex-wrap items-center gap-3">
                  <a
                    v-if="activeProduct.url"
                    :href="activeProduct.url"
                    target="_blank"
                    rel="noreferrer"
                    class="label link flex-1"
                    title="Ouvrir la page produit"
                  >
                    Ouvrir la page produit
                  </a>
                  <button
                    class="icon-btn"
                    title="Copier l'URL"
                    aria-label="Copier l'URL"
                    :class="!activeProduct.url ? 'opacity-50 pointer-events-none' : ''"
                    @click="copyActiveUrl"
                    type="button"
                  >
                    <i class="fa-solid fa-copy"></i>
                  </button>
                </div>
                <div class="label truncate">URL: {{ activeProduct.url || "n/a" }}</div>
                <div class="label">ASIN / Référence: {{ activeProduct.reference || "n/a" }}</div>
              </section>
              <section class="detail-card history-card space-y-3">
                <div class="section-title text-sm">Historique</div>
                <div class="detail-history-periods flex gap-2 flex-wrap">
                  <button
                    v-for="period in DETAIL_PERIODS"
                    :key="period.id"
                    class="detail-period-button"
                    :class="{ selected: detailHistoryPeriod === period.id }"
                    type="button"
                    @click="detailHistoryPeriod = period.id"
                  >
                    {{ period.label }}
                  </button>
                </div>
                <div class="detail-history-summary grid grid-cols-2 gap-2 text-xs">
                  <div>
                    <div class="label">Actuel</div>
                    <div class="section-title text-sm">{{ formatShortPrice(detailHistorySummary.current, activeProduct.currency) }}</div>
                  </div>
                  <div>
                    <div class="label">Min</div>
                    <div class="section-title text-sm">{{ formatShortPrice(detailHistorySummary.min, activeProduct.currency) }}</div>
                  </div>
                  <div>
                    <div class="label">Max</div>
                    <div class="section-title text-sm">{{ formatShortPrice(detailHistorySummary.max, activeProduct.currency) }}</div>
                  </div>
                  <div>
                    <div class="label">Delta</div>
                    <div class="section-title text-sm">{{ detailDeltaLabel }}</div>
                  </div>
                </div>
                <PriceHistoryChart
                  class="mt-2"
                  :history="detailHistory.history"
                  :current-price="detailHistory.currentPrice"
                  :min-price="detailHistory.minPrice"
                  :max-price="detailHistory.maxPrice"
                  :delta-label="detailDeltaLabel"
                />
              </section>
              <section class="detail-card edit-card space-y-3">
                <div class="section-title text-sm">Edition produit</div>
                <div class="space-y-2">
                  <input class="input" v-model="activeProduct.title" placeholder="Nom produit" />
                  <input class="input" v-model="activeProduct.url" placeholder="URL" />
                  <input class="input" v-model="activeProduct.category" placeholder="Catégorie" />
                  <input class="input" v-model="activeProduct.currency" placeholder="Devise" />
                  <input class="input" v-model="activeProduct.reference" placeholder="Référence" disabled />
                  <input class="input" v-model.number="activeProduct.refreshHours" type="number" min="1" placeholder="Fréquence (h)" />
                </div>
                <div class="flex items-center gap-2 actions-section">
                  <button class="icon-btn" title="Enregistrer" aria-label="Enregistrer le produit" @click="saveActiveProduct" type="button">
                    <i class="fa-solid fa-floppy-disk"></i>
                  </button>
                  <button class="icon-btn" title="Rafraîchir" aria-label="Rafraîchir le produit" @click="enqueueActiveProduct" type="button">
                    <i class="fa-solid fa-rotate"></i>
                  </button>
                  <button class="icon-btn" title="Supprimer" aria-label="Supprimer le produit" @click="deleteActiveProduct" type="button">
                    <i class="fa-solid fa-trash"></i>
                  </button>
                </div>
              </section>
            </div>
          </div>
        </div>
      </div>
    </div>
    <div class="fixed bottom-4 left-4 panel p-3 vintage-shadow flex items-center gap-3 log-status-panel">
      <div class="label">API {{ apiStatus }}</div>
      <div class="label">DB {{ apiHealth.db ? "OK" : "KO" }}</div>
      <div class="label">Redis {{ apiHealth.redis ? "OK" : "KO" }}</div>
      <button class="icon-btn" title="Logs" @click="toggleLogs">
        <i class="fa-solid fa-scroll"></i>
      </button>
    </div>

    <div v-if="logsOpen" class="fixed inset-0 bg-black/40 z-50" @click.self="toggleLogs">
      <div
        class="absolute bottom-0 left-0 w-full max-w-2xl panel p-6 logs-panel"
      >
        <div class="flex items-center justify-between mb-2">
          <div class="section-title text-lg">Logs</div>
          <button class="icon-btn" title="Fermer" @click="toggleLogs">
            <i class="fa-solid fa-xmark"></i>
          </button>
        </div>
        <div class="logs-content">
          <div
            v-for="entry in filteredLogs"
            :key="entry.id"
            class="panel p-2 log-entry"
            :class="{ 'log-entry-error': entry.level === 'ERROR' }"
          >
            <div class="label">{{ entry.time }} - {{ entry.source }} - {{ entry.level }}</div>
            <div>{{ entry.message }}</div>
          </div>
          <div v-if="filteredLogs.length === 0" class="label">Aucun log</div>
        </div>
        <div class="logs-toolbar flex items-center gap-3 mt-4">
          <button
            class="icon-btn"
            title="Frontend"
            aria-label="Afficher logs frontend"
            @click="logTab = 'frontend'"
          >
            <i class="fa-solid fa-desktop"></i>
          </button>
          <button
            class="icon-btn"
            title="Backend"
            aria-label="Afficher logs backend"
            @click="logTab = 'backend'"
          >
            <i class="fa-solid fa-server"></i>
          </button>
          <button
            class="icon-btn"
            title="Uvicorn"
            aria-label="Afficher logs uvicorn"
            @click="logTab = 'uvicorn'"
          >
            <i class="fa-solid fa-terminal"></i>
          </button>
          <button
            class="icon-btn"
            title="Erreurs"
            aria-label="Afficher les erreurs uniquement"
            @click="logTab = 'errors'"
          >
            <i class="fa-solid fa-triangle-exclamation"></i>
          </button>
          <button
            class="icon-btn"
            title="Rafraichir"
            aria-label="Rafraîchir les logs"
            @click="loadLogs"
          >
            <i class="fa-solid fa-rotate"></i>
          </button>
          <button
            class="icon-btn"
            title="Copier logs"
            aria-label="Copier les logs"
            @click="copyLogs"
          >
            <i class="fa-solid fa-copy"></i>
          </button>
        </div>
      </div>
    </div>
    <PriceHistoryHover
      :visible="historyPopupVisible"
      :position="historyPopup.position"
      :history="historyPopup.history"
      :current-price="historyPopup.currentPrice"
      :min-price="historyPopup.minPrice"
      :max-price="historyPopup.maxPrice"
      :delta="historyPopup.delta"
      @mouseenter="handlePopupEnter"
      @mouseleave="handlePopupLeave"
    />
  </div>
</template>

<script setup lang="ts">
import { computed, ref, watch, onMounted, onBeforeUnmount, nextTick } from "vue";
import { getStoreLogo } from "@/utils/storeLogos";
import PriceHistoryHover from "@/components/PriceHistoryHover.vue";
import PriceHistoryChart from "@/components/PriceHistoryChart.vue";
import MiniLineChart from "@/components/MiniLineChart.vue";
import { computeFloatingPosition } from "@/utils/computeFloatingPosition";

const theme = ref("gruvbox-dark");
const density = ref("dense");
const viewMode = ref("cards");
const layoutMode = ref("auto");
const compareMode = ref(false);
const settingsOpen = ref(false);
const addProductOpen = ref(false);
const logsOpen = ref(false);
const logTab = ref("frontend");
const searchQuery = ref("");
const DEFAULT_CARD_RATIO = 1;
const CARD_RATIO_PRESETS = [
  { id: "compact", label: "Compact", value: 0.85 },
  { id: "standard", label: "Standard", value: 1 },
  { id: "etendu", label: "Étendu", value: 1.2 },
];
const CARD_COLUMNS = 3;
const DEFAULT_IMAGE_HEIGHT = 160;
const CARD_HISTORY_LIMIT = 12;
const CHART_PERIOD_OPTIONS = [
  { value: 30, label: "30 jours" },
  { value: 90, label: "90 jours" },
  { value: 180, label: "180 jours" },
];
const DEFAULT_LOG_DURATION = 2500;
const DEFAULT_TREND_DOWN = -0.5;
const DEFAULT_TREND_UP = 0.5;
const IMAGE_MODES = [
  { value: "contain", label: "Contain (par défaut)" },
  { value: "cover", label: "Cover" },
];
const LOG_ICONS = {
  info: "▶",
  success: "✔",
  warn: "⚠",
  error: "✖",
};

type ScrapeLogEntry = {
  id: string;
  time: string;
  level: "info" | "success" | "warn" | "error";
  text: string;
};

const settings = ref({
  apiBase: "/api",
  apiToken: "",
  refreshMinutes: 60,
  pageSize: 24,
  preferWebp: true,
  iconSize: "md",
  showDebug: false,
  enableAnimations: true,
  fontTitle: "space-mono",
  fontBody: "jetbrains",
  fontMono: "jetbrains",
  fontSize: 16,
  showHistoryHover: true,
  cardRatio: DEFAULT_CARD_RATIO,
  cardImageHeight: DEFAULT_IMAGE_HEIGHT,
  chartPeriod: CHART_PERIOD_OPTIONS[0].value,
  imageMode: "contain",
  logDuration: DEFAULT_LOG_DURATION,
  trendThresholdDown: DEFAULT_TREND_DOWN,
  trendThresholdUp: DEFAULT_TREND_UP,
});

const STORE_ICON_KEY = "pw_store_icon_size";
const DEFAULT_STORE_ICON = 40;
const storeIconSize = ref(DEFAULT_STORE_ICON);
const storeIconDisplay = computed(() => `${storeIconSize.value}px`);
const scrapeLogRef = ref<HTMLElement | null>(null);
const scrapeLog = ref({
  visible: false,
  messages: [] as ScrapeLogEntry[],
  hideTimer: null as number | null,
  animating: false,
});

const filters = ref({
  store: "all",
  stock: "all",
  priceMin: null,
  priceMax: null,
  sort: "updated",
  sortOrder: "desc",
});

const apiState = ref({
  loading: false,
  error: "",
});

const apiHealth = ref({
  db: false,
  redis: false,
});

const frontendVersion = "0.1.0";
const backendVersion = ref("n/a");

const defaultStores = [
  { id: "amazon", name: "Amazon", domain: "amazon.fr" },
  { id: "cdiscount", name: "Cdiscount", domain: "cdiscount.com" },
  { id: "backmarket", name: "Backmarket", domain: "backmarket.fr" },
];

const DETAIL_PERIODS = [
  { id: "7", label: "7j", points: 10, days: 7 },
  { id: "30", label: "30j", points: 24, days: 30 },
  { id: "90", label: "90j", points: 40, days: 90 },
  { id: "max", label: "Max", points: 60, days: 365 },
];
const DEFAULT_HISTORY_PERIOD = "30";
const DEFAULT_DETAIL_TAB = "description";
const detailHistoryPeriod = ref(DEFAULT_HISTORY_PERIOD);
const detailTab = ref(DEFAULT_DETAIL_TAB);
const detailModalRef = ref<HTMLElement | null>(null);
const lastFocusedElement = ref<HTMLElement | null>(null);
let modalKeyHandlerAttached = false;

const stores = ref([...defaultStores]);

const storeLookup = computed(() =>
  Object.fromEntries(stores.value.map((store) => [store.id, store.name]))
);

const products = ref([]);
const productMeta = ref({});
const compareIds = ref([]);
const activeProductId = ref(null);
const storeDraftName = ref("");
const storeDraftDomain = ref("");
const addProductDraft = ref({
  url: "",
  usePlaywright: false,
});

const addProductState = ref({
  loading: false,
  error: "",
  snapshot: null,
});

const addProductSelectedImages = ref([]);

const logEntries = ref([]);
const backendLogs = ref([]);
const uvicornLogs = ref([]);

const fontPresets = [
  {
    id: "jetbrains",
    label: "JetBrains Mono",
    stack: "\"JetBrains Mono\", \"Fira Code\", \"IBM Plex Mono\", \"SFMono-Regular\", Menlo, monospace",
  },
  {
    id: "space-mono",
    label: "Space Mono",
    stack: "\"Space Mono\", \"JetBrains Mono\", \"Fira Code\", monospace",
  },
  {
    id: "ibm-plex",
    label: "IBM Plex Mono",
    stack: "\"IBM Plex Mono\", \"JetBrains Mono\", monospace",
  },
];

const placeholderImage =
  "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='240' height='240'><rect width='240' height='240' fill='%233c3836'/><text x='50%25' y='50%25' dominant-baseline='middle' text-anchor='middle' fill='%23a89984' font-size='14'>No image</text></svg>";

const themeClass = computed(() => `theme-${theme.value}`);
const densityClass = computed(() => `density-${density.value}`);
const layoutClass = computed(() => `layout-${layoutMode.value}`);

const rootStyle = computed(() => {
  const findStack = (id) => fontPresets.find((font) => font.id === id)?.stack;
  return {
    "--font-title": findStack(settings.value.fontTitle) || fontPresets[0].stack,
    "--font-body": findStack(settings.value.fontBody) || fontPresets[0].stack,
    "--font-mono": findStack(settings.value.fontMono) || fontPresets[0].stack,
    "--font-size": `${settings.value.fontSize || 16}px`,
  };
});

const statusDisplayMap = {
  in_stock: "En stock",
  out_of_stock: "Rupture",
  unknown: "Inconnu",
  error: "Erreur scrape",
  in_stocked: "En stock",
};

const historyPopup = ref({
  visible: false,
  position: { top: 0, left: 0 },
  history: [],
  productId: null,
  currentPrice: 0,
  minPrice: 0,
  maxPrice: 0,
  delta: 0,
});
const hoveredProductId = ref<string | null>(null);
const hoverTimeout = ref<number | null>(null);
const isHoveringPopup = ref(false);

const filteredProducts = computed(() => {
  return products.value
    .filter((product) => {
      if (filters.value.store !== "all" && product.storeId !== filters.value.store) {
        return false;
      }
      if (filters.value.stock !== "all" && product.stockStatus !== filters.value.stock) {
        return false;
      }
      if (filters.value.priceMin) {
        if (product.price === null || product.price === undefined) {
          return false;
        }
        if (product.price < filters.value.priceMin) {
          return false;
        }
      }
      if (filters.value.priceMax) {
        if (product.price === null || product.price === undefined) {
          return false;
        }
        if (product.price > filters.value.priceMax) {
          return false;
        }
      }
      if (searchQuery.value && !product.title.toLowerCase().includes(searchQuery.value.toLowerCase())) {
        return false;
      }
      return true;
    })
    .sort((a, b) => {
      const order = filters.value.sortOrder === "asc" ? 1 : -1;
      if (filters.value.sort === "price") {
        return order * ((a.price ?? Number.MAX_SAFE_INTEGER) - (b.price ?? Number.MAX_SAFE_INTEGER));
      }
      if (filters.value.sort === "delta") {
        return order * (a.delta - b.delta);
      }
      if (filters.value.sort === "stock") {
        return order * a.stockStatus.localeCompare(b.stockStatus);
      }
      return order * a.updatedAt.localeCompare(b.updatedAt);
    });
});

const activeProduct = computed(() =>
  products.value.find((product) => product.id === activeProductId.value)
);

const historyPopupVisible = computed(
  () => settings.value.showHistoryHover && historyPopup.value.visible
);

const detailHasDescription = computed(() => Boolean(activeProduct.value?.description?.trim()));
const detailHasSpecs = computed(() => {
  const specs = activeProduct.value?.specs;
  return specs ? Object.keys(specs).length > 0 : false;
});
const detailSpecEntries = computed(() => {
  const specs = activeProduct.value?.specs;
  return specs ? Object.entries(specs) : [];
});

const detailHistory = computed(() => {
  const period = DETAIL_PERIODS.find((item) => item.id === detailHistoryPeriod.value) ?? DETAIL_PERIODS[1];
  const rawHistory = selectHistoryEntries(activeProduct.value?.history ?? [], period.points, period.days ?? CHART_PERIOD_OPTIONS[1].value);
  const prices = rawHistory.map((entry) => entry.price);
  const currentPrice = prices.length ? prices[prices.length - 1] : Number(activeProduct.value?.price ?? NaN);
  const minPrice = prices.length ? Math.min(...prices) : currentPrice;
  const maxPrice = prices.length ? Math.max(...prices) : currentPrice;
  const firstPrice = prices.length ? prices[0] : currentPrice;
  const delta = Number.isFinite(firstPrice) && firstPrice > 0 ? ((currentPrice - firstPrice) / firstPrice) * 100 : 0;
  return {
    history: prices,
    minPrice,
    maxPrice,
    currentPrice: Number.isFinite(currentPrice) ? currentPrice : 0,
    delta,
  };
});

const detailDeltaLabel = computed(() => formatCardDelta(detailHistory.value.delta));

const detailHistorySummary = computed(() => ({
  current: detailHistory.value.currentPrice,
  min: detailHistory.value.minPrice,
  max: detailHistory.value.maxPrice,
  delta: detailHistory.value.delta,
}));

const compareProducts = computed(() =>
  products.value.filter((product) => compareIds.value.includes(product.id))
);

const totalProducts = computed(() => products.value.length);

const activeFilterChips = computed(() => {
  const chips = [];
  const storeValue = filters.value.store;
  if (storeValue && storeValue !== "all") {
    chips.push({
      key: "store",
      label: `Store: ${storeLabel(storeValue)}`,
      tooltip: "Retirer le filtre store",
    });
  }
  const stockValue = filters.value.stock;
  if (stockValue && stockValue !== "all") {
    const translation = {
      in_stock: "En stock",
      out_of_stock: "Rupture",
      unknown: "Inconnu",
    };
    chips.push({
      key: "stock",
      label: `Stock: ${translation[stockValue] ?? stockValue}`,
      tooltip: "Retirer le filtre stock",
    });
  }
  if (filters.value.priceMin) {
    chips.push({
      key: "priceMin",
      label: `Min: ${filters.value.priceMin}`,
      tooltip: "Retirer le minimum",
    });
  }
  if (filters.value.priceMax) {
    chips.push({
      key: "priceMax",
      label: `Max: ${filters.value.priceMax}`,
      tooltip: "Retirer le maximum",
    });
  }
  if (searchQuery.value.trim()) {
    chips.push({
      key: "search",
      label: `Recherche: ${searchQuery.value.trim()}`,
      tooltip: "Retirer la recherche",
    });
  }
  return chips;
});

const cardRatioLabel = computed(() => {
  const ratio = Number(settings.value.cardRatio ?? DEFAULT_CARD_RATIO);
  if (!Number.isFinite(ratio)) {
    return `Standard (${DEFAULT_CARD_RATIO.toFixed(2)})`;
  }
  const preset = CARD_RATIO_PRESETS.find((item) => Math.abs(item.value - ratio) < 0.03);
  const label = preset ? preset.label : "Personnalisé";
  return `${label} • ${ratio.toFixed(2)}`;
});

const cardColumnsLabel = computed(() => `${CARD_COLUMNS} colonnes (Bureau)`);
const cardImageHeightLabel = computed(() => `${Math.round(settings.value.cardImageHeight ?? DEFAULT_IMAGE_HEIGHT)}px`);
const chartPeriodLabel = computed(() => {
  const period = Number(settings.value.chartPeriod ?? CHART_PERIOD_OPTIONS[0].value);
  const found = CHART_PERIOD_OPTIONS.find((option) => option.value === period);
  if (found) {
    return found.label;
  }
  return `${period} jours`;
});

const productHistorySnapshots = computed(() => {
  const map = new Map();
  products.value.forEach((product) => {
    map.set(product.id, getCardHistorySnapshot(product));
  });
  return map;
});

const addProductImagesCount = computed(() => {
  if (!addProductState.value.snapshot || !addProductState.value.snapshot.images) {
    return 0;
  }
  return addProductState.value.snapshot.images.length;
});

const addProductImages = computed(() => {
  if (!addProductState.value.snapshot || !Array.isArray(addProductState.value.snapshot.images)) {
    return [];
  }
  return addProductState.value.snapshot.images.filter(Boolean);
});

const addProductImagePreview = computed(() => addProductImages.value[0] || placeholderImage);

const addProductImageThumbs = computed(() => addProductImages.value.slice(0, 4));

const selectedImagesSet = computed(() => new Set(addProductSelectedImages.value));

function toggleAddProductImage(index) {
  const selected = addProductSelectedImages.value.includes(index);
  if (selected) {
    addProductSelectedImages.value = addProductSelectedImages.value.filter((item) => item !== index);
    return;
  }
  addProductSelectedImages.value = [...addProductSelectedImages.value, index];
}

const addProductSpecsCount = computed(() => {
  if (!addProductState.value.snapshot || !addProductState.value.snapshot.specs) {
    return 0;
  }
  return Object.keys(addProductState.value.snapshot.specs).length;
});

const addProductSpecEntries = computed(() => {
  if (!addProductState.value.snapshot || !addProductState.value.snapshot.specs) {
    return [];
  }
  return Object.entries(addProductState.value.snapshot.specs).slice(0, 10);
});

if (typeof window !== "undefined") {
  const storedIcon = Number(window.localStorage.getItem(STORE_ICON_KEY));
  if (Number.isFinite(storedIcon)) {
    storeIconSize.value = Math.max(16, Math.min(40, storedIcon));
  }
}

const apiStatus = computed(() => {
  if (apiState.value.loading) {
    return "API...";
  }
  if (apiState.value.error) {
    return "API ERR";
  }
  return "API OK";
});

const filteredLogs = computed(() => {
  if (logTab.value === "backend") {
    return backendLogs.value;
  }
  if (logTab.value === "uvicorn") {
    return uvicornLogs.value;
  }
  if (logTab.value === "errors") {
    return logEntries.value.filter((entry) => entry.level === "ERROR");
  }
  return logEntries.value;
});

function formatPrice(value, currency = "EUR") {
  if (value === null || value === undefined) {
    return "n/a";
  }
  return `${value} ${currency}`;
}

function formatShortPrice(value, currency = "EUR") {
  if (value === null || value === undefined) {
    return "n/a";
  }
  return new Intl.NumberFormat("fr-FR", {
    style: "currency",
    currency,
    minimumFractionDigits: 2,
  }).format(value);
}

function formatCardDelta(delta) {
  if (delta === null || delta === undefined || Number.isNaN(delta)) {
    return "—";
  }
  const numeric = Number(delta);
  if (numeric === 0) {
    return "—";
  }
  const arrow = numeric > 0 ? "▲" : "▼";
  const absolute = Math.abs(numeric).toFixed(1).replace(/\.0$/, "");
  return `${arrow} ${absolute}%`;
}

function statusLabel(status) {
  if (!status) {
    return statusDisplayMap.unknown;
  }
  return statusDisplayMap[status] || status.replaceAll("_", " ").replace(/\b\w/g, (chr) => chr.toUpperCase());
}

function statusClass(status) {
  if (!status) {
    return "status-unknown";
  }
  return `status-${status.replaceAll(/[^a-zA-Z0-9]/g, "-")}`;
}

function formatDiscount(amount, percent) {
  if (amount === null || amount === undefined || percent === null || percent === undefined) {
    return "n/a";
  }
  return `${amount.toFixed(2)} (-${percent.toFixed(1)}%)`;
}

function storeLabel(storeId) {
  return storeLookup.value[storeId] || storeId || "Inconnu";
}

function toggleSettings() {
  settingsOpen.value = !settingsOpen.value;
}

function toggleLogs() {
  logsOpen.value = !logsOpen.value;
  if (logsOpen.value) {
    loadLogs();
  }
}

function openAddProduct() {
  addProductDraft.value = {
    url: "",
    usePlaywright: false,
  };
  addProductState.value = {
    loading: false,
    error: "",
    snapshot: null,
  };
  addProductOpen.value = true;
}

function closeAddProduct() {
  addProductOpen.value = false;
}

function reloadPage() {
  if (typeof window !== "undefined") {
    window.location.reload();
  }
}

function clearCacheAndReload() {
  if (typeof window !== "undefined") {
    localStorage.removeItem("pw_webui_settings");
    localStorage.removeItem("pw_webui_product_meta");
    localStorage.removeItem("pw_webui_stores");
    window.location.reload();
  }
}

function applySettingsAndReload() {
  persistSettings();
  persistStores();
  persistProductMeta();
  reloadPage();
}

function cycleTheme() {
  const themes = ["gruvbox-dark", "gruvbox-light", "monokai-dark", "monokai-light"];
  const index = themes.indexOf(theme.value);
  theme.value = themes[(index + 1) % themes.length];
}

function openProductDetails(productId) {
  activeProductId.value = productId;
}

function selectProduct(productId) {
  openProductDetails(productId);
}

function toggleCompare(productId) {
  if (compareIds.value.includes(productId)) {
    compareIds.value = compareIds.value.filter((id) => id !== productId);
    return;
  }
  if (compareIds.value.length >= 4) {
    return;
  }
  compareIds.value.push(productId);
}

function removeFilter(key) {
  if (key === "store") {
    filters.value.store = "all";
    return;
  }
  if (key === "stock") {
    filters.value.stock = "all";
    return;
  }
  if (key === "priceMin") {
    filters.value.priceMin = null;
    return;
  }
  if (key === "priceMax") {
    filters.value.priceMax = null;
    return;
  }
  if (key === "search") {
    searchQuery.value = "";
  }
}

function resetFilters() {
  filters.value.store = "all";
  filters.value.stock = "all";
  filters.value.priceMin = null;
  filters.value.priceMax = null;
  filters.value.sort = "updated";
  filters.value.sortOrder = "desc";
  searchQuery.value = "";
}

function addStore() {
  if (!storeDraftName.value.trim()) {
    return;
  }
  const id = storeDraftName.value.toLowerCase().replace(/\s+/g, "-");
  if (stores.value.some((store) => store.id === id)) {
    return;
  }
  stores.value.push({
    id,
    name: storeDraftName.value.trim(),
    domain: storeDraftDomain.value.trim() || "custom",
  });
  storeDraftName.value = "";
  storeDraftDomain.value = "";
}

function removeStore(storeId) {
  stores.value = stores.value.filter((store) => store.id !== storeId);
}

function mergeStoresFromProducts(items) {
  const known = new Set(stores.value.map((store) => store.id));
  const additions = [];
  items.forEach((product) => {
    if (product.storeId && !known.has(product.storeId)) {
      known.add(product.storeId);
      additions.push({
        id: product.storeId,
        name: product.storeId.toUpperCase(),
        domain: "custom",
      });
    }
  });
  if (additions.length) {
    stores.value = [...stores.value, ...additions];
  }
}

function getModalFocusableElements() {
  if (!detailModalRef.value) {
    return [];
  }
  const elements = detailModalRef.value.querySelectorAll<HTMLElement>(
    'button:not([disabled]), [href], input:not([disabled]), textarea:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])'
  );
  return Array.from(elements).filter((element) => element.offsetParent !== null || element === document.activeElement);
}

function focusFirstModalElement() {
  const focusables = getModalFocusableElements();
  focusables[0]?.focus();
}

function handleDetailKeydown(event: KeyboardEvent) {
  if (!activeProductId.value) {
    return;
  }
  if (event.key === "Escape") {
    activeProductId.value = null;
    event.preventDefault();
    return;
  }
  if (event.key !== "Tab" || !detailModalRef.value) {
    return;
  }
  const focusables = getModalFocusableElements();
  if (!focusables.length) {
    return;
  }
  const firstElement = focusables[0];
  const lastElement = focusables[focusables.length - 1];
  if (event.shiftKey && document.activeElement === firstElement) {
    event.preventDefault();
    lastElement.focus();
  } else if (!event.shiftKey && document.activeElement === lastElement) {
    event.preventDefault();
    firstElement.focus();
  }
}

function attachModalKeyHandler() {
  if (typeof document === "undefined" || modalKeyHandlerAttached) {
    return;
  }
  document.addEventListener("keydown", handleDetailKeydown);
  modalKeyHandlerAttached = true;
}

function detachModalKeyHandler() {
  if (typeof document === "undefined" || !modalKeyHandlerAttached) {
    return;
  }
  document.removeEventListener("keydown", handleDetailKeydown);
  modalKeyHandlerAttached = false;
}

watch(
  () => activeProduct.value,
  (product) => {
    if (product) {
      detailTab.value = detailHasDescription.value
        ? "description"
        : detailHasSpecs.value
        ? "specs"
        : DEFAULT_DETAIL_TAB;
      detailHistoryPeriod.value = DEFAULT_HISTORY_PERIOD;
    }
  }
);

watch(
  () => activeProductId.value,
  (value) => {
    if (value) {
      if (typeof document !== "undefined") {
        lastFocusedElement.value = document.activeElement as HTMLElement;
      }
      attachModalKeyHandler();
      nextTick(() => {
        focusFirstModalElement();
      });
    } else {
      detachModalKeyHandler();
      nextTick(() => {
        lastFocusedElement.value?.focus();
      });
    }
  }
);

onBeforeUnmount(() => {
  detachModalKeyHandler();
});

const DAY_MS = 86_400_000;

function formatHistoryDateLabel(value: number | string) {
  let timestamp: number | null = null;
  if (typeof value === "number") {
    timestamp = value;
  } else if (typeof value === "string") {
    const parsed = Date.parse(value);
    if (!Number.isNaN(parsed)) {
      timestamp = parsed;
    }
  }
  if (timestamp === null) {
    return typeof value === "string" ? value : "";
  }
  const diff = Math.max(0, Math.round((Date.now() - timestamp) / DAY_MS));
  if (diff === 0) {
    return "J";
  }
  return `J-${diff}`;
}

function parseHistoryTimestamp(raw) {
  if (!raw) {
    return null;
  }
  if (typeof raw === "number") {
    return raw;
  }
  const parsed = Date.parse(raw);
  return Number.isNaN(parsed) ? null : parsed;
}

type NormalizedHistoryEntry = {
  price: number;
  timestamp: number;
};

function normalizeHistoryEntries(rawHistory: unknown[] = []): NormalizedHistoryEntry[] {
  if (!Array.isArray(rawHistory)) {
    return [];
  }
  return (
    rawHistory
      .map((entry) => {
        if (!entry || typeof entry !== "object") {
          return null;
        }
        const record = entry as Record<string, unknown>;
        const price = Number(record.price);
        if (!Number.isFinite(price)) {
          return null;
        }
        const timestamp = parseHistoryTimestamp(record.fetched_at ?? record.fetchedAt ?? record.timestamp ?? record.time);
        if (!Number.isFinite(timestamp ?? NaN)) {
          return null;
        }
        return { price, timestamp };
      })
      .filter((item): item is NormalizedHistoryEntry => Boolean(item))
      .sort((a, b) => a.timestamp - b.timestamp)
  );
}

function selectHistoryEntries(
  rawHistory: unknown[] = [],
  limit = CARD_HISTORY_LIMIT,
  periodDays = CHART_PERIOD_OPTIONS[0].value
): NormalizedHistoryEntry[] {
  if (!Array.isArray(rawHistory) || limit <= 0) {
    return [];
  }
  const normalized = normalizeHistoryEntries(rawHistory);
  if (!normalized.length) {
    return [];
  }
  const cutoff = Date.now() - Math.max(0, periodDays) * DAY_MS;
  const filtered = normalized.filter((entry) => entry.timestamp >= cutoff);
  const candidates = filtered.length ? filtered : normalized;
  if (candidates.length <= limit) {
    return candidates;
  }
  return candidates.slice(-limit);
}

function entriesToChartPoints(entries: NormalizedHistoryEntry[]) {
  return entries.map((entry) => ({
    t: entry.timestamp,
    v: entry.price,
  }));
}

type CardHistorySnapshot = {
  points: Array<{ t: number; v: number }>;
  min: number | null;
  max: number | null;
  delta: number | null;
  trendIcon: string;
  trendLabel: string;
  trendDeltaLabel: string;
  trendColor: string;
  lastTimestamp: number | null;
};

function getTrendColor(delta: number): string {
  if (!Number.isFinite(delta)) {
    return "var(--muted)";
  }
  const down = Number(settings.value.trendThresholdDown ?? DEFAULT_TREND_DOWN);
  const up = Number(settings.value.trendThresholdUp ?? DEFAULT_TREND_UP);
  if (delta <= down) {
    return "var(--success)";
  }
  if (delta >= up) {
    return "var(--danger)";
  }
  return "var(--warning)";
}

function getCardHistorySnapshot(product: any): CardHistorySnapshot {
  const periodDays = Number(settings.value.chartPeriod ?? CHART_PERIOD_OPTIONS[0].value);
  const entries = selectHistoryEntries(product.history ?? [], CARD_HISTORY_LIMIT, periodDays);
  const points = entriesToChartPoints(entries);
  const prices = entries.map((entry) => entry.price);
  const min = prices.length ? Math.min(...prices) : null;
  const max = prices.length ? Math.max(...prices) : null;
  const first = prices.length ? prices[0] : Number(product.price ?? NaN);
  const last = prices.length ? prices[prices.length - 1] : Number(product.price ?? NaN);
  const delta =
    Number.isFinite(first) && first > 0 && Number.isFinite(last)
      ? ((last - first) / first) * 100
      : NaN;
  const down = Number(settings.value.trendThresholdDown ?? DEFAULT_TREND_DOWN);
  const up = Number(settings.value.trendThresholdUp ?? DEFAULT_TREND_UP);
  const icon = delta <= down ? "↘" : delta >= up ? "↗" : "→";
  const label = delta <= down ? "baisse" : delta >= up ? "hausse" : "stable";
  const deltaLabel = Number.isFinite(delta) ? `${delta >= 0 ? "+" : ""}${delta.toFixed(1)}%` : "—";
  const lastTimestamp =
    entries.length > 0
      ? entries[entries.length - 1].timestamp
      : parseHistoryTimestamp(product.updatedAt) ?? null;
  return {
    points,
    min,
    max,
    delta: Number.isFinite(delta) ? delta : null,
    trendIcon: icon,
    trendLabel: label,
    trendDeltaLabel: deltaLabel,
    trendColor: getTrendColor(Number.isFinite(delta) ? delta : NaN),
    lastTimestamp,
  };
}

function formatRelativeTimeAgo(timestamp: number | null) {
  if (!Number.isFinite(timestamp ?? NaN)) {
    return "à l’instant";
  }
  const diff = Date.now() - timestamp;
  if (diff < 60_000) {
    return "à l’instant";
  }
  const minutes = Math.floor(diff / 60_000);
  if (minutes < 60) {
    return `il y a ${minutes} min`;
  }
  const hours = Math.floor(diff / 3_600_000);
  if (hours < 24) {
    return `il y a ${hours} h`;
  }
  const days = Math.floor(diff / 86_400_000);
  return `il y a ${days} j`;
}

function updateHistoryPopupPosition(target: HTMLElement | null) {
  if (!target || typeof window === "undefined") {
    return;
  }
  const rect = target.getBoundingClientRect();
  const position = computeFloatingPosition({
    rect,
    popupWidth: 280,
    popupHeight: 140,
    viewportWidth: window.innerWidth,
    viewportHeight: window.innerHeight,
  });
  historyPopup.value.position = position;
}

function showHistoryPopup(product: any, target: HTMLElement | null) {
  if (!target) {
    return;
  }
  const periodDays = Number(settings.value.chartPeriod ?? CHART_PERIOD_OPTIONS[0].value);
  const historyEntries = selectHistoryEntries(product.history ?? [], CARD_HISTORY_LIMIT, periodDays);
  if (!historyEntries.length) {
    hideHistoryPopup();
    return;
  }
  const prices = historyEntries.map((entry) => entry.price);
  const currentPrice = prices[prices.length - 1];
  const minPrice = Math.min(...prices);
  const maxPrice = Math.max(...prices);
  const firstPrice = prices[0];
  const delta =
    Number.isFinite(firstPrice) && firstPrice > 0 ? ((currentPrice - firstPrice) / firstPrice) * 100 : 0;
  historyPopup.value.history = prices;
  historyPopup.value.currentPrice = Number.isFinite(currentPrice) ? currentPrice : 0;
  historyPopup.value.minPrice = minPrice;
  historyPopup.value.maxPrice = maxPrice;
  historyPopup.value.delta = delta;
  historyPopup.value.productId = product.id;
  historyPopup.value.visible = true;
  nextTick(() => updateHistoryPopupPosition(target));
  isHoveringPopup.value = true;
  if (hoverTimeout.value) {
    window.clearTimeout(hoverTimeout.value);
    hoverTimeout.value = null;
  }
}

function hideHistoryPopup() {
  if (isHoveringPopup.value) {
    return;
  }
  historyPopup.value.visible = false;
}

function scheduleHideHistory(delay = 120) {
  if (hoverTimeout.value) {
    window.clearTimeout(hoverTimeout.value);
  }
  hoverTimeout.value = window.setTimeout(() => {
    if (!isHoveringPopup.value) {
      hideHistoryPopup();
    }
  }, delay);
}

function handleCardHover(product: any, event: MouseEvent | FocusEvent) {
  const target = event.currentTarget as HTMLElement;
  showHistoryPopup(product, target);
}

function handlePopupEnter() {
  isHoveringPopup.value = true;
  if (hoverTimeout.value) {
    window.clearTimeout(hoverTimeout.value);
    hoverTimeout.value = null;
  }
}

function handlePopupLeave() {
  isHoveringPopup.value = false;
  scheduleHideHistory(0);
}

function applyStoreIconSize(value) {
  if (typeof document === "undefined") {
    return;
  }
  const size = Math.max(16, Math.min(40, value));
  document.documentElement.style.setProperty("--pw-store-icon", `${size}px`);
}

function applyCardRatio(value) {
  if (typeof document === "undefined") {
    return;
  }
  const numeric = Number(value);
  const ratio = Number.isFinite(numeric) ? numeric : DEFAULT_CARD_RATIO;
  const clamped = Math.min(1.3, Math.max(0.75, ratio));
  const mobileFactor = Math.min(clamped, 1.1);
  document.documentElement.style.setProperty("--pw-card-height-factor", `${clamped}`);
  document.documentElement.style.setProperty("--pw-card-mobile-height-factor", `${mobileFactor}`);
}

function applyCardImageHeight(value) {
  if (typeof document === "undefined") {
    return;
  }
  const numeric = Number(value);
  const height = Number.isFinite(numeric) ? numeric : DEFAULT_IMAGE_HEIGHT;
  const clamped = Math.min(220, Math.max(120, height));
  document.documentElement.style.setProperty("--pw-card-media-height", `${clamped}px`);
}

function storeLogo(storeId) {
  return getStoreLogo(storeId) || null;
}

function storeInitials(storeId) {
  const label = storeLabel(storeId);
  if (!label) {
    return "NA";
  }
  return label
    .split(" ")
    .map((part) => part.charAt(0).toUpperCase())
    .join("")
    .slice(0, 2);
}

function formatDate(value) {
  if (!value) {
    return "n/a";
  }
  try {
    return new Date(value).toLocaleString("fr-FR");
  } catch {
    return value;
  }
}

function resolveApiUrl(path) {
  const base = settings.value.apiBase || "/api";
  if (base.startsWith("http")) {
    return new URL(base.replace(/\/$/, "") + path).toString();
  }
  const basePath = base.endsWith("/") ? base.slice(0, -1) : base;
  return `${basePath}${path}`;
}

function apiHeaders() {
  const headers = {
    "Content-Type": "application/json",
  };
  if (settings.value.apiToken) {
    headers.Authorization = `Bearer ${settings.value.apiToken}`;
  }
  return headers;
}

function mapProduct(apiProduct) {
  const meta = productMeta.value[apiProduct.id] || {};
  const imageUrl = Array.isArray(apiProduct.images) && apiProduct.images.length > 0
    ? apiProduct.images[0]
    : placeholderImage;
  const history = Array.isArray(apiProduct.history) ? apiProduct.history : [];
  return {
    id: apiProduct.id,
    title: apiProduct.title || apiProduct.reference || "Sans titre",
    storeId: apiProduct.source || "unknown",
    reference: apiProduct.reference || "",
    price: apiProduct.latest_price ?? null,
    delta: 0,
    stockStatus: apiProduct.latest_stock_status || "unknown",
    updatedAt: apiProduct.last_updated_at || "",
    updatedAtLabel: formatDate(apiProduct.last_updated_at),
    refreshHours: meta.refreshHours ?? 24,
    url: apiProduct.url || "",
    category: apiProduct.category || "",
    currency: apiProduct.currency || "EUR",
    description: apiProduct.description || "",
    specs: apiProduct.specs || {},
    msrp: apiProduct.msrp ?? null,
    discountAmount: apiProduct.discount_amount ?? null,
    discountPercent: apiProduct.discount_percent ?? null,
    imageWebp: imageUrl,
    imageJpg: imageUrl,
    history,
  };
}

async function fetchProducts() {
  apiState.value.loading = true;
  apiState.value.error = "";
  try {
    const url = new URL(resolveApiUrl("/products"), window.location.origin);
    if (filters.value.store !== "all") {
      url.searchParams.set("source", filters.value.store);
    }
    if (filters.value.priceMin) {
      url.searchParams.set("price_min", String(filters.value.priceMin));
    }
    if (filters.value.priceMax) {
      url.searchParams.set("price_max", String(filters.value.priceMax));
    }
    if (filters.value.stock !== "all") {
      url.searchParams.set("stock_status", filters.value.stock);
    }
    if (settings.value.pageSize) {
      url.searchParams.set("limit", String(settings.value.pageSize));
    }
    const response = await fetch(url.toString(), {
      headers: apiHeaders(),
    });
    if (!response.ok) {
      throw new Error(`API ${response.status}`);
    }
    const payload = await response.json();
    const mapped = payload.map(mapProduct);
    products.value = mapped;
    mergeStoresFromProducts(mapped);
    if (activeProductId.value && !products.value.find((item) => item.id === activeProductId.value)) {
      activeProductId.value = products.value[0]?.id ?? null;
    }
  } catch (error) {
    apiState.value.error = error instanceof Error ? error.message : "Erreur API";
    addLog("ERROR", "frontend", apiState.value.error);
  } finally {
    apiState.value.loading = false;
  }
}

async function saveActiveProduct() {
  if (!activeProduct.value) {
    return;
  }
  const payload = {
    url: activeProduct.value.url,
    title: activeProduct.value.title,
    category: activeProduct.value.category,
    currency: activeProduct.value.currency,
  };
  try {
    const response = await fetch(resolveApiUrl(`/products/${activeProduct.value.id}`), {
      method: "PATCH",
      headers: apiHeaders(),
      body: JSON.stringify(payload),
    });
    if (!response.ok) {
      throw new Error(`API ${response.status}`);
    }
    await fetchProducts();
  } catch (error) {
    apiState.value.error = error instanceof Error ? error.message : "Erreur API";
    addLog("ERROR", "frontend", apiState.value.error);
  }
}

async function refreshProduct(productId) {
  const product = products.value.find((item) => item.id === productId);
  if (!product || !product.url) {
    addLog("WARN", "frontend", "Impossible de relancer le scrap: URL manquante");
    return;
  }
  const startTime = Date.now();
  pushScrapeLog("info", "Démarrage scraping Amazon.fr");
  pushScrapeLog("info", "Chargement page produit");
  try {
    const response = await fetch(resolveApiUrl("/enqueue"), {
      method: "POST",
      headers: apiHeaders(),
      body: JSON.stringify({
        url: product.url,
        save_db: true,
      }),
    });
    if (!response.ok) {
      throw new Error(`API ${response.status}`);
    }
    addLog("INFO", "backend", `Scrap relancé pour ${product.reference || product.title}`);
    await fetchProducts();
    const duration = ((Date.now() - startTime) / 1000).toFixed(1);
    const updated = products.value.find((item) => item.id === productId);
    const priceLabel = updated ? formatShortPrice(updated.price, updated.currency) : "n/a";
    pushScrapeLog("success", `Scraping terminé (${duration}s) • Prix détecté : ${priceLabel}`);
  } catch (error) {
    apiState.value.error = error instanceof Error ? error.message : "Erreur API";
    addLog("ERROR", "frontend", apiState.value.error);
    pushScrapeLog("error", `${apiState.value.error}`);
  }
}

async function deleteProduct(productId) {
  try {
    const response = await fetch(resolveApiUrl(`/products/${productId}`), {
      method: "DELETE",
      headers: apiHeaders(),
    });
    if (!response.ok) {
      throw new Error(`API ${response.status}`);
    }
    if (activeProductId.value === productId) {
      activeProductId.value = null;
    }
    addLog("INFO", "backend", "Produit supprimé");
    await fetchProducts();
  } catch (error) {
    apiState.value.error = error instanceof Error ? error.message : "Erreur API";
    addLog("ERROR", "backend", apiState.value.error);
  }
}

async function enqueueActiveProduct() {
  if (!activeProduct.value) {
    return;
  }
  await refreshProduct(activeProduct.value.id);
}

async function deleteActiveProduct() {
  if (!activeProduct.value) {
    return;
  }
  await deleteProduct(activeProduct.value.id);
}

function openProductUrl(product) {
  if (!product?.url) {
    addLog("WARN", "frontend", "Aucune URL disponible");
    return;
  }
  if (typeof window === "undefined") {
    return;
  }
  window.open(product.url, "_blank", "noopener");
}

async function copyActiveUrl() {
  if (!activeProduct.value?.url) {
    addLog("WARN", "frontend", "Aucune URL à copier");
    return;
  }
  if (typeof navigator === "undefined" || !navigator.clipboard) {
    addLog("WARN", "frontend", "Clipboard non disponible");
    return;
  }
  try {
    await navigator.clipboard.writeText(activeProduct.value.url);
    addLog("INFO", "frontend", "URL copiée");
  } catch (error) {
    addLog("ERROR", "frontend", "Erreur copie de l'URL");
  }
}

async function previewAddProduct() {
  if (!addProductDraft.value.url) {
    return;
  }
  addProductState.value.loading = true;
  addProductState.value.error = "";
  addProductState.value.snapshot = null;
  try {
    const response = await fetch(resolveApiUrl("/scrape/preview"), {
      method: "POST",
      headers: apiHeaders(),
      body: JSON.stringify({
        url: addProductDraft.value.url,
        use_playwright: addProductDraft.value.usePlaywright,
      }),
    });
    if (!response.ok) {
      throw new Error(`API ${response.status}`);
    }
    const payload = await response.json();
    if (!payload.success) {
      addProductState.value.error = payload.error || "Aucune donnee";
      addLog("ERROR", "backend", addProductState.value.error);
      return;
    }
    if (!payload.snapshot) {
      addProductState.value.error = "Aucune donnee";
      addLog("ERROR", "backend", addProductState.value.error);
      return;
    }
    if (!payload.snapshot.title && !payload.snapshot.price) {
      addProductState.value.error = "Donnees incompletes";
    }
    addProductState.value.snapshot = payload.snapshot;
    const imageCount = Array.isArray(payload.snapshot.images) ? payload.snapshot.images.length : 0;
    addProductSelectedImages.value = imageCount ? [0] : [];
    addLog("INFO", "backend", "Preview scraping OK");
  } catch (error) {
    addProductState.value.error = error instanceof Error ? error.message : "Erreur API";
    addLog("ERROR", "backend", addProductState.value.error);
  } finally {
    addProductState.value.loading = false;
  }
}

async function commitAddProduct() {
  if (!addProductState.value.snapshot) {
    return;
  }
  try {
    const response = await fetch(resolveApiUrl("/scrape/commit"), {
      method: "POST",
      headers: apiHeaders(),
      body: JSON.stringify({
        snapshot: addProductState.value.snapshot,
      }),
    });
    if (!response.ok) {
      throw new Error(`API ${response.status}`);
    }
    addProductOpen.value = false;
    await fetchProducts();
    addLog("INFO", "backend", "Produit enregistre");
  } catch (error) {
    addProductState.value.error = error instanceof Error ? error.message : "Erreur API";
    addLog("ERROR", "backend", addProductState.value.error);
  }
}

let refreshTimer: ReturnType<typeof setInterval> | null = null;

function startAutoRefresh() {
  if (refreshTimer) {
    clearInterval(refreshTimer);
    refreshTimer = null;
  }
  const minutes = Number(settings.value.refreshMinutes);
  if (Number.isFinite(minutes) && minutes > 0) {
    refreshTimer = setInterval(fetchProducts, minutes * 60 * 1000);
  }
}

function persistSettings() {
  if (typeof window === "undefined") {
    return;
  }
  const payload = {
    theme: theme.value,
    density: density.value,
    layoutMode: layoutMode.value,
    settings: settings.value,
    viewMode: viewMode.value,
    filters: filters.value,
  };
  localStorage.setItem("pw_webui_settings", JSON.stringify(payload));
}

function loadSettings() {
  if (typeof window === "undefined") {
    return;
  }
  const raw = localStorage.getItem("pw_webui_settings");
  if (!raw) {
    return;
  }
  try {
    const payload = JSON.parse(raw);
    if (payload.theme) {
      theme.value = payload.theme;
    }
    if (payload.density) {
      density.value = payload.density;
    }
    if (payload.layoutMode) {
      layoutMode.value = payload.layoutMode;
    }
    if (payload.settings) {
      settings.value = { ...settings.value, ...payload.settings };
    }
    if (payload.viewMode) {
      viewMode.value = payload.viewMode;
    }
    if (payload.filters) {
      filters.value = { ...filters.value, ...payload.filters };
    }
  } catch {
    return;
  }
}

function persistStores() {
  if (typeof window === "undefined") {
    return;
  }
  localStorage.setItem("pw_webui_stores", JSON.stringify(stores.value));
}

function loadStores() {
  if (typeof window === "undefined") {
    return;
  }
  const raw = localStorage.getItem("pw_webui_stores");
  if (!raw) {
    return;
  }
  try {
    const payload = JSON.parse(raw);
    if (Array.isArray(payload)) {
      stores.value = payload;
    }
  } catch {
    return;
  }
}

function persistProductMeta() {
  if (typeof window === "undefined") {
    return;
  }
  const payload = {};
  products.value.forEach((product) => {
    payload[product.id] = {
      refreshHours: product.refreshHours,
    };
  });
  localStorage.setItem("pw_webui_product_meta", JSON.stringify(payload));
}

async function fetchHealth() {
  try {
    const response = await fetch(resolveApiUrl("/health"), {
      headers: apiHeaders(),
    });
    if (!response.ok) {
      throw new Error(`API ${response.status}`);
    }
    const payload = await response.json();
    apiHealth.value = payload;
  } catch (error) {
    apiHealth.value = { db: false, redis: false };
  }
}

async function fetchVersion() {
  try {
    const response = await fetch(resolveApiUrl("/version"), {
      headers: apiHeaders(),
    });
    if (!response.ok) {
      throw new Error(`API ${response.status}`);
    }
    const payload = await response.json();
    backendVersion.value = payload.api_version || "n/a";
  } catch {
    backendVersion.value = "n/a";
  }
}

async function loadLogs() {
  backendLogs.value = [];
  uvicornLogs.value = [];
  try {
    const url = new URL(resolveApiUrl("/logs"), window.location.origin);
    url.searchParams.set("limit", "10");
    const response = await fetch(url.toString(), { headers: apiHeaders() });
    if (response.ok) {
      const payload = await response.json();
      backendLogs.value = payload.map((entry, index) => ({
        id: `backend-${entry.id}-${index}`,
        time: formatDate(entry.fetched_at),
        level: entry.fetch_status?.toUpperCase() || "INFO",
        source: "backend",
        message: `${entry.source} ${entry.url} (${entry.fetch_method})`,
      }));
    }
    const backendResponse = await fetch(resolveApiUrl("/logs/backend"), {
      headers: apiHeaders(),
    });
    if (backendResponse.ok) {
      const backendPayload = await backendResponse.json();
      const backendEntries = backendPayload.map((entry, index) => ({
        id: `backend-log-${index}`,
        time: formatDate(entry.time),
        level: entry.level,
        source: "backend",
        message: entry.message,
      }));
      backendLogs.value = [...backendEntries, ...backendLogs.value];
    }
    const uvicornResponse = await fetch(resolveApiUrl("/logs/uvicorn"), {
      headers: apiHeaders(),
    });
    if (uvicornResponse.ok) {
      const uvicornPayload = await uvicornResponse.json();
      uvicornLogs.value = uvicornPayload.map((entry, index) => ({
        id: `uvicorn-log-${index}`,
        time: "",
        level: "INFO",
        source: "uvicorn",
        message: entry.line,
      }));
    }
  } catch (error) {
    addLog("ERROR", "backend", "Impossible de charger les logs");
  }
}

async function copyLogs() {
  if (typeof navigator === "undefined" || !navigator.clipboard) {
    addLog("WARN", "frontend", "Clipboard indisponible pour copier les logs");
    return;
  }
  const payload = filteredLogs.value
    .map((entry) => `${entry.time} [${entry.source}] ${entry.level} - ${entry.message}`)
    .join("\n");
  if (!payload) {
    addLog("WARN", "frontend", "Aucun log à copier");
    return;
  }
  try {
    await navigator.clipboard.writeText(payload);
    addLog("INFO", "frontend", "Logs copiés");
  } catch {
    addLog("ERROR", "frontend", "Erreur lors de la copie des logs");
  }
}

function addLog(level, source, message) {
  logEntries.value.unshift({
    id: `${Date.now()}-${Math.random().toString(16).slice(2)}`,
    time: new Date().toLocaleTimeString("fr-FR"),
    level,
    source,
    message,
  });
  if (logEntries.value.length > 200) {
    logEntries.value.pop();
  }
}

function clearScrapeHideTimer() {
  if (typeof window === "undefined") {
    return;
  }
  if (scrapeLog.value.hideTimer) {
    window.clearTimeout(scrapeLog.value.hideTimer);
    scrapeLog.value.hideTimer = null;
  }
}

function scheduleHideScrapeLog() {
  clearScrapeHideTimer();
  if (typeof window === "undefined") {
    return;
  }
  const duration = Math.max(500, Number(settings.value.logDuration ?? DEFAULT_LOG_DURATION));
  scrapeLog.value.hideTimer = window.setTimeout(() => {
    scrapeLog.value.visible = false;
    scrapeLog.value.hideTimer = null;
  }, duration);
}

function pushScrapeLog(level: ScrapeLogEntry["level"], text: string) {
  const timestamp = new Date().toLocaleTimeString("fr-FR");
  const entry: ScrapeLogEntry = {
    id: `${Date.now()}-${Math.random().toString(16).slice(2)}`,
    time: timestamp,
    level,
    text,
  };
  const lean = [...scrapeLog.value.messages.slice(-4), entry];
  scrapeLog.value.messages = lean;
  scrapeLog.value.visible = true;
  nextTick(() => {
    if (scrapeLogRef.value) {
      scrapeLogRef.value.scrollTop = scrapeLogRef.value.scrollHeight;
    }
  });
  scheduleHideScrapeLog();
}

function loadProductMeta() {
  if (typeof window === "undefined") {
    return;
  }
  const raw = localStorage.getItem("pw_webui_product_meta");
  if (!raw) {
    return;
  }
  try {
    const payload = JSON.parse(raw);
    if (payload && typeof payload === "object") {
      productMeta.value = payload;
    }
  } catch {
    return;
  }
}

loadSettings();
loadStores();
loadProductMeta();

watch([theme, density, layoutMode, settings, viewMode, filters], persistSettings, { deep: true });
watch(stores, persistStores, { deep: true });
watch(products, persistProductMeta, { deep: true });
watch(() => settings.value.refreshMinutes, startAutoRefresh);
watch(
  [() => settings.value.apiBase, () => settings.value.apiToken, () => settings.value.pageSize, filters],
  () => {
    fetchProducts();
    fetchHealth();
    fetchVersion();
  },
  { deep: true }
);

watch(
  storeIconSize,
  (value) => {
    applyStoreIconSize(value);
    if (typeof window !== "undefined") {
      window.localStorage.setItem(STORE_ICON_KEY, String(Math.max(16, Math.min(40, value))));
    }
  },
  { immediate: true }
);

watch(
  () => settings.value.cardRatio,
  (value) => {
    applyCardRatio(value);
  },
  { immediate: true }
);

watch(
  () => settings.value.cardImageHeight,
  (value) => {
    applyCardImageHeight(value);
  },
  { immediate: true }
);

onMounted(() => {
  fetchProducts();
  startAutoRefresh();
  fetchHealth();
  fetchVersion();
});

onBeforeUnmount(() => {
  if (refreshTimer) {
    clearInterval(refreshTimer);
  }
  if (hoverTimeout.value) {
    window.clearTimeout(hoverTimeout.value);
  }
  if (scrapeLog.value.hideTimer) {
    window.clearTimeout(scrapeLog.value.hideTimer);
  }
});
</script>
