Source: services/tile-cache-manager.js

import { createStore, get, set, del, keys } from 'idb-keyval'
import OlExtElement from 'ol-ext/util/element'
import { createButton } from '../control/ControlUtils'

// ============================================================
// Exports de préconfigurations
// ============================================================

/**
 * TTL illimité (100 ans) pour tous les types de ressources
 * Le cache n'expire pratiquement jamais
 * @constant {Object}
 * @example
 * viewer.addVectorTile('layer', {
 *   cache: { ttl: CACHE_TTL_UNLIMITED }
 * })
 */
export const CACHE_TTL_UNLIMITED = {
  tiles: 100 * 365 * 24 * 60 * 60 * 1000,
  style: 100 * 365 * 24 * 60 * 60 * 1000,
  source: 100 * 365 * 24 * 60 * 60 * 1000,
  sprite: 100 * 365 * 24 * 60 * 60 * 1000,
  spriteImage: 100 * 365 * 24 * 60 * 60 * 1000,
  geoJson: 100 * 365 * 24 * 60 * 60 * 1000,
}

/**
 * TTL d'un mois (30 jours) pour tous les types de ressources
 * @constant {Object}
 * @example
 * viewer.addVectorTile('layer', {
 *   cache: { ttl: CACHE_TTL_ONE_MONTH }
 * })
 */
export const CACHE_TTL_ONE_MONTH = {
  tiles: 30 * 24 * 60 * 60 * 1000,
  style: 30 * 24 * 60 * 60 * 1000,
  source: 30 * 24 * 60 * 60 * 1000,
  sprite: 30 * 24 * 60 * 60 * 1000,
  spriteImage: 30 * 24 * 60 * 60 * 1000,
  geoJson: 30 * 24 * 60 * 60 * 1000,
}

/**
 * Stratégie cache-first pour tous les types de ressources
 * Utilise le cache en priorité, fetch le réseau si absent
 * @constant {Object}
 * @example
 * viewer.addVectorTile('layer', {
 *   cache: { strategy: CACHE_STRATEGY_CACHE_FIRST }
 * })
 */
export const CACHE_STRATEGY_CACHE_FIRST = {
  Tiles: 'cache-first',
  Style: 'cache-first',
  Source: 'cache-first',
  Sprite: 'cache-first',
  SpriteImage: 'cache-first',
  GeoJSON: 'cache-first',
}

/**
 * Stratégie network-first pour tous les types de ressources
 * Fetch le réseau en priorité, utilise le cache en fallback si échec
 * @constant {Object}
 * @example
 * viewer.addVectorTile('layer', {
 *   cache: { strategy: CACHE_STRATEGY_NETWORK_FIRST }
 * })
 */
export const CACHE_STRATEGY_NETWORK_FIRST = {
  Tiles: 'network-first',
  Style: 'network-first',
  Source: 'network-first',
  Sprite: 'network-first',
  SpriteImage: 'network-first',
  GeoJSON: 'network-first',
}

// ============================================================
// Fonctions utilitaires
// ============================================================

/**
 * Formate un nombre d'octets en format lisible
 * @param {number} bytes Nombre d'octets
 * @returns {string} Taille formatée (ex: "15.2 MB")
 */
function formatBytes (bytes) {
  if (bytes === 0) return '0 B'
  if (bytes === Infinity || !isFinite(bytes)) return '∞'
  const k = 1024
  const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
  const i = Math.floor(Math.log(bytes) / Math.log(k))
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}

/**
 * Map des constantes TTL prédéfinies
 * @private
 */
const TTL_PRESETS = {
  CACHE_TTL_UNLIMITED,
  CACHE_TTL_ONE_MONTH,
}

/**
 * Map des constantes Strategy prédéfinies
 * @private
 */
const STRATEGY_PRESETS = {
  CACHE_STRATEGY_CACHE_FIRST,
  CACHE_STRATEGY_NETWORK_FIRST,
}

// ============================================================
// JSDoc Typedefs
// ============================================================

/**
 * @typedef {Object} TileCacheTTLOptions
 * @property {number} [tiles] Durée de vie des tuiles en ms (défaut: 7 jours)
 * @property {number} [style] Durée de vie du style en ms (défaut: 7 jours)
 * @property {number} [source] Durée de vie de la source en ms (défaut: 7 jours)
 * @property {number} [sprite] Durée de vie du sprite en ms (défaut: 7 jours)
 * @property {number} [spriteImage] Durée de vie de l'image sprite en ms (défaut: 7 jours)
 * @property {number} [geoJson] Durée de vie du GeoJSON en ms (défaut: 7 jours)
 */

/**
 * @typedef {Object} TileCacheStrategyOptions
 * @property {('cache-first'|'network-first')} [Tiles] Stratégie pour les tuiles (défaut: 'network-first')
 * @property {('cache-first'|'network-first')} [Style] Stratégie pour le style (défaut: 'network-first')
 * @property {('cache-first'|'network-first')} [Source] Stratégie pour la source (défaut: 'network-first')
 * @property {('cache-first'|'network-first')} [Sprite] Stratégie pour les sprites (défaut: 'network-first')
 * @property {('cache-first'|'network-first')} [SpriteImage] Stratégie pour les images sprite (défaut: 'network-first')
 * @property {('cache-first'|'network-first')} [GeoJSON] Stratégie pour le GeoJSON (défaut: 'network-first')
 */

/**
 * Options de configuration du cache IndexedDB pour les tuiles vectorielles
 * @typedef {Object} TileCacheOptions
 * @property {TileCacheTTLOptions|string|number} [ttl] Durées de vie par type de ressource (objet, nom de constante, ou nombre de minutes pour tous les types)
 * @property {TileCacheStrategyOptions|string} [strategy] Stratégie de cache par type de ressource (objet ou nom de constante : 'CACHE_STRATEGY_CACHE_FIRST' ou 'CACHE_STRATEGY_NETWORK_FIRST')
 * @property {number} [maxEntries] Nombre maximum d'entrées en cache (défaut: Infinity = illimité)
 * @property {number} [quotaThreshold] Seuil de quota en % déclenchant l'éviction (défaut: 0.9 = 90%)
 * @property {number} [quotaEvictionPercent] Pourcentage d'entrées à supprimer lors d'éviction quota (défaut: 0.3 = 30%)
 * @property {number} [quotaCheckInterval] Vérifier le quota toutes les N écritures (défaut: 50)
 * @property {boolean} [enabled] Active ou désactive le cache (défaut: true)
 * @property {string} [dbName] Nom de base de la base de données IndexedDB (défaut: 'karteis-mapviewer-tiles')
 */

/**
 * Gestionnaire de cache IndexedDB pour les tuiles vectorielles
 * Chaque instance gère le cache d'un layer spécifique via un custom store
 */
export class TileCacheManager {
  /**
   * @param {string} layerId Identifiant unique du layer
   * @param {TileCacheOptions} [options] Options de configuration du cache
   */
  constructor (layerId, options = {}) {
    this.layerId = layerId
    this.enabled = false

    if (options.enabled === false) {
      return
    }
    // Créer une database spécifique pour ce layer
    // idb-keyval ne supporte pas plusieurs object stores dans une même DB
    // Donc on crée une database par layer : '{dbName}-layer-{idLayer}'
    const baseDbName = options.dbName || 'karteis-mapviewer-tiles'
    this.dbName = `${baseDbName}-layer-${layerId}`
    this.store = createStore(this.dbName, 'keyval')

    // Configuration des TTL par type de ressource
    // Supporte un objet, une string référençant une constante, ou un number (minutes)
    let ttlConfig
    if (typeof options.ttl === 'string') {
      // String : référence à une constante
      ttlConfig = TTL_PRESETS[options.ttl] || {}
    } else if (typeof options.ttl === 'number') {
      // Number : minutes pour tous les types
      const ttlMs = options.ttl * 60 * 1000
      ttlConfig = {
        tiles: ttlMs,
        style: ttlMs,
        source: ttlMs,
        sprite: ttlMs,
        spriteImage: ttlMs,
        geoJson: ttlMs,
      }
    } else {
      // Objet ou undefined
      ttlConfig = options.ttl || {}
    }

    this.ttl = {
      ...CACHE_TTL_ONE_MONTH, // Valeurs par défaut
      ...ttlConfig,
    }

    this.maxEntries = options.maxEntries ?? Infinity
    this.quotaThreshold = options.quotaThreshold ?? 0.9
    this.quotaEvictionPercent = options.quotaEvictionPercent ?? 0.3
    this.quotaCheckInterval = options.quotaCheckInterval ?? 50

    // Configuration de la stratégie par type de ressource
    // Supporte un objet ou une string référençant une constante
    const strategyConfig = typeof options.strategy === 'string'
      ? (STRATEGY_PRESETS[options.strategy] || {})
      : (options.strategy || {})

    this.strategy = {
      ...CACHE_STRATEGY_NETWORK_FIRST, // Valeurs par défaut
      ...strategyConfig,
    }

    // Compteur d'écritures pour optimiser les vérifications de quota
    this.writeCount = 0

    this.enabled = options.enabled !== false
  }

  getCacheKey (url, resourceType) {
    // Format: "resourceType:url"
    return `${resourceType}:${url}`
  }

  /**
   * Récupère une ressource depuis le cache
   * @param {string} url URL de la ressource
   * @param {string} resourceType Type de ressource
   * @returns {Promise<Response|null>} Response depuis le cache ou null
   */
  async getFromCache (url, resourceType) {
    if (!this.enabled) return null

    try {
      const key = this.getCacheKey(url, resourceType)
      const cached = await get(key, this.store)

      if (!cached) return null

      // Vérifier l'expiration
      const now = Date.now()
      const ttl = this.ttl[resourceType] || this.ttl.Tiles

      if (now - cached.timestamp > ttl) {
        await del(key, this.store)
        return null
      }

      // Reconstruire une Response depuis le cache
      return new Response(cached.data, {
        status: 200,
        statusText: 'OK (from cache)',
        headers: new Headers(cached.headers),
      })
    } catch (error) {
      console.warn(`Erreur lecture cache layer ${this.layerId}:`, error)
      return null
    }
  }

  /**
   * Stocke une ressource dans le cache
   * @param {string} url URL de la ressource
   * @param {string} resourceType Type de ressource
   * @param {Response} response Response à mettre en cache
   */
  async putInCache (url, resourceType, response) {
    if (!this.enabled) return

    try {
      const clonedResponse = response.clone()
      const data = await clonedResponse.arrayBuffer()
      const headers = {}

      // Extraire les headers importants
      clonedResponse.headers.forEach((value, key) => {
        headers[key] = value
      })

      const cacheEntry = {
        url,
        resourceType,
        data,
        headers,
        timestamp: Date.now(),
        ttl: this.ttl[resourceType],
      }

      const key = this.getCacheKey(url, resourceType)
      await set(key, cacheEntry, this.store)

      // Incrémenter le compteur d'écritures
      this.writeCount++

      // Vérifier la taille du cache (avec optimisation pour le quota)
      await this.evictIfNeeded()
    } catch (error) {
      console.warn(`Erreur mise en cache layer ${this.layerId}:`, error)
    }
  }

  /**
   * Fetch une ressource avec gestion du cache selon la stratégie configurée
   * @param {string} url URL de la ressource
   * @param {string} resourceType Type de ressource ('Tiles', 'Style', etc.)
   * @param {Function} createRequest Fonction qui retourne Request ou Promise<Request>
   * @returns {Promise<Response>} Response depuis le réseau ou le cache
   */
  async fetchWithCache (url, resourceType, createRequest, throwErrorOnKoRequest = false) {
    if (!this.enabled) {
      // Cache désactivé, fetch direct
      const request = await Promise.resolve(createRequest())
      return fetch(request)
    }

    const strategy = this.strategy[resourceType] || 'network-first'

    try {
      // Stratégie cache-first
      if (strategy === 'cache-first') {
        const cachedResponse = await this.getFromCache(url, resourceType)
        if (cachedResponse) {
          return cachedResponse
        }

        // Pas en cache, fetch depuis le réseau
        const request = await Promise.resolve(createRequest())
        const response = await fetch(request)

        if (response.ok) {
          // Mise en cache asynchrone (pas d'attente)
          this.putInCache(url, resourceType, response).catch(err => {
            console.warn(`Erreur cache asynchrone layer ${this.layerId}:`, err)
          })
        }
        if (throwErrorOnKoRequest && !response.ok) {
          throw new Error(`Response status: ${response.status}`)
        }
        return response
      }

      // Stratégie network-first (par défaut)
      const request = await Promise.resolve(createRequest())
      const response = await fetch(request)

      if (response.ok) {
        // Mise en cache asynchrone
        this.putInCache(url, resourceType, response).catch(err => {
          console.warn(`Erreur cache asynchrone layer ${this.layerId}:`, err)
        })
        return response
      }

      // Erreur réseau ou réponse non-ok, essayer le cache en fallback
      const cachedResponse = await this.getFromCache(url, resourceType)
      if (cachedResponse) {
        console.warn(`Layer ${this.layerId}: utilisation cache fallback pour ${resourceType} (${url})`)
        return cachedResponse
      }
      if (throwErrorOnKoRequest && !response.ok) {
        throw new Error(`Response status: ${response.status}`)
      }
      // Pas de cache disponible, retourner la réponse d'erreur
      return response
    } catch (error) {
      // Exception réseau, essayer le cache en dernier recours
      const cachedResponse = await this.getFromCache(url, resourceType)
      if (cachedResponse) {
        console.warn(`Layer ${this.layerId}: erreur réseau, utilisation cache pour ${resourceType} (${error.message})`)
        return cachedResponse
      }

      // Pas de cache, propager l'erreur
      throw error
    }
  }

  /**
   * Vérifie le quota de stockage IndexedDB
   * @returns {Promise<Object>} Informations sur le quota
   */
  async checkStorageQuota () {
    try {
      // Vérifier si l'API est disponible
      if (!navigator.storage || !navigator.storage.estimate) {
        return { needsEviction: false, reason: 'API non disponible' }
      }

      const estimate = await navigator.storage.estimate()
      const usage = estimate.usage || 0
      const quota = estimate.quota || Infinity
      const usagePercent = quota === Infinity ? 0 : usage / quota

      if (usagePercent >= this.quotaThreshold) {
        return {
          needsEviction: true,
          reason: 'quota',
          usage,
          quota,
          usagePercent,
        }
      }

      return { needsEviction: false, usage, quota, usagePercent }
    } catch (error) {
      console.warn(`Erreur vérification quota layer ${this.layerId}:`, error)
      return { needsEviction: false, error: error.message }
    }
  }

  /**
   * Éviction des entrées les plus anciennes selon maxEntries ou le quota
   */
  async evictIfNeeded () {
    try {
      let evictionReason = null
      let evictionPercent = 0.2 // 20% par défaut
      let allKeys = []

      if (this.maxEntries === Infinity) {
        // Mode quota uniquement : vérifier le quota (pas besoin de keys() si pas d'éviction)
        if (this.writeCount % this.quotaCheckInterval === 0) {
          const quotaCheck = await this.checkStorageQuota()
          if (quotaCheck.needsEviction) {
            // Maintenant on récupère les clés pour l'éviction
            allKeys = await keys(this.store)
            evictionReason = 'quota'
            evictionPercent = this.quotaEvictionPercent
            console.log(
              `Layer ${this.layerId}: quota à ${(quotaCheck.usagePercent * 100).toFixed(1)}% ` +
              `(${formatBytes(quotaCheck.usage)} / ${formatBytes(quotaCheck.quota)})`
            )
          }
        }
      } else {
        // Mode maxEntries : récupérer les clés et vérifier maxEntries
        allKeys = await keys(this.store)

        if (allKeys.length > this.maxEntries) {
          evictionReason = 'maxEntries'
          evictionPercent = 0.2
        } else if (this.writeCount % this.quotaCheckInterval === 0) {
          // Vérifier aussi le quota si maxEntries pas dépassé
          const quotaCheck = await this.checkStorageQuota()
          if (quotaCheck.needsEviction) {
            evictionReason = 'quota'
            evictionPercent = this.quotaEvictionPercent
            console.log(
              `Layer ${this.layerId}: quota à ${(quotaCheck.usagePercent * 100).toFixed(1)}% ` +
              `(${formatBytes(quotaCheck.usage)} / ${formatBytes(quotaCheck.quota)})`
            )
          }
        }
      }

      // Pas besoin d'éviction
      if (!evictionReason) {
        return
      }

      // Effectuer l'éviction LRU
      const entries = await Promise.all(
        allKeys.map(async key => ({
          key,
          entry: await get(key, this.store),
        }))
      )

      // Trier par timestamp (plus ancien en premier)
      entries.sort((a, b) => a.entry.timestamp - b.entry.timestamp)

      // Supprimer le pourcentage configuré des entrées les plus anciennes
      const toDelete = entries.slice(0, Math.floor(entries.length * evictionPercent))
      await Promise.all(toDelete.map(({ key }) => del(key, this.store)))

      console.log(
        `Cache layer ${this.layerId}: ${toDelete.length} entrées supprimées ` +
        `(éviction ${evictionReason}, ${(evictionPercent * 100).toFixed(0)}% du cache)`
      )
    } catch (error) {
      console.warn(`Erreur éviction cache layer ${this.layerId}:`, error)
    }
  }

  /**
   * Vide le cache pour ce layer
   * @param {string} [resourceType] Type de ressource optionnel (vide tout si non spécifié)
   */
  async clearCache (resourceType) {
    try {
      const allKeys = await keys(this.store)

      if (resourceType) {
        // Supprimer uniquement les clés d'un type spécifique
        const keysToDelete = allKeys.filter(k => k.startsWith(`${resourceType}:`))
        await Promise.all(keysToDelete.map(key => del(key, this.store)))
        console.log(`Cache layer ${this.layerId}: ${keysToDelete.length} entrées ${resourceType} supprimées`)
      } else {
        // Supprimer toutes les clés du store
        await Promise.all(allKeys.map(key => del(key, this.store)))
        console.log(`Cache layer ${this.layerId}: cache complet supprimé (${allKeys.length} entrées)`)
      }
    } catch (error) {
      console.warn(`Erreur clear cache layer ${this.layerId}:`, error)
    }
  }

  /**
   * Obtient les statistiques du cache pour ce layer
   * @returns {Promise<Object>} Statistiques du cache
   */
  async getCacheStats () {
    try {
      const allKeys = await keys(this.store)
      const quotaInfo = await this.checkStorageQuota()

      const stats = {
        layerId: this.layerId,
        total: allKeys.length,
        maxEntries: this.maxEntries,
        byType: {},
        oldestEntry: null,
        newestEntry: null,
      }

      // Ajouter les infos de quota si disponibles
      if (quotaInfo.usage !== undefined) {
        stats.quota = {
          usage: quotaInfo.usage,
          quota: quotaInfo.quota,
          usagePercent: quotaInfo.usagePercent,
          available: quotaInfo.quota !== Infinity ? quotaInfo.quota - quotaInfo.usage : Infinity,
          usageFormatted: formatBytes(quotaInfo.usage),
          quotaFormatted: formatBytes(quotaInfo.quota),
          availableFormatted: formatBytes(
            quotaInfo.quota !== Infinity ? quotaInfo.quota - quotaInfo.usage : Infinity
          ),
        }
      }

      // Compter par type
      for (const key of allKeys) {
        const [type] = key.split(':')
        stats.byType[type] = (stats.byType[type] || 0) + 1
      }

      // Trouver les entrées les plus anciennes/récentes
      if (allKeys.length > 0) {
        const entries = await Promise.all(
          allKeys.slice(0, Math.min(100, allKeys.length)).map(async key => {
            const entry = await get(key, this.store)
            return { key, timestamp: entry?.timestamp }
          })
        )

        const sorted = entries.filter(e => e.timestamp).sort((a, b) => a.timestamp - b.timestamp)
        if (sorted.length > 0) {
          stats.oldestEntry = new Date(sorted[0].timestamp).toISOString()
          stats.newestEntry = new Date(sorted[sorted.length - 1].timestamp).toISOString()
        }
      }

      return stats
    } catch (error) {
      console.warn(`Erreur stats cache layer ${this.layerId}:`, error)
      return { layerId: this.layerId, error: error.message }
    }
  }

  /**
   * Supprime complètement la database d'un layer
   * @param {string} layerId Identifiant du layer
   * @param {string} [baseDbName] Nom de base de la base de données (défaut: 'karteis-mapviewer-tiles')
   */
  static async deleteLayerStore (layerId, baseDbName = 'karteis-mapviewer-tiles') {
    try {
      const dbName = `${baseDbName}-layer-${layerId}`

      return new Promise((resolve, reject) => {
        const request = indexedDB.deleteDatabase(dbName)

        request.onsuccess = () => {
          console.log(`Database layer ${layerId} supprimée`)
          resolve()
        }

        request.onerror = () => {
          console.warn(`Erreur suppression database layer ${layerId}:`, request.error)
          reject(request.error)
        }

        request.onblocked = () => {
          console.warn(`Suppression database layer ${layerId} bloquée (connexions ouvertes)`)
        }
      })
    } catch (error) {
      console.warn(`Erreur suppression database layer ${layerId}:`, error)
    }
  }

  /**
   * Liste tous les layers ayant un cache
   * @param {string} [baseDbName] Nom de base de la base de données (défaut: 'karteis-mapviewer-tiles')
   * @returns {Promise<string[]>} Liste des identifiants de layers en cache
   */
  static async listCachedLayers (baseDbName = 'karteis-mapviewer-tiles') {
    try {
      // indexedDB.databases() retourne toutes les databases
      if (!indexedDB.databases) {
        console.warn('indexedDB.databases() non disponible dans ce navigateur')
        return []
      }

      const databases = await indexedDB.databases()
      const prefix = `${baseDbName}-layer-`

      // Extraire les layer IDs depuis les noms de databases
      const layerIds = databases
        .filter(db => db.name && db.name.startsWith(prefix))
        .map(db => db.name.replace(prefix, ''))

      return layerIds
    } catch (error) {
      console.warn('Erreur listage layers en cache:', error)
      return []
    }
  }

  /**
   * Affiche une interface de gestion du cache
   * @param {HTMLElement} target Élément cible où ajouter le panneau
   */
  async showCacheManagerDialog (target) {
    // Récupérer les statistiques
    const stats = await this.getCacheStats()

    // Créer le conteneur principal
    const dialog = OlExtElement.create('DIV', {
      className: 'kmapv-cache-manager-dialog',
      style: {
        position: 'absolute',
        top: '10px',
        right: '10px',
        backgroundColor: 'white',
        borderRadius: '8px',
        padding: '15px',
        maxWidth: '500px',
        maxHeight: '80vh',
        overflowY: 'auto',
        boxShadow: '0 4px 20px rgba(0, 0, 0, 0.3)',
        zIndex: '1000',
      },
    })

    // Titre avec bouton de fermeture
    const titleContainer = OlExtElement.create('DIV', {
      style: {
        display: 'flex',
        justifyContent: 'space-between',
        alignItems: 'center',
        marginBottom: '15px',
        borderBottom: '2px solid #e0e0e0',
        paddingBottom: '10px',
      },
    })

    const title = OlExtElement.create('H3', {
      text: `Gestion du cache - ${stats.layerId}`,
      style: {
        margin: '0',
        color: '#333',
        fontSize: '16px',
        fontWeight: 'bold',
      },
    })

    const closeButton = OlExtElement.create('i', {
      className: 'kmapv-icon kmapv-icon-cancel',
      style: {
        cursor: 'pointer',
      },
      on: {
        click: () => {
          dialog.remove()
        },
      },
      title: 'Fermer',
    })

    titleContainer.appendChild(title)
    titleContainer.appendChild(closeButton)
    dialog.appendChild(titleContainer)

    // Statistiques globales
    const globalStats = OlExtElement.create('DIV', {
      style: {
        marginBottom: '10px',
        padding: '10px',
        backgroundColor: '#f5f5f5',
        borderRadius: '4px',
      },
    })
    globalStats.innerHTML = `
      <strong>Statistiques globales :</strong><br/>
      Total d'entrées : <strong>${stats.total}</strong><br/>
      Limite : <strong>${stats.maxEntries === Infinity ? 'Illimitée' : stats.maxEntries}</strong>
    `
    dialog.appendChild(globalStats)

    // Statistiques par type
    if (Object.keys(stats.byType).length > 0) {
      const typeStats = OlExtElement.create('DIV', {
        style: {
          marginBottom: '10px',
          padding: '10px',
          backgroundColor: '#f5f5f5',
          borderRadius: '4px',
        },
      })
      let typeHtml = '<strong>Par type de ressource :</strong><br/>'
      for (const [type, count] of Object.entries(stats.byType)) {
        typeHtml += `  - ${type} : <strong>${count}</strong> entrée(s)<br/>`
      }
      typeStats.innerHTML = typeHtml
      dialog.appendChild(typeStats)
    }

    // Informations de quota
    if (stats.quota) {
      const quotaStats = OlExtElement.create('DIV', {
        style: {
          marginBottom: '10px',
          padding: '10px',
          backgroundColor: '#e8f5e9',
          borderRadius: '4px',
        },
      })
      quotaStats.innerHTML = `
        <strong>Quota IndexedDB :</strong><br/>
        Utilisation : <strong>${stats.quota.usageFormatted}</strong> / ${stats.quota.quotaFormatted}<br/>
        Pourcentage : <strong>${(stats.quota.usagePercent * 100).toFixed(1)}%</strong><br/>
        Disponible : <strong>${stats.quota.availableFormatted}</strong>
      `
      dialog.appendChild(quotaStats)
    }

    // Dates des entrées
    if (stats.oldestEntry && stats.newestEntry) {
      const dateStats = OlExtElement.create('DIV', {
        style: {
          marginBottom: '10px',
          padding: '10px',
          backgroundColor: '#f5f5f5',
          borderRadius: '4px',
          fontSize: '12px',
        },
      })
      dateStats.innerHTML = `
        <strong>Plage temporelle :</strong><br/>
        Plus ancienne : ${new Date(stats.oldestEntry).toLocaleString()}<br/>
        Plus récente : ${new Date(stats.newestEntry).toLocaleString()}
      `
      dialog.appendChild(dateStats)
    }

    // Conteneur des boutons
    const buttonsContainer = OlExtElement.create('DIV', {
      style: {
        display: 'flex',
        gap: '10px',
        justifyContent: 'flex-end',
        marginTop: '15px',
      },
    })

    // Bouton effacer le cache
    const clearButton = createButton(
      '<i class="kmapv-icon kmapv-icon-clear"></i><span>Effacer le cache</span>',
      async () => {
        if (confirm(`Êtes-vous sûr de vouloir effacer tout le cache du layer "${stats.layerId}" ?`)) {
          await this.clearCache()
          dialog.remove()
          alert('Cache effacé avec succès !')
        }
      }
    )
    clearButton.title = 'Effacer tout le cache de ce layer'

    buttonsContainer.appendChild(clearButton)
    dialog.appendChild(buttonsContainer)

    // Ajouter à l'élément de la carte
    target.appendChild(dialog)
  }
}