/**
 * Fichier  : assets/js/pages/scanner.js
 * Auteur   : KREMER Régis
 * Version  : 2.2.0
 * Date     : 2026-02-21
 * Description : JS — Scanner QR/barcode : détection, création produit, intégration mouvements
 *
 * Changelog :
 *   v2.1.0 (2026-02-21) — Version initiale auditée et sécurisée
 *   v2.1.1 (2026-02-21) — Corrections sécurité, utils.js centralisé
 *   v2.2.0 (2026-02-21) — Permissions magasins, bons de commande, rapport d'écart,
 *                      import CSV, historique prix, bon de livraison multi-lignes
 */
// ================================================================
// scanner.js — Page Scanner (camera barcode + QR, image decode, manual entry)
// Depends on: Api, Modal, toast, ScannerService, Icons, renderIcons
// ================================================================
const ScanPage = {
  recentScans: [],
  _prodCache: {},
  _lastCode: null,
  _lastTime: 0,
  _cameraRunning: false,
  _cameraMode: null, // 'barcode' | 'qrcode'
  _terrainMode: false,
  _terrainPid: null,
  _terrainP: null,

  async init() {

    // Buttons (safe bindings: elements may not exist if template changes)
    const btnStart = document.getElementById("btnStartScan");
    const btnStop = document.getElementById("btnStopScan");
    const btnImg = document.getElementById("btnScanImage");
    const btnQr = document.getElementById("btnScanQR");
    const btnView = document.getElementById("btnScanView");
    const inpFile = document.getElementById("scanFileInput");
    const btnManual = document.getElementById("btnManualSearch");
    const inpManual = document.getElementById("manualCode");
    const btnTerrain = document.getElementById("btnTerrainMode");

    if (btnStart) btnStart.onclick = () => this.startBarcodeCamera();
    if (btnStop) btnStop.onclick = () => this.stopCamera();
    if (btnImg && inpFile) btnImg.onclick = () => inpFile.click();
    if (inpFile) inpFile.onchange = (e) => this.decodeImage(e);
    if (btnQr) btnQr.onclick = () => this.openQrScanModal();
    if (btnManual) btnManual.onclick = () => this.manualSearch();
    if (inpManual) inpManual.onkeydown = (e) => { if (e.key === "Enter") this.manualSearch(); };
    if (btnView) btnView.onclick = () => this.toggleResultCard(true);
    if (btnTerrain) btnTerrain.onclick = () => this.toggleTerrainMode();

    // Keyboard shortcuts (Lot 5)
    document.addEventListener('keydown', (e) => {
      // Avoid hijacking when typing
      const t = e.target;
      const isTyping = t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable);
      if (isTyping) return;
      if (e.key === ' '){
        e.preventDefault();
        if (this._cameraRunning) this.stopCamera(true); else this.startBarcodeCamera();
      }
      if (e.key === 't' || e.key === 'T') this.toggleTerrainMode();
    });

    // Auto-sync offline queue when back online
    window.addEventListener('online', () => this.syncOfflineQueue());

    this.renderRecent();

    // Show sync button if queue exists
    this._refreshOfflineUi();

    if (typeof ScannerService === "undefined") {
      console.error("ScannerService not loaded");
      toast("Bibliothèque de scan non chargée.", "warning");
    }
  },

  // ─────────────────────────────────────────────────────────────
  // UI helpers
  // ─────────────────────────────────────────────────────────────
  toggleResultCard(show) {
    const card = document.getElementById("scanResultCard");
    if (!card) return;
    card.style.display = show ? "" : "none";
  },

  // ─────────────────────────────────────────────────────────────
  // Lot 5 — Terrain mode + offline queue
  // ─────────────────────────────────────────────────────────────
  _isMobile() {
    try { return window.matchMedia && window.matchMedia('(max-width: 768px)').matches; }
    catch(_) { return false; }
  },

  toggleTerrainMode(force) {
    const on = (typeof force === 'boolean') ? force : !this._terrainMode;
    this._terrainMode = on;
    document.body.classList.toggle('sm-terrain-mode', on);

    const btnTerrain = document.getElementById('btnTerrainMode');
    if (btnTerrain) btnTerrain.classList.toggle('btn-primary', on);

    // Best effort fullscreen on mobile
    try {
      if (on && document.documentElement.requestFullscreen) document.documentElement.requestFullscreen().catch(()=>{});
      if (!on && document.fullscreenElement && document.exitFullscreen) document.exitFullscreen().catch(()=>{});
    } catch(_){ }

    // Terrain panel visibility depends on having a product selected
    this._updateTerrainPanelVisibility();
  },

  _updateTerrainPanelVisibility(){
    const panel = document.getElementById('terrainPanel');
    if (!panel) return;
    panel.style.display = (this._terrainMode && this._terrainPid) ? '' : 'none';
  },

  _offlineKey(){ return 'sm_offline_movements_v1'; },
  _getOfflineQueue(){
    try {
      const raw = localStorage.getItem(this._offlineKey());
      const arr = raw ? JSON.parse(raw) : [];
      return Array.isArray(arr) ? arr : [];
    } catch(_){ return []; }
  },
  _setOfflineQueue(arr){
    try { localStorage.setItem(this._offlineKey(), JSON.stringify(arr || [])); } catch(_){ }
  },
  _queueMovement(payload){
    const q = this._getOfflineQueue();
    q.push({ id: Date.now() + '-' + Math.random().toString(16).slice(2), at: Date.now(), payload });
    this._setOfflineQueue(q);
    this._refreshOfflineUi();
  },
  _refreshOfflineUi(){
    const btn = document.getElementById('tsSync');
    const hint = document.getElementById('tsHint');
    const q = this._getOfflineQueue();
    if (btn) btn.style.display = q.length ? '' : 'none';
    if (hint) {
      if (!q.length) { hint.style.display = 'none'; hint.textContent = ''; }
      else {
        hint.style.display = '';
        hint.textContent = `Hors ligne : ${q.length} mouvement(s) en attente de synchronisation.`;
      }
    }
    if (btn) btn.onclick = () => this.syncOfflineQueue();
  },

  async syncOfflineQueue(){
    const q = this._getOfflineQueue();
    if (!q.length) return;
    if (!navigator.onLine) {
      toast('Toujours hors ligne — synchronisation impossible.', 'warning');
      this._refreshOfflineUi();
      return;
    }
    let ok = 0;
    const rest = [];
    for (const it of q){
      try {
        await Api.mvAdd(it.payload);
        ok++;
      } catch(e){
        // keep for later
        rest.push(it);
      }
    }
    this._setOfflineQueue(rest);
    this._refreshOfflineUi();
    if (ok) toast(`Synchronisé : ${ok} mouvement(s).`, 'success');
    if (rest.length) toast(`Restant : ${rest.length} mouvement(s) en attente.`, 'warning');
  },

  _setResult({ status, title, text, actionsHtml }) {
    const card = document.getElementById("scanResultCard");
    const t = document.getElementById("scanResultTitle");
    const s = document.getElementById("scanResultText");
    const a = document.getElementById("scanResultActions");
    if (!card || !t || !s || !a) return;

    card.classList.remove("is-found", "is-notfound", "is-error");
    if (status) card.classList.add(status);
    t.textContent = title || "";
    s.textContent = text || "";
    a.innerHTML = actionsHtml || "";

    this.toggleResultCard(true);
    if (typeof renderIcons === "function") renderIcons(card);
    try{
      if (window.matchMedia && window.matchMedia("(max-width: 768px)").matches) {
        card.scrollIntoView({ behavior: "smooth", block: "nearest" });
      }
    }catch(e){}
  },

  _setCameraUi(running, mode) {
    const btnStart = document.getElementById("btnStartScan");
    const btnStop  = document.getElementById("btnStopScan");
    const preview  = document.getElementById("scannerPreview");
    const placeholder = document.getElementById("scannerPlaceholder");

    // ── Boutons ──────────────────────────────────────────────────
    if (btnStart) {
      btnStart.style.display = running ? "none" : "";
    }
    if (btnStop) {
      btnStop.style.display = running ? "" : "none";
    }

    // ── Placeholder ──────────────────────────────────────────────
    if (placeholder) placeholder.style.display = running ? "none" : "";

    if (!preview) return;

    // ── data-scan-mode → CSS driven show/hide de la frame ────────
    preview.dataset.scanMode = running ? (mode || "barcode") : "";

    const frame = preview.querySelector(".scanner-frame");
    if (!frame) return;

    if (!running) {
      frame.style.inset = "";
      return;
    }

    // ── Injection des 2 coins bas (une seule fois) ────────────────
    if (!frame.querySelector(".scanner-frame-bot")) {
      const bot = document.createElement("span");
      bot.className = "scanner-frame-bot";
      frame.appendChild(bot);
    }

    // ── Alignement de la frame sur le vrai qrbox html5-qrcode ────
    // html5-qrcode génère un div qrbox dont on lit les coords réelles.
    // Si non disponible (délai), on utilise des % calculés.
    this._alignFrame(preview, frame, mode);

    // Retry après que html5-qrcode ait fini d'initialiser (≈500ms)
    setTimeout(() => this._alignFrame(preview, frame, mode), 600);
  },

  // ─────────────────────────────────────────────────────────────
  // Aligne .scanner-frame exactement sur le qrbox rendu par html5-qrcode
  // ─────────────────────────────────────────────────────────────
  _alignFrame(preview, frame, mode) {
    if (!preview || !frame) return;

    const pRect = preview.getBoundingClientRect();
    if (!pRect.width || !pRect.height) return;

    // html5-qrcode injecte un div positionné avec le qrbox
    // On cherche le div fils direct du host qui contient la vidéo
    const host = document.getElementById("scannerH5");
    let qrboxEl = null;
    if (host) {
      // Le qrbox est un div avec style contenant width/height px
      const candidates = host.querySelectorAll("div[style*='width'][style*='height']");
      for (const el of candidates) {
        const st = el.style;
        if (st.width && st.height && !st.width.includes("%")) {
          qrboxEl = el;
          break;
        }
      }
    }

    if (qrboxEl) {
      // Lire les coords réelles du qrbox
      const qRect = qrboxEl.getBoundingClientRect();
      const top    = qRect.top  - pRect.top;
      const left   = qRect.left - pRect.left;
      const right  = pRect.right  - qRect.right;
      const bottom = pRect.bottom - qRect.bottom;
      frame.style.top    = Math.max(0, top)    + "px";
      frame.style.left   = Math.max(0, left)   + "px";
      frame.style.right  = Math.max(0, right)  + "px";
      frame.style.bottom = Math.max(0, bottom) + "px";
    } else {
      // Fallback %  : ScannerService utilise qrbox = 72% du min(w,h)
      const w = pRect.width;
      const h = pRect.height;
      const size = Math.min(w, h) * 0.72;
      const hPad = (w - size) / 2;
      const vPad = (h - size) / 2;
      if (mode === "barcode") {
        // Barcode : large, peu haut (ratio 3:1)
        const bh = size * 0.36;
        frame.style.top    = ((h - bh) / 2) + "px";
        frame.style.left   = hPad + "px";
        frame.style.right  = hPad + "px";
        frame.style.bottom = ((h - bh) / 2) + "px";
      } else {
        // QR : carré
        frame.style.top    = vPad + "px";
        frame.style.left   = hPad + "px";
        frame.style.right  = hPad + "px";
        frame.style.bottom = vPad + "px";
      }
    }
  },

  // ─────────────────────────────────────────────────────────────
  // Camera: BARCODE
  // ─────────────────────────────────────────────────────────────
  async startBarcodeCamera() {
    if (this._cameraRunning) return;

    const preview = document.getElementById("scannerPreview");
    if (!preview) { toast("Zone de prévisualisation introuvable", "error"); return; }
    if (typeof ScannerService === "undefined") { toast("ScannerService indisponible.", "error"); return; }

    // Ensure host for html5-qrcode injected video
    let host = document.getElementById("scannerH5");
    if (!host) {
      host = document.createElement("div");
      host.id = "scannerH5";
      preview.appendChild(host);
    }
    host.innerHTML = ""; // reset

    // Hide legacy <video> placeholder (avoid conflicts)
    const legacyVideo = document.getElementById("scannerVideo");
    if (legacyVideo) {
      legacyVideo.style.display = "none";
      try { legacyVideo.pause(); legacyVideo.srcObject = null; } catch (_) {}
    }

    this._cameraRunning = true;
    this._cameraMode = "barcode";
    this._setCameraUi(true, "barcode");

    try {
      await ScannerService.start({
        target: host,
        mode: "barcode",
        fps: 14,
        onDetected: (code) => this.onScan(code),
        onError: (err) => {
          console.error("Camera error:", err);
          toast("Impossible d'activer la caméra : " + ((err && err.message) ? err.message : err), "error");
          this.stopCamera();
        }
      });
      toast("Caméra active — Scannez un code", "success");
    } catch (e) {
      console.error(e);
      toast("Impossible d'activer la caméra : " + (e.message || e), "error");
      this.stopCamera();
    }
  },

  async stopCamera(silent = false) {
    if (!this._cameraRunning) return;

    this._cameraRunning = false;
    this._cameraMode = null;
    this._setCameraUi(false);

    if (typeof ScannerService !== "undefined") {
      try { await ScannerService.stop(); } catch (_) {}
    }

    const host = document.getElementById("scannerH5");
    if (host) host.innerHTML = "";

    // Restore legacy <video> placeholder if needed
    const legacyVideo = document.getElementById("scannerVideo");
    if (legacyVideo) legacyVideo.style.display = "";

    if (!silent) toast("Caméra arrêtée", "info");
  },

  // ─────────────────────────────────────────────────────────────
  // QR Camera (modal)
  // ─────────────────────────────────────────────────────────────
  async openQrScanModal() {
    if (typeof ScannerService === "undefined") {
      toast("Scanner QR indisponible sur ce navigateur.", "warning");
      return;
    }

    // .qr-scan-wrap contraint le host à un carré responsive (voir CSS)
    const html = `
      <div style="display:flex;flex-direction:column;gap:14px;align-items:center">
        <div class="qr-scan-wrap">
          <div id="scanQrHost"></div>
        </div>
        <p style="font-size:13px;color:var(--text-muted);text-align:center;margin:0">
          Présentez un QR code devant la caméra
        </p>
      </div>
    `;

    Modal.show("Scanner un QR Code", html, [
      { label: "Fermer", cls: "btn-secondary", onclick: async () => { await this._stopQrScan(); Modal.close(); } }
    ], "info");

    await this._startQrScan();
  },

  async _startQrScan() {
    const host = document.getElementById("scanQrHost");
    if (!host) return;

    try {
      await ScannerService.start({
        target: host,
        mode: "qrcode",
        fps: 10,
        onDetected: async (code) => {
          const raw = (code || "").trim();
          if (!raw) return;
          await this._stopQrScan();
          Modal.close();
          this.onScan(raw);
        },
        onError: async (err) => {
          console.error(err);
          toast("Caméra QR indisponible (autorisation / HTTPS requis).", "error");
          await this._stopQrScan();
        }
      });
    } catch (e) {
      console.error(e);
      toast("Caméra QR indisponible.", "error");
      await this._stopQrScan();
    }
  },

  async _stopQrScan() {
    if (typeof ScannerService !== "undefined") {
      try { await ScannerService.stop(); } catch (_) {}
    }
    const host = document.getElementById("scanQrHost");
    if (host) host.innerHTML = "";
  },

  // ─────────────────────────────────────────────────────────────
  // Image decode
  // ─────────────────────────────────────────────────────────────
  async decodeImage(e) {
    const file = e && e.target && e.target.files ? e.target.files[0] : null;
    if (!file) return;
    try {
      if (typeof ScannerService === "undefined") throw new Error("ScannerService indisponible.");
      // ScannerService expose scanImage(file, options) — pas scanImageFile
      let detected = null;
      if (typeof ScannerService.scanImageFile === "function") {
        // ancienne API (assets/js/ScannerService.js)
        detected = await ScannerService.scanImageFile(file);
        if (detected) this.onScan(detected);
        else toast("Aucun code détecté sur cette image", "warning");
      } else if (typeof ScannerService.scanImage === "function") {
        // nouvelle API (assets/js/scanner/ScannerService.js)
        await ScannerService.scanImage(file, {
          mode: "auto",
          onDetected: (code) => { detected = code; this.onScan(code); },
          onError: () => {}
        });
        if (!detected) toast("Aucun code détecté sur cette image", "warning");
      } else {
        throw new Error("Méthode de décodage image indisponible.");
      }
    } catch (err) {
      console.error(err);
      toast("Aucun code détecté sur cette image", "warning");
    } finally {
      try { e.target.value = ""; } catch (_) {}
    }
  },

  // ─────────────────────────────────────────────────────────────
  // Scan handling
  // ─────────────────────────────────────────────────────────────
  async onScan(code) {
    let raw = (code || "").trim();
    if (!raw) return;

    // Dedup rapid fires
    if (this._lastCode === raw && Date.now() - this._lastTime < 800) return;
    this._lastCode = raw;
    this._lastTime = Date.now();

    // ══════════════════════════════════════════════════════════════
    // Parser le QR code s'il est en JSON (format StockMaster)
    // Format: {"type":"product","id":123,"name":"...","sku":"...","barcode":"..."}
    // ══════════════════════════════════════════════════════════════
    let searchTerm = raw;
    let parsedQr = null;
    
    if (raw.startsWith("{") && raw.endsWith("}")) {
      try {
        parsedQr = JSON.parse(raw);
        // Priorité: barcode > sku > id
        if (parsedQr.barcode) {
          searchTerm = parsedQr.barcode;
        } else if (parsedQr.sku) {
          searchTerm = parsedQr.sku;
        } else if (parsedQr.id) {
          searchTerm = String(parsedQr.id);
        }
      } catch (e) {
      }
    }

    try {
      // Chercher par le terme extrait
      let prods = await Api.products(searchTerm);
      let has = Array.isArray(prods) && prods.length > 0;
      
      // Si pas trouvé et qu'on a un ID du QR, chercher par ID
      if (!has && parsedQr && parsedQr.id) {
        const allProds = await Api.products();
        const byId = allProds.find(p => String(p.id) === String(parsedQr.id));
        if (byId) {
          prods = [byId];
          has = true;
        }
      }

      if (has) {
        const p = prods[0];
        this._prodCache[String(p.id)] = p;
        this._terrainPid = String(p.id);
        this._terrainP = p;
        
        // ✅ Son de succès
        this.playSuccessSound();

        this._setResult({
          status: "is-found",
          title: p.name || "Produit trouvé",
          text: p.sku || p.barcode || searchTerm,
          actionsHtml: `
            <button class="btn btn-success btn-sm" id="btnScanAdd">＋ Ajout</button>
            <button class="btn btn-danger btn-sm" id="btnScanRemove">− Sortie</button>
            <button class="btn btn-secondary btn-sm" id="btnScanQrInternal">QR interne</button>
            <button class="btn btn-secondary btn-sm" id="btnScanRescan">Rescanner</button>
          `
        });

        // Bind actions
        const bAdd = document.getElementById("btnScanAdd");
        const bRem = document.getElementById("btnScanRemove");
        const bQr  = document.getElementById("btnScanQrInternal");
        const bRe = document.getElementById("btnScanRescan");
        if (bAdd) bAdd.onclick = () => this.quickMove(String(p.id), "add");
        if (bRem) bRem.onclick = () => this.quickMove(String(p.id), "remove");
        if (bQr)  bQr.onclick  = () => this.showInternalQR(p);
        if (bRe) bRe.onclick = () => this.scanAgain();

        // Terrain panel (Lot 5)
        await this._setupTerrainPanel(p);
        this._updateTerrainPanelVisibility();

        // Recent
        this.recentScans.unshift({ code: searchTerm, name: p.name || "", sku: p.sku || "", time: Date.now() });
        if (this.recentScans.length > 15) this.recentScans.pop();
        this.renderRecent();
      } else {
        // ❌ Son d'erreur
        this.playErrorSound();
        
        const canCreate = (window.SM_ROLE === "admin");
        this._setResult({
          status: "is-notfound",
          title: "Code non trouvé",
          text: searchTerm,
          actionsHtml: `
            ${canCreate ? `<button class="btn btn-primary btn-sm" id="btnScanCreate">Créer</button>` : ""}
            <button class="btn btn-secondary btn-sm" id="btnScanRescan">Rescanner</button>
          `
        });

        this._terrainPid = null;
        this._terrainP = null;
        this._updateTerrainPanelVisibility();

        const bCreate = document.getElementById("btnScanCreate");
        const bRe = document.getElementById("btnScanRescan");
        if (bCreate) bCreate.onclick = () => this.createFromScan(searchTerm);
        if (bRe) bRe.onclick = () => this.scanAgain();
      }
    } catch (err) {
      console.error(err);
      // ❌ Son d'erreur
      this.playErrorSound();
      toast("Erreur recherche produit: " + (err.message || err), "error");
    }
  },

  manualSearch() {
    const inp = document.getElementById("manualCode");
    const code = (inp ? inp.value : "").trim();
    if (code) this.onScan(code);
  },

  scanAgain() {
    this._lastCode = null;
    this._lastTime = 0;
    this._setResult({
      status: "",
      title: "Scanner prêt",
      text: "Scannez un nouveau code.",
      actionsHtml: ""
    });

    this._terrainPid = null;
    this._terrainP = null;
    this._updateTerrainPanelVisibility();
  },

  async _setupTerrainPanel(p){
    const panel = document.getElementById('terrainPanel');
    if (!panel) return;

    // Populate warehouses
    const whs = await Api.warehouses();
    const tsWh = document.getElementById('tsWh');
    if (tsWh) {
      tsWh.innerHTML = (whs || []).map(w => `<option value="${w.id}">${this._esc(w.name || ('#'+w.id))}</option>`).join('');
    }

    // Units
    const tsUnit = document.getElementById('tsUnit');
    if (tsUnit) tsUnit.innerHTML = this._buildUnitOptions(p);

    // Locations per warehouse
    const loadLocs = async () => {
      const wid = tsWh ? tsWh.value : '';
      const tsLoc = document.getElementById('tsLoc');
      if (!tsLoc) return;
      try {
        const locs = await Api.locations({ warehouseId: wid });
        const arr = Array.isArray(locs) ? locs : (locs && locs.items ? locs.items : []);
        const filtered = (arr || []).filter(l => String(l.warehouseId||'') === String(wid));
        const list = filtered.length ? filtered : [{ key:'default', label:'Défaut (sans emplacement)' }];
        tsLoc.innerHTML = list.map(l => {
          const key = l.key || l.id || 'default';
          const label = l.label || l.name || key;
          return `<option value="${this._esc(String(key))}">${this._esc(String(label))}</option>`;
        }).join('');
      } catch(_){
        // fail-soft
        tsLoc.innerHTML = `<option value="default">Défaut (sans emplacement)</option>`;
      }
    };
    if (tsWh) tsWh.onchange = () => loadLocs();
    await loadLocs();

    // Qty quick buttons
    const qInp = document.getElementById('tsQty');
    const bump = (delta) => {
      if (!qInp) return;
      const v = parseFloat(qInp.value || '0') || 0;
      const nv = Math.max(0, v + delta);
      qInp.value = String(nv);
    };
    const m1 = document.getElementById('tsMinus1');
    const p1 = document.getElementById('tsPlus1');
    const p5 = document.getElementById('tsPlus5');
    if (m1) m1.onclick = () => bump(-1);
    if (p1) p1.onclick = () => bump(+1);
    if (p5) p5.onclick = () => bump(+5);

    // Actions
    const doAdd = document.getElementById('tsDoAdd');
    const doRem = document.getElementById('tsDoRemove');
    const doQr = document.getElementById('tsQr');
    if (doQr) doQr.onclick = () => this.showInternalQR(p);
    if (doAdd) doAdd.onclick = () => this._terrainMove('add');
    if (doRem) doRem.onclick = () => this._terrainMove('remove');

    this._refreshOfflineUi();
  },

  async _terrainMove(type){
    const p = this._terrainP;
    const pid = this._terrainPid;
    if (!p || !pid) return;
    const wid = (document.getElementById('tsWh') || {}).value;
    const locKey = (document.getElementById('tsLoc') || {}).value || 'default';
    const unit = (document.getElementById('tsUnit') || {}).value || this._getBaseUnit(p);
    const qty = parseFloat((document.getElementById('tsQty') || {}).value || '0') || 0;
    if (qty <= 0){ toast('Quantité > 0', 'error'); return; }

    const payload = {
      productId: parseInt(pid, 10),
      warehouseId: parseInt(wid, 10),
      type,
      quantity: qty,
      unit,
      locationId: String(locKey || ''),  // API attend locationId (pas locationKey)
      note: 'Scan terrain'
    };

    // If offline, queue it
    if (!navigator.onLine){
      this._queueMovement(payload);
      toast('Hors ligne : mouvement enregistré et mis en attente.', 'warning');
      return;
    }

    try {
      await Api.mvAdd(payload);
      toast(type === 'add' ? 'Ajout enregistré.' : 'Sortie enregistrée.', 'success');
    } catch(e){
      const msg = (e && e.message) ? e.message : String(e);
      // Distinguish business errors (server responded with error) from network failures.
      // Business errors contain specific keywords from the API; network errors are generic.
      const isBusinessError = msg.includes('Stock') || msg.includes('réserv') || msg.includes('insuffisant')
                           || msg.includes('Accès') || msg.includes('obligatoire') || msg.includes('invalide')
                           || msg.includes('bloqué') || msg.includes('Lot') || msg.includes('Permission');
      if (isBusinessError) {
        // Show the server error message clearly — do NOT queue
        toast(msg, 'error', 6000);
      } else {
        // True network/parse error — queue for later sync
        this._queueMovement(payload);
        toast('Mouvement mis en attente (erreur réseau).', 'warning');
      }
    }
  },

  async showInternalQR(p){
    const payload = JSON.stringify({ type:'product', id:p.id, sku:p.sku||'', barcode:p.barcode||'', name:p.name||'' });
    const html = `
      <div class="qr-internal-wrap">
        <div id="smQrBox"></div>
        <div class="qr-internal-label">
          QR interne StockMaster — ${this._esc(p.sku || p.barcode || p.name || '')}
        </div>
      </div>
    `;
    Modal.show('QR interne', html, [
      { label:'Imprimer', cls:'btn-primary',   onclick: () => { try { window.print(); } catch(_){} } },
      { label:'Fermer',   cls:'btn-secondary', onclick: () => Modal.close() },
    ], 'info');

    if (typeof qrcode === 'undefined') {
      await this._loadScriptOnce('libs/qrcode.js');
    }
    try {
      const el = document.getElementById('smQrBox');
      if (!el) return;
      const qr = qrcode(0, 'M');
      qr.addData(payload);
      qr.make();
      el.innerHTML = qr.createImgTag(5);
      const img = el.querySelector('img');
      if (img) { img.style.cssText = 'width:100%;height:100%;display:block;object-fit:contain'; }
    } catch(e){
      console.error(e);
      toast('Impossible de générer le QR.', 'error');
    }
  },

  _esc(s){
    return String(s ?? '').replace(/[&<>"]/g, (c)=>({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;' }[c]));
  },

  // ─────────────────────────────────────────────────────────────
  // Quick move (kept compatible with existing module)
  // ─────────────────────────────────────────────────────────────
  async _getProductById(pid) {
    const k = String(pid);
    if (this._prodCache[k]) return this._prodCache[k];
    const all = await Api.products();
    const p = all.find(x => String(x.id) === k);
    if (p) this._prodCache[k] = p;
    return p || null;
  },

  _getBaseUnit(p) {
    const u = (p && p.packBaseUnit ? String(p.packBaseUnit) : "").trim();
    if (u) return u;
    const legacy = (p && p.unitType ? String(p.unitType) : "piece");
    if (legacy === "liter") return "litre";
    return legacy || "piece";
  },

  _buildUnitOptions(p) {
    const units = [
      { key: "piece", label: "pièce" },
      { key: "carton", label: "carton" },
      { key: "palette", label: "palette" },
      { key: "bidon", label: "bidon" },
      { key: "fut", label: "fût" },
      { key: "litre", label: "litre" },
      { key: "kg", label: "kg" }
    ];

    const base = this._getBaseUnit(p);
    const hasCarton = (parseFloat(p && p.packCartonQty) || 0) > 0;
    const hasPalette = (parseFloat(p && p.packPaletteQty) || 0) > 0;
    const hasBidon = (parseFloat(p && p.packBidonQty) || 0) > 0;
    const hasFut = (parseFloat(p && p.packFutQty) || 0) > 0;

    const allowed = new Set([base]);
    if (base === "piece") allowed.add("piece");
    if (hasCarton) allowed.add("carton");
    if (hasPalette) allowed.add("palette");
    if (hasBidon) allowed.add("bidon");
    if (hasFut) allowed.add("fut");

    return units
      .filter(u => allowed.has(u.key))
      .map(u => `<option value="${u.key}">${u.label}</option>`)
      .join("");
  },

  _getUnitCostPrefill(p, unit) {
    const map = (p && typeof p.costByUnit === "object" && p.costByUnit) ? p.costByUnit : {};
    const v = map ? map[unit] : null;
    if (v != null && v !== "" && !Number.isNaN(parseFloat(v))) return parseFloat(v);
    const base = this._getBaseUnit(p);
    if (unit === base && !Number.isNaN(parseFloat(p && p.cump))) return parseFloat(p.cump);
    if (unit === base && !Number.isNaN(parseFloat(p && p.cost))) return parseFloat(p.cost);
    if (!Number.isNaN(parseFloat(p && p.cost))) return parseFloat(p.cost);
    return 0;
  },

  // ─────────────────────────────────────────────────────────────
  // quickMove — Ajout/Sortie rapide depuis le scanner
  // Logique : produit connu (scanné) → choisir le magasin
  // (avec stock affiché) → puis quantité + détails
  // ─────────────────────────────────────────────────────────────
  async quickMove(pid, type) {
    const p = await this._getProductById(pid);
    if (!p) { toast("Produit introuvable", "error"); return; }

    const [stockAll, whs] = await Promise.all([Api.stock(pid), Api.warehouses()]);
    const baseUnit = this._getBaseUnit(p);
    const needsLot = !!p.trackLot;
    const needsExp = !!p.trackExpiry;

    // ── Construction des options magasin avec stock ──────────────
    // Filtrer selon allowedWarehouses du produit
    let allowedWh = p.allowedWarehouses;
    if (typeof allowedWh === 'string') try { allowedWh = JSON.parse(allowedWh||'[]'); } catch(e){ allowedWh=[]; }
    const allowedWhStrs = Array.isArray(allowedWh) ? allowedWh.map(String) : [];
    const eligibleAllWhs = (!allowedWhStrs.length)
        ? whs
        : whs.filter(w => allowedWhStrs.includes(String(w.id)));

    const stockMap = {};
    (stockAll || []).forEach(s => { stockMap[String(s.warehouseId)] = Number(s.quantity) || 0; });

    let eligibleWhs = eligibleAllWhs;
    if (type === 'remove') {
      eligibleWhs = eligibleAllWhs.filter(w => (stockMap[String(w.id)] || 0) > 0);
    }

    const whOpts = eligibleWhs.map(w => {
      const qty = stockMap[String(w.id)] || 0;
      const stockLabel = type === 'remove'
        ? ` — stock : ${qty} ${baseUnit}`
        : qty > 0 ? ` — stock actuel : ${qty} ${baseUnit}` : ' — aucun stock';
      return `<option value="${w.id}">${this._esc(w.name)}${stockLabel}</option>`;
    }).join('');

    if (type === 'remove' && eligibleWhs.length === 0) {
      toast(`Aucun stock disponible pour "${p.name}"`, 'warning');
      return;
    }

    // ── Unités disponibles pour ce produit ───────────────────────
    const unitOpts = this._buildUnitOptions(p);
    const unitCostPrefill = this._getUnitCostPrefill(p, baseUnit);

    // ── Blocs optionnels selon le type ───────────────────────────
    const costBlock = type === 'add' ? `
      <div class="sq-field-group">
        <label class="lbl">Prix d'achat (pour l'unité choisie) — optionnel</label>
        <input type="number" class="inp" id="sqUnitCost" min="0" step="0.0001" value="${unitCostPrefill}">
      </div>` : '';

    const traceBlock = (needsLot || needsExp) ? `
      <div class="sq-sep"></div>
      <div class="sq-section-title">📋 Traçabilité</div>
      <div class="sq-field-group">
        <label class="lbl">Numéro de lot${needsLot ? ' *' : ''}</label>
        <input type="text" class="inp" id="sqLot" placeholder="Ex: LOT-2026-001">
      </div>
      <div class="sq-field-group">
        <label class="lbl">DLUO / Péremption${needsExp ? ' *' : ''}</label>
        <input type="date" class="inp" id="sqExp">
      </div>` : '';

    const removeBlock = type === 'remove' ? `
      <div class="sq-sep"></div>
      <div class="sq-section-title">📋 Destination</div>
      <div class="sq-field-group">
        <label class="lbl">Raison / Destination</label>
        <select class="sel" id="sqReason">
          <option value="Chantier">Chantier</option>
          <option value="Réparation">Réparation</option>
          <option value="Livraison client">Livraison client</option>
          <option value="Consommable">Consommable</option>
          <option value="Autre">Autre</option>
        </select>
      </div>
      <div class="sq-field-group">
        <label class="lbl">Chantier / Lieu</label>
        <input type="text" class="inp" id="sqSite" placeholder="Ex: Chantier Rue Dupont…">
      </div>
      <div class="sq-field-group">
        <label class="lbl">Opérateur</label>
        <input type="text" class="inp" id="sqOper" placeholder="Nom prénom…">
      </div>` : '';

    const noteBlock = `
      <div class="sq-field-group">
        <label class="lbl">Note</label>
        <input type="text" class="inp" id="sqNote">
      </div>`;

    // ── Formulaire en cascade ────────────────────────────────────
    const html = `
<div class="sq-form">

  <!-- Produit — lecture seule (déjà connu via scan) -->
  <div class="sq-product-banner">
    <div class="sq-product-icon">${type === 'add' ? '＋' : '−'}</div>
    <div class="sq-product-info">
      <div class="sq-product-name">${this._esc(p.name || '')}</div>
      <div class="sq-product-sku">${this._esc(p.sku || p.barcode || '')}</div>
    </div>
  </div>

  <!-- ① Magasin -->
  <div class="sq-step" id="sqStepWh">
    <div class="sq-step-header">
      <span class="sq-step-num">1</span>
      <span class="sq-step-label">Magasin *</span>
      <span class="sq-stock-badge" id="sqStockBadge" style="display:none"></span>
    </div>
    <select class="sel" id="sqWh">
      <option value="">— Choisir un magasin —</option>
      ${whOpts}
    </select>
  </div>

  <!-- ② Quantité & Conditionnement (activé après magasin) -->
  <div class="sq-step sq-step-locked" id="sqStepQty">
    <div class="sq-step-header">
      <span class="sq-step-num">2</span>
      <span class="sq-step-label">Quantité & Conditionnement</span>
    </div>
    <div class="sq-qty-row">
      <div>
        <label class="lbl">Unité</label>
        <select class="sel" id="sqUnit" disabled>${unitOpts}</select>
      </div>
      <div>
        <label class="lbl">Quantité *</label>
        <input type="number" class="inp" id="sqQty" min="0.01" step="0.01" value="1" disabled>
      </div>
    </div>
    <small class="sq-conv-hint">Conversion automatique vers l'unité de base (${baseUnit}) lors de l'enregistrement.</small>
    ${costBlock}
  </div>

  <!-- ③ Détails (traçabilité + destination + note) -->
  <div class="sq-step sq-step-locked" id="sqStepDetails">
    <div class="sq-step-header">
      <span class="sq-step-num">3</span>
      <span class="sq-step-label">Détails</span>
    </div>
    ${traceBlock}
    ${removeBlock}
    ${noteBlock}
  </div>

</div>`;

    Modal.show(
      (type === 'add' ? 'Ajout de stock — ' : 'Sortie de stock — ') + this._esc(p.name || ''),
      html,
      [
        { label: 'Annuler', cls: 'btn-secondary', onclick: () => Modal.close() },
        { label: 'Valider', cls: type === 'add' ? 'btn-success' : 'btn-danger', onclick: () => this._submitQuickMove(p, pid, type, baseUnit, needsLot, needsExp) }
      ],
      type === 'add' ? 'success' : 'error'
    );

    // ── Injecter CSS (une seule fois) ────────────────────────────
    this._injectQuickMoveCss();

    // ── Cascade : sélection magasin → débloque qty ───────────────
    const whSel = document.getElementById('sqWh');
    const stepQty = document.getElementById('sqStepQty');
    const stepDetails = document.getElementById('sqStepDetails');
    const badge = document.getElementById('sqStockBadge');

    const unlock = () => {
      const wid = whSel ? whSel.value : '';
      if (!wid) return;

      // Badge stock
      const qty = stockMap[String(wid)] || 0;
      if (badge) {
        badge.style.display = '';
        if (type === 'remove') {
          badge.className = `sq-stock-badge ${qty <= 0 ? 'sq-stock-zero' : qty < (p.minStock || 5) ? 'sq-stock-low' : 'sq-stock-ok'}`;
          badge.textContent = `Stock disponible : ${qty} ${baseUnit}`;
        } else {
          badge.className = 'sq-stock-badge sq-stock-ok';
          badge.textContent = `Stock actuel : ${qty} ${baseUnit}`;
        }
      }

      // Débloquer étape 2
      if (stepQty) stepQty.classList.remove('sq-step-locked');
      ['sqUnit','sqQty'].forEach(id => {
        const el = document.getElementById(id);
        if (el) el.disabled = false;
      });

      // Unité par défaut
      const unitSel = document.getElementById('sqUnit');
      if (unitSel && [...unitSel.options].some(o => o.value === baseUnit)) {
        unitSel.value = baseUnit;
      }

      // Coût préfill si unité change
      if (unitSel) {
        unitSel.onchange = () => {
          const inp = document.getElementById('sqUnitCost');
          if (inp) inp.value = String(this._getUnitCostPrefill(p, unitSel.value) || 0);
        };
      }

      // Débloquer étape 3
      if (stepDetails) stepDetails.classList.remove('sq-step-locked');
      ['sqLot','sqExp','sqNote','sqSite','sqOper','sqReason'].forEach(id => {
        const el = document.getElementById(id);
        if (el && el.disabled !== undefined) el.disabled = false;
      });

      // Focus quantité
      setTimeout(() => { const q = document.getElementById('sqQty'); if (q) q.focus(); }, 60);
    };

    if (whSel) {
      whSel.addEventListener('change', unlock);
      // Si un seul magasin éligible : présélectionner et déverrouiller
      if (eligibleWhs.length === 1) {
        whSel.value = eligibleWhs[0].id;
        unlock();
      }
    }
  },

  // ─────────────────────────────────────────────────────────────
  // Soumission du quickMove
  // ─────────────────────────────────────────────────────────────
  async _submitQuickMove(p, pid, type, baseUnit, needsLot, needsExp) {
    const wid  = (document.getElementById('sqWh') || {}).value;
    const unit = (document.getElementById('sqUnit') || {}).value || baseUnit;
    const qty  = parseFloat((document.getElementById('sqQty') || {}).value) || 0;

    if (!wid) { toast('Choisissez un magasin', 'error'); return; }
    if (qty <= 0) { toast('Quantité > 0', 'error'); return; }

    const lot    = ((document.getElementById('sqLot') || {}).value || '').trim();
    const expiry = (document.getElementById('sqExp') || {}).value || '';
    if (needsLot && !lot)    { toast('Numéro de lot obligatoire pour ce produit', 'error'); return; }
    if (needsExp && !expiry) { toast('Date de péremption obligatoire pour ce produit', 'error'); return; }

    const unitCost = type === 'add'
      ? parseFloat((document.getElementById('sqUnitCost') || {}).value) || 0
      : 0;

    const payload = {
      productId:   parseInt(pid, 10),
      warehouseId: parseInt(wid, 10),
      type,
      quantity: qty,
      unit,
      locationId:  '',   // sans emplacement spécifique → bucket 'default' via getLocationKey('')
      lotNumber:   lot   || null,
      expiryDate:  expiry || null,
      unitCost,
      reason:   (document.getElementById('sqReason') || {}).value || '',
      site:     (document.getElementById('sqSite')   || {}).value || '',
      operator: (document.getElementById('sqOper')   || {}).value || '',
      note:     (document.getElementById('sqNote')   || {}).value || ''
    };

    try {
      await Api.mvAdd(payload);
      toast(type === 'add' ? 'Ajout enregistré.' : 'Sortie enregistrée.', 'success');
      Modal.close();
    } catch(e) {
      toast(e.message || e, 'error');
    }
  },

  // ─────────────────────────────────────────────────────────────
  // CSS du formulaire quickMove (injecté une seule fois)
  // ─────────────────────────────────────────────────────────────
  _injectQuickMoveCss() {
    if (document.getElementById('sq-modal-css')) return;
    const s = document.createElement('style');
    s.id = 'sq-modal-css';
    s.textContent = `
/* ── Produit banner (lecture seule) ────────────────────────── */
.sq-product-banner {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 14px;
  background: var(--bg-elevated);
  border-radius: var(--r-lg, 10px);
  border: 1px solid var(--border);
  margin-bottom: 2px;
}
.sq-product-icon {
  width: 36px; height: 36px;
  border-radius: 50%;
  background: var(--blue-pale, #eff6ff);
  color: var(--blue, #3b82f6);
  display: flex; align-items: center; justify-content: center;
  font-size: 18px; font-weight: 700; flex-shrink: 0;
}
.sq-product-name { font-weight: 600; font-size: 15px; color: var(--text-primary); }
.sq-product-sku  { font-size: 12px; color: var(--text-muted); font-family: monospace; }

/* ── Steps ──────────────────────────────────────────────────── */
.sq-form { display: flex; flex-direction: column; gap: 6px; }
.sq-step {
  border: 1px solid var(--border);
  border-radius: var(--r-lg, 10px);
  padding: 14px 16px;
  background: var(--bg-card);
  transition: opacity .2s, background .2s;
}
.sq-step-locked {
  opacity: .42;
  pointer-events: none;
  background: var(--bg-elevated);
}
.sq-step-header {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 10px;
  flex-wrap: wrap;
}
.sq-step-num {
  width: 24px; height: 24px;
  border-radius: 50%;
  background: var(--blue, #3b82f6);
  color: #fff;
  font-size: 12px; font-weight: 700;
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0;
}
.sq-step-locked .sq-step-num { background: var(--text-muted, #94a3b8); }
.sq-step-label { font-size: 14px; font-weight: 600; color: var(--text-primary); flex: 1; }

/* ── Stock badge ─────────────────────────────────────────────── */
.sq-stock-badge {
  font-size: 12px; font-weight: 600;
  padding: 3px 10px; border-radius: 999px;
}
.sq-stock-ok   { background: var(--green-pale, #dcfce7); color: var(--green, #16a34a); }
.sq-stock-low  { background: var(--yellow-pale, #fef9c3); color: #b45309; }
.sq-stock-zero { background: var(--red-pale, #fee2e2);   color: var(--red, #dc2626); }

/* ── Qty grid ────────────────────────────────────────────────── */
.sq-qty-row {
  display: grid;
  grid-template-columns: 1fr 2fr;
  gap: 12px;
}
@media (max-width: 400px) {
  .sq-qty-row { grid-template-columns: 1fr; }
}
.sq-conv-hint {
  display: block; font-size: 12px; color: var(--text-muted);
  margin-top: 6px; margin-bottom: 4px;
}

/* ── Misc ─────────────────────────────────────────────────────── */
.sq-sep { height: 1px; background: var(--border); margin: 10px 0 4px; }
.sq-section-title { font-size: 13px; font-weight: 700; color: var(--text-secondary); margin-bottom: 8px; }
.sq-field-group { margin-top: 10px; }
.sq-field-group .lbl { display: block; margin-bottom: 4px; }
`;
    document.head.appendChild(s);
  },

  // Create-from-scan preserved (loads products page module)
  async createFromScan(code) {
    if (typeof ProdPage === "undefined") {
      try { await this._loadScriptOnce("assets/js/pages/products.js"); }
      catch (_) { toast("Impossible de charger le module produits", "error"); return; }
    }
    try {
      if (!Array.isArray(ProdPage.cats) || !Array.isArray(ProdPage.whs) || !Array.isArray(ProdPage.suppliers)) {
        const [cats, whs, suppliers] = await Promise.all([Api.categories(), Api.warehouses(), Api.suppliers()]);
        ProdPage.cats = cats; ProdPage.whs = whs; ProdPage.suppliers = suppliers;
      }
      await ProdPage.openModal();
      setTimeout(() => {
        const bc = document.getElementById("pmBarcode");
        if (bc) bc.value = code;
        const name = document.getElementById("pmName");
        if (name) name.focus();
      }, 120);
    } catch (e) {
      console.error(e);
      toast(e.message || "Erreur ouverture création produit", "error");
    }
  },

  _loadScriptOnce(src) {
    return new Promise((resolve, reject) => {
      if (document.querySelector('script[data-sm-dyn="' + src + '"]')) return resolve();
      const s = document.createElement("script");
      s.src = src;
      s.async = true;
      s.dataset.smDyn = src;
      s.onload = () => resolve();
      s.onerror = () => reject(new Error("load failed"));
      document.head.appendChild(s);
    });
  },

  renderRecent() {
    const el = document.getElementById("recentScans");
    if (!el) return;
    if (!this.recentScans.length) { el.innerHTML = '<div class="recent-empty">Aucun scan récent</div>'; return; }
    el.innerHTML = this.recentScans.map(s => {
      const d = new Date(s.time);
      return `<div class="recent-item"><span class="ri-code">${s.code}</span><span class="ri-name">${s.name}</span><span class="ri-time">${d.getHours()}:${String(d.getMinutes()).padStart(2, "0")}</span></div>`;
    }).join("");
  },

  // Sounds
  playSuccessSound() {
    try {
      const audioContext = new (window.AudioContext || window.webkitAudioContext)();
      const oscillator = audioContext.createOscillator();
      const gainNode = audioContext.createGain();
      oscillator.connect(gainNode);
      gainNode.connect(audioContext.destination);
      oscillator.frequency.value = 800;
      oscillator.type = "sine";
      gainNode.gain.setValueAtTime(0.25, audioContext.currentTime);
      gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.18);
      oscillator.start(audioContext.currentTime);
      oscillator.stop(audioContext.currentTime + 0.18);
    } catch (_) {}
  },

  playErrorSound() {
    try {
      const audioContext = new (window.AudioContext || window.webkitAudioContext)();
      const oscillator = audioContext.createOscillator();
      const gainNode = audioContext.createGain();
      oscillator.connect(gainNode);
      gainNode.connect(audioContext.destination);
      oscillator.frequency.value = 220;
      oscillator.type = "sawtooth";
      gainNode.gain.setValueAtTime(0.22, audioContext.currentTime);
      gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.22);
      oscillator.start(audioContext.currentTime);
      oscillator.stop(audioContext.currentTime + 0.22);
    } catch (_) {}
  }
};
