/**
 * ProxmoxPanel — Alpine.js stores + composants + Swup init + HTMX config
 *
 * Chargé AVANT alpine.min.js (qui est defer).
 * L'événement 'alpine:init' est déclenché par Alpine avant qu'il parcourt le DOM.
 */

// ── WsProxy — WebSocket via Service Worker avec fallback direct ─────────────

const WsProxy = {
  _subs: new Map(),      // channel → [{onMessage, onStatus}]
  _direct: new Map(),    // channel → WebSocket (fallback sans SW)

  init() {
    if (!('serviceWorker' in navigator)) return
    navigator.serviceWorker.register('/ws.sw.js')
      .catch(e => console.warn('[WsProxy] SW registration failed:', e))
    navigator.serviceWorker.addEventListener('message', event => {
      const { channel, type, data, status } = event.data || {}
      if (!channel) return
      const subs = this._subs.get(channel) || []
      if (type === 'WS_MESSAGE') subs.forEach(s => s.onMessage?.(data))
      if (type === 'WS_STATUS')  subs.forEach(s => s.onStatus?.(status))
    })
  },

  async subscribe(channel, url, onMessage, onStatus) {
    if (!this._subs.has(channel)) this._subs.set(channel, [])
    const sub = { onMessage, onStatus }
    this._subs.get(channel).push(sub)

    if ('serviceWorker' in navigator) {
      try {
        await navigator.serviceWorker.ready
        const sw = navigator.serviceWorker.controller
        if (sw) {
          sw.postMessage({ type: 'WS_SUBSCRIBE', channel, url })
          return () => this._unsub(channel, sub, true)
        }
      } catch(e) { /* fallback */ }
    }
    this._connectDirect(channel, url)
    return () => this._unsub(channel, sub, false)
  },

  _unsub(channel, sub, usedSW) {
    const subs = this._subs.get(channel) || []
    const idx = subs.indexOf(sub)
    if (idx > -1) subs.splice(idx, 1)
    if (usedSW) {
      navigator.serviceWorker?.controller?.postMessage({ type: 'WS_UNSUBSCRIBE', channel })
    } else if (subs.length === 0) {
      this._direct.get(channel)?.close()
      this._direct.delete(channel)
    }
  },

  _connectDirect(channel, url) {
    const ex = this._direct.get(channel)
    if (ex && ex.readyState < 2) return
    const ws = new WebSocket(url)
    this._direct.set(channel, ws)
    const fire = (cb, arg) => (this._subs.get(channel) || []).forEach(s => s[cb]?.(arg))
    ws.onopen    = ()  => fire('onStatus', 'connected')
    ws.onmessage = (e) => fire('onMessage', e.data)
    ws.onerror   = ()  => fire('onStatus', 'error')
    ws.onclose   = ()  => {
      fire('onStatus', 'disconnected')
      setTimeout(() => {
        if ((this._subs.get(channel) || []).length > 0) this._connectDirect(channel, url)
      }, 3000)
    }
  },

  send(channel, data) {
    const sw = navigator.serviceWorker?.controller
    if (sw) sw.postMessage({ type: 'WS_SEND', channel, payload: data })
    else this._direct.get(channel)?.send(data)
  },
}

// ── Utilitaires ────────────────────────────────────────────────────────────

function apiFetch(path, opts = {}) {
  const token = localStorage.getItem('pxp_token')
  return fetch(path, {
    ...opts,
    headers: {
      'Content-Type': 'application/json',
      ...(token ? { Authorization: 'Bearer ' + token } : {}),
      ...(opts.headers || {}),
    },
  })
}

// ── Alpine:init ────────────────────────────────────────────────────────────

document.addEventListener('alpine:init', () => {

  // ── Store toasts ────────────────────────────────────────────────────────
  Alpine.store('toasts', {
    items: [],
    _id: 0,
    add(message, type = 'info', duration = 5000) {
      const id = ++this._id
      this.items.push({ id, message, type })
      if (duration > 0) setTimeout(() => this.remove(id), duration)
      return id
    },
    remove(id) { this.items = this.items.filter(t => t.id !== id) },
    error(msg)   { return this.add(msg, 'error',   8000) },
    success(msg) { return this.add(msg, 'success', 4000) },
    warn(msg)    { return this.add(msg, 'warn',    6000) },
    info(msg)    { return this.add(msg, 'info',    4000) },
  })

  // Injecter le conteneur de toasts dans le body (persiste à travers Swup)
  ;(function() {
    const tc = document.createElement('div')
    tc.id = 'pxp-toasts'
    tc.setAttribute('x-data', '')
    tc.innerHTML = `
      <template x-for="t in $store.toasts.items" :key="t.id">
        <div class="toast" :class="'toast--' + t.type" x-transition.opacity>
          <i :class="{
            'lnid-cross-circle':      t.type === 'error',
            'lnid-check-circle-1':    t.type === 'success',
            'lnid-warning-triangle-1':t.type === 'warn',
            'lnid-information-circle':t.type === 'info'
          }"></i>
          <span class="toast-msg" x-text="t.message"></span>
          <button class="toast-close" @click="$store.toasts.remove(t.id)">
            <i class="lnid-cross"></i>
          </button>
        </div>
      </template>
    `
    document.body.appendChild(tc)
  })()

  // ── Store auth ─────────────────────────────────────────────────────────
  Alpine.store('auth', {
    token: null,
    user: null,
    get isAuthenticated() { return !!this.token && !!this.user },

    async init() {
      this.token = localStorage.getItem('pxp_token')
      if (this.token) {
        try {
          await this.fetchMe()
        } catch {
          await this.tryRefresh()
        }
      }
    },

    async fetchMe() {
      const res = await apiFetch('/api/auth/me')
      if (res.ok) {
        this.user = await res.json()
        // Sync préférences DB → localStorage + stores
        const u = this.user
        if (u.theme && u.theme !== Alpine.store('ui').theme) {
          localStorage.setItem('pxp_theme', u.theme)
          Alpine.store('ui').theme = u.theme
          Alpine.store('ui').applyTheme()
        }
        if (u.sidebar_position && u.sidebar_position !== Alpine.store('ui').sidebarPosition) {
          Alpine.store('ui').setSidebarPosition(u.sidebar_position)
        }
        if (u.lang && u.lang !== Alpine.store('i18n').lang) {
          Alpine.store('i18n').setLang(u.lang)
        }
      } else if (res.status === 401) {
        // Token expiré → tenter un refresh silencieusement
        await this.tryRefresh()
      } else {
        // Erreur inattendue (404, 500…) — signaler + tenter quand même
        const body = await res.json().catch(() => ({}))
        console.error(`[auth/me] HTTP ${res.status}`, body.error || '')
        Alpine.store('toasts').error(
          `Erreur ${res.status} sur /api/auth/me : ${body.error || 'voir console'}`
        )
        await this.tryRefresh()
      }
    },

    async tryRefresh() {
      const res = await fetch('/api/auth/refresh', { method: 'POST', credentials: 'include' })
      if (res.ok) {
        const data = await res.json()
        this.token = data.access_token
        localStorage.setItem('pxp_token', data.access_token)
        await this.fetchMe()
      } else {
        // Lire le vrai message d'erreur du backend pour le diagnostic
        const body = await res.json().catch(() => ({}))
        const serverMsg = body.error || `HTTP ${res.status}`
        const page = window.location.pathname.replace(/^\/|\.html$/g, '')
        if (page !== 'login' && page !== 'install' && page !== 'index' && page !== '') {
          sessionStorage.setItem('pxp_auth_notice', `Refresh échoué : ${serverMsg}`)
        }
        console.error('[auth/tryRefresh]', res.status, serverMsg)
        this.clear()
      }
    },

    async login(username, password) {
      const res = await fetch('/api/auth/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        credentials: 'include',
        body: JSON.stringify({ username, password }),
      })
      if (!res.ok) {
        const err = await res.json().catch(() => ({}))
        throw new Error(err.error || 'Identifiants invalides')
      }
      const data = await res.json()
      // Le backend retourne "access_token" (pas "token")
      this.token = data.access_token
      this.user = data.user
      localStorage.setItem('pxp_token', data.access_token)
    },

    async logout() {
      await apiFetch('/api/auth/logout', { method: 'POST', credentials: 'include' }).catch(() => {})
      this.clear()
      sessionStorage.setItem('pxp_auth_notice', 'Déconnexion réussie')
      window.location.href = '/login.html'
    },

    clear() {
      this.token = null
      this.user = null
      localStorage.removeItem('pxp_token')
    },
  })

  // ── Store UI ────────────────────────────────────────────────────────────
  Alpine.store('ui', {
    theme: localStorage.getItem('pxp_theme') || 'dark',
    sidebarCollapsed: localStorage.getItem('pxp_sidebar') === 'true',
    sidebarPosition: localStorage.getItem('pxp_sidebar_pos') || 'left',
    currentPage: '',

    init() {
      this.applyTheme()
      this.applySidebarPosition()
      const path = window.location.pathname
      this.currentPage = path.replace(/^\/|\.html$/g, '') || 'index'
    },

    applyTheme() {
      document.documentElement.setAttribute('data-theme', this.theme)
    },

    toggleTheme() {
      this.theme = this.theme === 'dark' ? 'light' : 'dark'
      localStorage.setItem('pxp_theme', this.theme)
      this.applyTheme()
    },

    applySidebarPosition() {
      document.documentElement.setAttribute('data-sidebar', this.sidebarPosition)
    },

    setSidebarPosition(pos) {
      this.sidebarPosition = pos
      localStorage.setItem('pxp_sidebar_pos', pos)
      this.applySidebarPosition()
    },

    toggleSidebar() {
      this.sidebarCollapsed = !this.sidebarCollapsed
      localStorage.setItem('pxp_sidebar', this.sidebarCollapsed)
    },
  })

  // ── Store i18n ──────────────────────────────────────────────────────────
  Alpine.store('i18n', {
    lang: localStorage.getItem('pxp_lang') || 'fr',
    msgs: {},
    loaded: false,

    async init() {
      await this.load(this.lang)
    },

    async load(lang) {
      try {
        const res = await fetch(`/locales/${lang}.json`)
        if (res.ok) {
          this.msgs = await res.json()
          this.lang = lang
          localStorage.setItem('pxp_lang', lang)
          this.loaded = true
        }
      } catch (e) {
        console.error('i18n load error', e)
      }
    },

    t(key, vars = {}) {
      const parts = key.split('.')
      let val = this.msgs
      for (const p of parts) {
        val = val?.[p]
        if (val === undefined) return key
      }
      if (typeof val !== 'string') return key
      return val.replace(/\{(\w+)\}/g, (_, k) => vars[k] ?? `{${k}}`)
    },

    async setLang(lang) {
      await this.load(lang)
    },
  })

  // ── Composant: sidebar ──────────────────────────────────────────────────
  // Items CORE toujours visibles (sidebar hardcodée pour le CORE)
  const _coreNavItems = [
    { id: 'dashboard', iconClass: 'lnid-dashboard-square-1', iconColor: '#6c8ef4', labelKey: 'nav.dashboard', href: '/dashboard.html' },
    { id: 'proxmox',   iconClass: 'lnid-server-1',           iconColor: '#22c55e', labelKey: 'nav.proxmox',   href: '/proxmox.html'   },
    { id: 'updates',   iconClass: 'lnid-arrow-upward',        iconColor: '#f59e0b', labelKey: 'nav.updates',   href: '/updates.html'   },
    { id: 'settings',  iconClass: 'lnid-gear-1',              iconColor: '#94a3b8', labelKey: 'nav.settings',  href: '/settings.html'  },
    { id: 'modules',   iconClass: 'lnid-puzzle',              iconColor: '#f472b6', labelKey: 'nav.modules',   href: '/modules.html'   },
  ]

  Alpine.data('sidebar', () => ({
    get collapsed() { return Alpine.store('ui').sidebarCollapsed },
    get currentPage() { return Alpine.store('ui').currentPage },

    navItems: [..._coreNavItems],

    async init() {
      await this.refreshNav()
    },

    async refreshNav() {
      try {
        const res = await apiFetch('/api/modules')
        if (!res.ok) return
        const allModules = await res.json() || []
        // Modules optionnels activés avec nav_href défini
        const moduleItems = allModules
          .filter(m => !m.is_core && m.is_enabled && m.nav_href)
          .map(m => ({
            id: m.id,
            iconClass: m.nav_icon || 'lnid-puzzle',
            iconColor: m.nav_color || '#94a3b8',
            labelKey: m.nav_label_key || `nav.${m.id}`,
            href: m.nav_href,
          }))
        // Insérer entre Updates et Settings
        const insertAt = _coreNavItems.findIndex(i => i.id === 'settings')
        this.navItems = [
          ..._coreNavItems.slice(0, insertAt),
          ...moduleItems,
          ..._coreNavItems.slice(insertAt),
        ]
      } catch(e) {}
    },

    iconStyle(item) {
      return this.isActive(item.id) ? '' : `color: ${item.iconColor}`
    },

    isActive(id) {
      return this.currentPage === id
    },

    navigate(href) {
      if (window.swup) {
        window.swup.navigate(href)
      } else {
        window.location.href = href
      }
    },

    t(key) { return Alpine.store('i18n').t(key) },
    toggle() { Alpine.store('ui').toggleSidebar() },
  }))

  // ── Composant: navbar ───────────────────────────────────────────────────
  Alpine.data('navbar', () => ({
    get theme() { return Alpine.store('ui').theme },
    get user() { return Alpine.store('auth').user },
    get lang() { return Alpine.store('i18n').lang },
    set lang(v) { /* x-model a besoin d'un setter ; @change gère la vraie MAJ */ },

    toggleTheme() { Alpine.store('ui').toggleTheme() },
    logout() { Alpine.store('auth').logout() },
    async setLang(lang) { await Alpine.store('i18n').setLang(lang) },
    t(key) { return Alpine.store('i18n').t(key) },
  }))

  // ── Composant: loginPage ────────────────────────────────────────────────
  Alpine.data('loginPage', () => ({
    username: '',
    password: '',
    error: '',
    loading: false,

    init() {
      // Afficher le message si la session a expiré avant la redirection
      const msg = sessionStorage.getItem('pxp_auth_notice')
      if (msg) {
        sessionStorage.removeItem('pxp_auth_notice')
        setTimeout(() => Alpine.store('toasts').warn(msg), 200)
      }
    },

    async submit() {
      this.error = ''
      this.loading = true
      try {
        await Alpine.store('auth').login(this.username, this.password)
        window.location.href = '/dashboard.html'
      } catch (e) {
        this.error = e.message
      } finally {
        this.loading = false
      }
    },

    t(key) { return Alpine.store('i18n').t(key) },
  }))

  // ── Composant: installPage ──────────────────────────────────────────────
  Alpine.data('installPage', () => ({
    step: 1,
    totalSteps: 4,
    error: '',
    loading: false,
    sshTesting: false,
    sshStatus: '',

    form: {
      instance_name: 'ProxmoxPanel',
      public_url: window.location.origin,
      default_lang: 'fr',
      ssh_host: '',
      ssh_username: '',
      ssh_password: '',
      proxmox_url: '',
      proxmox_token_id: '',
      proxmox_token_secret: '',
    },

    async testSSH() {
      this.sshTesting = true
      this.sshStatus = ''
      try {
        const res = await fetch('/api/install/test-ssh', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            host: this.form.ssh_host,
            username: this.form.ssh_username,
            password: this.form.ssh_password,
          }),
        })
        this.sshStatus = res.ok ? 'ok' : 'error'
      } catch {
        this.sshStatus = 'error'
      } finally {
        this.sshTesting = false
      }
    },

    nextStep() {
      if (this.step < this.totalSteps) this.step++
    },
    prevStep() {
      if (this.step > 1) this.step--
    },

    async finish() {
      this.loading = true
      this.error = ''
      try {
        const res = await fetch('/api/install/configure', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(this.form),
        })
        if (!res.ok) {
          const d = await res.json().catch(() => ({}))
          throw new Error(d.error || 'Erreur installation')
        }
        window.location.href = '/login.html'
      } catch (e) {
        this.error = e.message
      } finally {
        this.loading = false
      }
    },

    t(key, vars) { return Alpine.store('i18n').t(key, vars) },
  }))

  // ── Composant: dashboardPage ────────────────────────────────────────────
  Alpine.data('dashboardPage', () => ({
    resources: [],
    _unsubWS: null,
    wsStatus: 'connecting',
    editMode: false,
    dragSrcIdx: null,
    _dragOriginal: null,
    hoveredWidgetId: null,
    shortcuts: [],

    widgets: (function() {
      const defaults = [
        { id: 'status',   visible: true,  size: 1, label: 'Statut LXC'  },
        { id: 'lxc-list', visible: true,  size: 1, label: 'Liste LXC'   },
        { id: 'links',    visible: false, size: 1, label: 'Raccourcis'  },
      ]
      try {
        const saved = JSON.parse(localStorage.getItem('pxp_widgets') || 'null')
        if (!saved) return defaults
        const ids = saved.map(w => w.id)
        return [...saved, ...defaults.filter(d => !ids.includes(d.id))]
      } catch(e) { return defaults }
    })(),

    saveWidgets() { localStorage.setItem('pxp_widgets', JSON.stringify(this.widgets)) },

    // ── Edition
    toggleEdit() { this.editMode = !this.editMode },

    showWidget(id) {
      const w = this.widgets.find(w => w.id === id)
      if (w) { w.visible = true; this.saveWidgets() }
    },

    hideWidget(w) {
      w.visible = false
      this.saveWidgets()
    },

    // ── Resize via souris (bords de la tuile)
    startResize(event, w) {
      event.stopPropagation()
      event.preventDefault()
      const startX  = event.clientX
      const origSize = w.size || 1
      const onMove = (e) => {
        const grid = this.$el.querySelector('.widgets-grid')
        const col  = grid ? (grid.offsetWidth / 2) : 400
        const dx   = e.clientX - startX
        if      (origSize === 1 && dx >  col * 0.3) w.size = 2
        else if (origSize === 2 && dx < -col * 0.3) w.size = 1
        else                                         w.size = origSize
      }
      const onUp = () => {
        this.saveWidgets()
        document.removeEventListener('mousemove', onMove)
        document.removeEventListener('mouseup',   onUp)
      }
      document.addEventListener('mousemove', onMove)
      document.addEventListener('mouseup',   onUp)
    },

    // ── DnD — réorganisation live (les autres widgets se déplacent pendant le drag)
    onDragStart(idx) {
      this.dragSrcIdx   = idx
      this._dragOriginal = JSON.parse(JSON.stringify(this.widgets))
    },

    onDragEnter(idx) {
      if (this.dragSrcIdx === null || this.dragSrcIdx === idx) return
      const moved = this.widgets.splice(this.dragSrcIdx, 1)[0]
      this.widgets.splice(idx, 0, moved)
      this.dragSrcIdx = idx
    },

    onDragEnd() {
      // Annulation (pas de drop) → restaurer l'ordre original
      if (this._dragOriginal) {
        this.widgets = JSON.parse(JSON.stringify(this._dragOriginal))
        this._dragOriginal = null
      }
      this.dragSrcIdx = null
    },

    onDrop() {
      this._dragOriginal = null  // commit — ne pas restaurer dans onDragEnd
      this.dragSrcIdx    = null
      this.saveWidgets()
    },

    // ── Data
    async init() {
      await this.fetchResources()
      await this.connectWS()
      await this.loadShortcuts()
    },

    destroy() {
      if (this._unsubWS) { this._unsubWS(); this._unsubWS = null }
    },

    async loadShortcuts() {
      try {
        const res = await apiFetch('/api/settings')
        if (res.ok) {
          const data = await res.json()
          if (data.dashboard_shortcuts) {
            const parsed = JSON.parse(data.dashboard_shortcuts)
            if (Array.isArray(parsed) && parsed.length > 0) this.shortcuts = parsed
          }
        }
      } catch(e) { /* use defaults */ }
    },

    get displayShortcuts() {
      return this.shortcuts.length ? this.shortcuts : [
        { href: '/proxmox.html', icon: 'lnid-server-1', label: 'Proxmox' },
        { href: '/terminal.html', icon: 'lnid-terminal', label: 'Terminal' },
        { href: '/updates.html', icon: 'lnid-arrow-upward', label: 'Updates' },
      ]
    },

    async fetchResources() {
      try {
        const res = await apiFetch('/api/proxmox/resources')
        if (res.ok) { this.resources = await res.json() || []; this.wsStatus = 'connected' }
      } catch (e) { /* WS prendra le relais */ }
    },

    async connectWS() {
      const proto = location.protocol === 'https:' ? 'wss' : 'ws'
      const token = encodeURIComponent(localStorage.getItem('pxp_token') || '')
      const url   = `${proto}://${location.host}/ws/proxmox?token=${token}`
      this._unsubWS = await WsProxy.subscribe(
        'proxmox', url,
        (data) => {
          const msg = JSON.parse(data)
          if (msg.type === 'resources_update') { this.resources = msg.payload || []; this.wsStatus = 'connected' }
        },
        (status) => { this.wsStatus = status }
      )
    },

    get visibleWidgets() { return this.widgets.filter(w => w.visible) },
    get lxc()            { return this.resources.filter(r => r.type === 'lxc') },
    get running()        { return this.lxc.filter(r => r.status === 'running') },
    get stopped()        { return this.lxc.filter(r => r.status !== 'running') },
    get lxcList()        { return this.lxc },
    get vmList()         { return this.resources.filter(r => r.type === 'qemu') },

    t(key) { return Alpine.store('i18n').t(key) },
  }))

  // ── Composant: profilePage ──────────────────────────────────────────────
  Alpine.data('profilePage', () => ({
    theme: '',
    sidebarPosition: '',
    lang: '',
    sessions: [],
    sessionsLoading: true,
    revoking: {},

    async init() {
      this.theme = Alpine.store('ui').theme
      this.sidebarPosition = Alpine.store('ui').sidebarPosition
      this.lang = Alpine.store('i18n').lang
      await this.loadSessions()
    },

    setTheme(t) {
      this.theme = t
      Alpine.store('ui').theme = t
      Alpine.store('ui').applyTheme()
      localStorage.setItem('pxp_theme', t)
      apiFetch('/api/auth/preferences', { method: 'PATCH', body: JSON.stringify({ theme: t }) }).catch(() => {})
    },

    setSidebarPosition(pos) {
      this.sidebarPosition = pos
      Alpine.store('ui').setSidebarPosition(pos)
      apiFetch('/api/auth/preferences', { method: 'PATCH', body: JSON.stringify({ sidebar_position: pos }) }).catch(() => {})
    },

    async setLang(lang) {
      this.lang = lang
      await Alpine.store('i18n').setLang(lang)
      apiFetch('/api/auth/preferences', { method: 'PATCH', body: JSON.stringify({ lang }) }).catch(() => {})
    },

    async loadSessions() {
      this.sessionsLoading = true
      try {
        const res = await apiFetch('/api/auth/sessions')
        if (res.ok) this.sessions = await res.json()
      } catch (e) { /* ignore */ }
      this.sessionsLoading = false
    },

    async revokeSession(id) {
      this.revoking[id] = true
      try {
        const res = await apiFetch(`/api/auth/sessions/${id}`, { method: 'DELETE' })
        if (res.ok) {
          this.sessions = this.sessions.filter(s => s.id !== id)
          Alpine.store('toasts').success('Session révoquée')
        } else {
          const d = await res.json().catch(() => ({}))
          Alpine.store('toasts').error(d.error || 'Erreur révocation session')
        }
      } catch (e) {
        Alpine.store('toasts').error(`Erreur réseau — ${e.message}`)
      } finally {
        this.revoking[id] = false
      }
    },

    isRevoking(id) { return this.revoking[id] === true },

    formatDate(raw) {
      if (!raw) return '—'
      // SQLite retourne "YYYY-MM-DD HH:MM:SS" ou RFC3339 selon le driver
      // new Date() gère les deux si on normalise le séparateur
      const d = new Date(raw.includes('T') ? raw : raw.replace(' ', 'T') + 'Z')
      if (isNaN(d.getTime())) return raw
      return d.toLocaleString('fr-FR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' })
    },

    parseUA(ua) {
      if (!ua) return 'Navigateur inconnu'
      if (/Firefox\/(\d+)/i.test(ua)) return `Firefox ${RegExp.$1}`
      if (/Chrome\/(\d+)/i.test(ua) && !/Chromium|Edge|OPR/i.test(ua)) return `Chrome ${RegExp.$1}`
      if (/Edg\/(\d+)/i.test(ua)) return `Edge ${RegExp.$1}`
      if (/Safari\//i.test(ua) && !/Chrome/i.test(ua)) return 'Safari'
      if (/OPR\/(\d+)/i.test(ua)) return `Opera ${RegExp.$1}`
      return ua.slice(0, 40)
    },

    get user() { return Alpine.store('auth').user },
    t(key) { return Alpine.store('i18n').t(key) },
  }))

  // ── Composant: proxmoxPage ──────────────────────────────────────────────
  Alpine.data('proxmoxPage', () => ({
    resources: [],
    _unsubWS: null,
    wsStatus: 'connecting',
    actionLoading: {},

    async init() {
      await this.fetchResources()
      await this.connectWS()
    },
    destroy() { if (this._unsubWS) { this._unsubWS(); this._unsubWS = null } },

    async fetchResources() {
      try {
        const res = await apiFetch('/api/proxmox/resources')
        if (res.ok) {
          this.resources = await res.json() || []
          this.wsStatus = 'connected'
        }
      } catch (e) { /* WS prendra le relais */ }
    },

    async connectWS() {
      const proto = location.protocol === 'https:' ? 'wss' : 'ws'
      const token = encodeURIComponent(localStorage.getItem('pxp_token') || '')
      const url   = `${proto}://${location.host}/ws/proxmox?token=${token}`
      this._unsubWS = await WsProxy.subscribe(
        'proxmox', url,
        (data) => {
          const msg = JSON.parse(data)
          if (msg.type === 'resources_update') { this.resources = msg.payload || []; this.wsStatus = 'connected' }
        },
        (status) => { this.wsStatus = status }
      )
    },

    async action(vmid, type, action) {
      const key = `${vmid}-${action}`
      this.actionLoading[key] = true
      try {
        const res = await apiFetch(`/api/proxmox/${type}/${vmid}/${action}`, { method: 'POST' })
        if (res.ok) {
          Alpine.store('toasts').success(`Action « ${action} » envoyée sur ${type} ${vmid}`)
        } else {
          const d = await res.json().catch(() => ({}))
          Alpine.store('toasts').error(d.error || `Erreur action ${action} (HTTP ${res.status})`)
        }
      } catch(e) {
        Alpine.store('toasts').error(`Erreur réseau — ${e.message}`)
      } finally {
        this.actionLoading[key] = false
      }
    },

    cpuColor(pct) {
      if (pct > 80) return 'var(--neu-danger)'
      if (pct > 50) return 'var(--neu-warning)'
      return 'var(--neu-success)'
    },

    formatMem(bytes) {
      if (!bytes) return '0 MB'
      return Math.round(bytes / 1024 / 1024) + ' MB'
    },

    t(key) { return Alpine.store('i18n').t(key) },
  }))

  // ── Composant: updatesPage ──────────────────────────────────────────────
  Alpine.data('updatesPage', () => ({
    targets: [],
    loading: true,
    ws: null,
    currentJob: null,
    output: '',
    jobStatus: '',
    activeTab: 'targets',
    history: [],
    historyLoading: false,

    async init() {
      await this.loadTargets()
      await this.checkAll()
    },

    destroy() { if (this.ws) this.ws.close() },

    async loadTargets() {
      this.loading = true
      try {
        const res = await apiFetch('/api/proxmox/resources')
        if (res.ok) {
          const resources = await res.json() || []
          this.targets = [
            { id: 'host', name: 'Proxmox Host', status: 'running', packages: null, checking: false, updating: false },
            ...resources
              .filter(r => r.type === 'lxc')
              .map(r => ({
                id: `lxc:${r.vmid}`,
                name: r.name || `LXC ${r.vmid}`,
                status: r.status,
                vmid: r.vmid,
                packages: null,
                checking: false,
                updating: false,
              })),
          ]
        }
      } catch(e) {
        console.error('loadTargets', e)
      } finally {
        this.loading = false
      }
    },

    async checkTarget(target) {
      if (target.status !== 'running') return  // container arrêté → pas de SSH possible
      target.checking = true
      target.packages = null
      try {
        const res = await apiFetch(`/api/updates/packages?target=${encodeURIComponent(target.id)}`)
        if (res.ok) {
          target.packages = await res.json()
        } else {
          target.packages = []
        }
      } catch(e) {
        console.error('checkTarget', e)
        target.packages = []
      } finally {
        target.checking = false
      }
    },

    async checkAll() {
      for (const t of this.targets) {
        await this.checkTarget(t)
      }
    },

    async updateTarget(target) {
      target.updating = true
      this.output = ''
      this.jobStatus = 'running'
      try {
        const res = await apiFetch('/api/updates/run', {
          method: 'POST',
          body: JSON.stringify({ target: target.id }),
        })
        if (!res.ok) throw new Error('Erreur démarrage mise à jour')
        const data = await res.json()
        this.currentJob = data.job_id
        await this.watchJob(data.job_id)
        target.packages = []
      } catch(e) {
        this.output += '\n[ERREUR] ' + e.message
        this.jobStatus = 'error'
      } finally {
        target.updating = false
      }
    },

    async updateAll() {
      this.output = ''
      this.jobStatus = 'running'
      try {
        const res = await apiFetch('/api/updates/run', {
          method: 'POST',
          body: JSON.stringify({ target: 'all' }),
        })
        if (!res.ok) throw new Error('Erreur démarrage')
        const data = await res.json()
        this.currentJob = data.job_id
        await this.watchJob(data.job_id)
        for (const t of this.targets) t.packages = []
      } catch(e) {
        this.output += '\n[ERREUR] ' + e.message
        this.jobStatus = 'error'
      }
    },

    watchJob(jobId) {
      return new Promise((resolve) => {
        if (this.ws) this.ws.close()
        const proto = location.protocol === 'https:' ? 'wss' : 'ws'
        const token = localStorage.getItem('pxp_token')
        this.ws = new WebSocket(`${proto}://${location.host}/ws/updates/${jobId}?token=${token}`)
        this.ws.onmessage = (e) => {
          const msg = JSON.parse(e.data)
          // Le backend publie dans msg.payload (pas msg.data)
          if (msg.type === 'update_output') {
            this.output += msg.payload?.chunk || ''
          } else if (msg.type === 'update_done') {
            this.jobStatus = 'success'
            resolve()
          } else if (msg.type === 'update_error') {
            this.jobStatus = 'error'
            this.output += '\n[ERREUR] ' + (msg.payload?.error || '')
            resolve()
          }
        }
        this.ws.onerror = () => { this.jobStatus = 'error'; resolve() }
        this.ws.onclose = () => { if (this.jobStatus === 'running') { this.jobStatus = 'error'; resolve() } }
      })
    },

    async loadHistory() {
      this.historyLoading = true
      try {
        const res = await apiFetch('/api/updates/history')
        if (res.ok) this.history = await res.json() || []
      } catch(e) { /* ignore */ }
      this.historyLoading = false
    },

    formatDate(iso) {
      if (!iso) return '—'
      const d = new Date(iso)
      return d.toLocaleString('fr-FR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' })
    },

    get totalPackages() {
      return this.targets.reduce((sum, t) => sum + (t.packages?.length || 0), 0)
    },

    t(key) { return Alpine.store('i18n').t(key) },
  }))

  // ── Composant: settingsPage ─────────────────────────────────────────────
  Alpine.data('settingsPage', () => ({
    tab: 'general',
    loading: true,
    saving: false,
    saved: false,
    error: '',
    shortcuts: [],
    repairModules: [],
    repairLoading: false,
    resetting: {},
    settings: {
      instance_name: '',
      public_url: '',
      default_lang: 'fr',
      ssh_host: '',
      ssh_username: '',
      ssh_password: '',    // chiffré, laisser vide = pas de changement
      proxmox_url: '',
      proxmox_token: '',   // chiffré, format: user@realm!tokenid=secret
    },

    async init() {
      await this.load()
    },

    async load() {
      this.loading = true
      try {
        const res = await apiFetch('/api/settings')
        if (res.ok) {
          const data = await res.json()
          Object.assign(this.settings, data)
          if (data.dashboard_shortcuts) {
            try { this.shortcuts = JSON.parse(data.dashboard_shortcuts) } catch(e) { this.shortcuts = [] }
          }
        }
      } finally {
        this.loading = false
      }
    },

    async save() {
      this.saving = true
      this.saved = false
      this.error = ''
      try {
        // Backend: PUT /api/settings/{key} avec { value: "..." } — un appel par clé
        const keys = Object.keys(this.settings)
        for (const key of keys) {
          const val = this.settings[key]
          // Ignorer les champs vides pour les secrets (ne pas écraser l'existant)
          if (val === '' && (key === 'ssh_password' || key === 'proxmox_token')) continue
          const res = await apiFetch(`/api/settings/${key}`, {
            method: 'PUT',
            body: JSON.stringify({ value: val }),
          })
          if (!res.ok) {
            const d = await res.json().catch(() => ({}))
            throw new Error(d.error || `Erreur sauvegarde de ${key}`)
          }
        }
        this.saved = true
        setTimeout(() => { this.saved = false }, 3000)
      } catch(e) {
        this.error = e.message
      } finally {
        this.saving = false
      }
    },

    addShortcut() {
      this.shortcuts.push({ label: '', href: '', icon: 'lnid-link-1' })
    },

    removeShortcut(idx) {
      this.shortcuts.splice(idx, 1)
    },

    async saveShortcuts() {
      this.saving = true
      this.saved = false
      this.error = ''
      try {
        const res = await apiFetch('/api/settings/dashboard_shortcuts', {
          method: 'PUT',
          body: JSON.stringify({ value: JSON.stringify(this.shortcuts) }),
        })
        if (!res.ok) {
          const d = await res.json().catch(() => ({}))
          throw new Error(d.error || 'Erreur sauvegarde raccourcis')
        }
        this.saved = true
        setTimeout(() => { this.saved = false }, 3000)
      } catch(e) {
        this.error = e.message
      } finally {
        this.saving = false
      }
    },

    async loadRepair() {
      this.repairLoading = true
      try {
        const res = await apiFetch('/api/repair/modules')
        if (res.ok) {
          const data = await res.json()
          this.repairModules = data.modules || []
        }
      } finally {
        this.repairLoading = false
      }
    },

    async resetModule(mod) {
      if (!confirm(`Supprimer "${mod.name || mod.id}" de la base de données ?\nLe module pourra être réinstallé depuis le Store.`)) return
      this.resetting[mod.id] = true
      try {
        const res = await apiFetch(`/api/repair/modules/${mod.id}`, { method: 'DELETE' })
        if (res.ok) {
          this.repairModules = this.repairModules.filter(m => m.id !== mod.id)
          Alpine.store('toasts').success(`Module "${mod.name || mod.id}" supprimé de la DB`)
        } else {
          const d = await res.json().catch(() => ({}))
          Alpine.store('toasts').error(d.error || 'Erreur suppression')
        }
      } catch(e) {
        Alpine.store('toasts').error(e.message)
      } finally {
        this.resetting[mod.id] = false
      }
    },

    t(key) { return Alpine.store('i18n').t(key) },
  }))

  // ── Composant: modulesPage ──────────────────────────────────────────────
  Alpine.data('modulesPage', () => ({
    modules: [],
    loading: true,
    toggling: {},
    storeModules: [],
    storeLoading: true,
    storeError: '',
    installing: {},
    rebuilding: false,
    rebuildDone: false,
    _pollTimer: null,

    async init() {
      await Promise.all([this.load(), this.loadStore()])
    },

    destroy() {
      if (this._pollTimer) clearInterval(this._pollTimer)
    },

    async load() {
      this.loading = true
      try {
        const res = await apiFetch('/api/modules')
        if (res.ok) this.modules = await res.json() || []
      } finally {
        this.loading = false
      }
    },

    async loadStore() {
      this.storeLoading = true
      this.storeError = ''
      try {
        const res = await apiFetch('/api/registry/modules')
        if (res.ok) {
          const data = await res.json()
          this.storeModules = data.modules || []
          if (data.error) this.storeError = data.error
        } else {
          this.storeError = `Erreur serveur (HTTP ${res.status})`
          this.storeModules = []
        }
      } catch(e) {
        this.storeError = 'Impossible de joindre le store : ' + e.message
        this.storeModules = []
      } finally {
        this.storeLoading = false
      }
    },

    async toggle(mod) {
      this.toggling[mod.id] = true
      try {
        const action = mod.is_enabled ? 'disable' : 'enable'
        const res = await apiFetch(`/api/modules/${mod.id}/${action}`, { method: 'POST' })
        if (res.ok) {
          const data = await res.json().catch(() => ({}))
          mod.is_enabled = !mod.is_enabled
          if (data.restarting) {
            Alpine.store('toasts').info('Redémarrage du container en cours…')
            this._startRebuildPoll()
          }
          // Rafraîchir la sidebar
          const sb = document.querySelector('[x-data="sidebar()"]')
          if (sb) {
            const sidebarData = Alpine.$data(sb)
            if (sidebarData?.refreshNav) await sidebarData.refreshNav()
          }
        }
      } catch(e) {
        console.error(e)
      } finally {
        this.toggling[mod.id] = false
      }
    },

    async install(mod) {
      this.installing[mod.id] = true
      try {
        const res = await apiFetch(`/api/registry/modules/${mod.id}/install`, { method: 'POST' })
        if (res.ok) {
          const data = await res.json().catch(() => ({}))
          mod.installed = true
          if (data.rebuilding) {
            this.rebuilding = true
            Alpine.store('toasts').info(`Module ${mod.id} installé — rebuild en cours (~2 min)`)
            this._startRebuildPoll()
          } else {
            Alpine.store('toasts').success(`Module ${mod.id} installé`)
          }
          await this.load()
        } else {
          const b = await res.json().catch(() => ({}))
          Alpine.store('toasts').error(b.error || 'Erreur installation')
        }
      } catch(e) {
        Alpine.store('toasts').error(e.message)
      } finally {
        this.installing[mod.id] = false
      }
    },

    // Poll /api/health toutes les 3s pour détecter le retour du container après rebuild/restart.
    _startRebuildPoll() {
      if (this._pollTimer) return
      // Attendre 5s avant de commencer à poller (le container est encore en train de s'arrêter)
      setTimeout(() => {
        this._pollTimer = setInterval(async () => {
          try {
            const res = await fetch('/api/health')
            if (res.ok) {
              clearInterval(this._pollTimer)
              this._pollTimer = null
              this.rebuilding = false
              this.rebuildDone = true
            }
          } catch(_) { /* container encore hors ligne, on attend */ }
        }, 3000)
      }, 5000)
    },

    t(key) { return Alpine.store('i18n').t(key) },
  }))

  // Note: servicePage et logsPage ont été déplacés dans les modules
  // indépendants viewServices et viewLogs.

}) // end alpine:init

// ── DOMContentLoaded : init stores + Swup ─────────────────────────────────

document.addEventListener('DOMContentLoaded', async () => {
  // Init stores
  await Alpine.store('i18n').init()
  await Alpine.store('auth').init()
  Alpine.store('ui').init()

  const publicPages = ['login', 'install', 'index', '']
  const currentPage = window.location.pathname.replace(/^\/|\.html$/g, '') || 'index'

  // Guard auth : si pas authentifié (token absent ou invalid/expiré), redirect login
  if (!publicPages.includes(currentPage) && !Alpine.store('auth').isAuthenticated) {
    window.location.href = '/login.html'
    return
  }

  // Redirect depuis index
  if (currentPage === 'index' || currentPage === '') {
    const res = await fetch('/api/install/status').catch(() => null)
    if (res && res.ok) {
      const data = await res.json().catch(() => ({}))
      window.location.href = data.installed ? '/login.html' : '/install.html'
    } else {
      window.location.href = '/login.html'
    }
    return
  }

  // Init Swup pour transitions de page
  if (typeof Swup !== 'undefined') {
    const swup = new Swup({
      containers: ['#swup'],
      animationSelector: '[class*="transition-"]',
    })

    window.swup = swup

    // Guard auth sur navigation
    swup.hooks.on('visit:start', (visit) => {
      const dest = new URL(visit.to.url, location.href).pathname
        .replace(/^\/|\.html$/g, '') || 'index'
      if (!publicPages.includes(dest) && !Alpine.store('auth').isAuthenticated) {
        visit.abort()
        window.location.href = '/login.html'
      }
    })

    // Destroy Alpine scope de l'ancien contenu AVANT le swap
    swup.hooks.on('animation:out:end', () => {
      const container = document.getElementById('swup')
      if (container && typeof Alpine.destroyTree === 'function') {
        Alpine.destroyTree(container)
      }
    })

    // Init Alpine sur le nouveau contenu APRÈS le swap
    swup.hooks.on('content:replace', () => {
      const container = document.getElementById('swup')
      if (container) {
        Alpine.initTree(container)
      }
      // Update current page dans UI store
      Alpine.store('ui').currentPage =
        window.location.pathname.replace(/^\/|\.html$/g, '') || 'index'
    })
  }

  // HTMX : inject Authorization header sur toutes les requêtes
  document.addEventListener('htmx:configRequest', (e) => {
    const token = localStorage.getItem('pxp_token')
    if (token) {
      e.detail.headers['Authorization'] = 'Bearer ' + token
    }
  })
})
