/**
 * Fichier  : assets/js/scanner/ScannerConsumer.js
 * Auteur   : KREMER Régis
 * Version  : 2.6.98
 * Date     : 2026-07-20
 * Description : Composant scanner commun pour les parcours métier MageStock.
 *
 * Changelog :
 *   v2.6.90 (2026-07-20) — SCANNER CONSUMERS MIGRATION3 : surface caméra,
 *                           résolution, saisie manuelle, image, cycle de vie et
 *                           états partagés par Inventaires, Mouvements,
 *                           Commandes et recherche Produits.
 *   v2.6.92 (2026-07-20) — SCANNER UX POLISH OPTIONAL1 : barre caméra commune,
 *                           feedback sonore/haptique unifié et scan en chaîne verrouillé.
 *   v2.6.94 (2026-07-20) — SCANNER UX FINAL FIELD VALIDATION2 : contexte audio amorcé
 *                           au geste utilisateur et suppression des doubles retours adaptateurs.
 *   v2.6.95 (2026-07-20) — SCANNER RESULT DISPLAY FIX3 : carte résultat commune clarifiée.
 *   v2.6.97 (2026-07-20) — SCANNER OVERLAY CONSUMER FIX5 : icônes SVG communes
 *                           et ligne de visée animée dans tous les scanners métier.
 *   v2.6.98 (2026-07-20) — SCANNER TERRAIN CONFIRMATION1 : suppression des glyphes
 *                           variables, balayage relancé à chaque reprise et diagnostics de contexte.
 */
(function scannerConsumerFactory(global, document) {
    'use strict';

    if (!global || !document || global.MageStockScannerConsumer) return;

    const VERSION = '2.6.98';
    let activeHandle = null;
    let sequence = 0;

    const esc = value => String(value == null ? '' : value)
        .replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;')
        .replace(/'/g, '&#039;');


    const STATUS_ICONS = {
        idle: '<svg viewBox="0 0 24 24"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"></path><circle cx="12" cy="13" r="4"></circle></svg>',
        starting: '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"></circle><path d="M12 7v5l3 2"></path></svg>',
        running: '<svg viewBox="0 0 24 24"><path d="M4 7V4h3M17 4h3v3M20 17v3h-3M7 20H4v-3"></path><path d="M7 12h10"></path></svg>',
        resolving: '<svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"></circle><path d="m20 20-4-4"></path></svg>',
        paused: '<svg viewBox="0 0 24 24"><rect x="6" y="5" width="4" height="14"></rect><rect x="14" y="5" width="4" height="14"></rect></svg>',
        success: '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"></circle><path d="m8 12 2.5 2.5L16 9"></path></svg>',
        warning: '<svg viewBox="0 0 24 24"><path d="M12 3 2 21h20L12 3z"></path><path d="M12 9v5M12 18h.01"></path></svg>',
        error: '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"></circle><path d="m9 9 6 6M15 9l-6 6"></path></svg>',
        notfound: '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"></circle><path d="M9.8 9a2.4 2.4 0 1 1 3.7 2c-1 .7-1.5 1.2-1.5 2.5M12 17h.01"></path></svg>'
    };

    function scannerCore() {
        return global.MageStockScanner || global.ScannerService || null;
    }

    const feedback = global.MageStockScannerFeedback || (() => {
        let audioContext = null;
        let lastKind = '';
        let lastAt = 0;

        function context() {
            const Ctx = global.AudioContext || global.webkitAudioContext;
            if (!Ctx) return null;
            if (!audioContext || audioContext.state === 'closed') audioContext = new Ctx();
            if (audioContext.state === 'suspended') audioContext.resume?.().catch(() => {});
            return audioContext;
        }

        function tone(ctx, frequency, start, duration, gain, type) {
            const oscillator = ctx.createOscillator();
            const volume = ctx.createGain();
            oscillator.connect(volume);
            volume.connect(ctx.destination);
            oscillator.frequency.setValueAtTime(frequency, start);
            oscillator.type = type || 'sine';
            volume.gain.setValueAtTime(gain, start);
            volume.gain.exponentialRampToValueAtTime(0.001, start + duration);
            oscillator.start(start);
            oscillator.stop(start + duration);
        }

        function play(kind) {
            const now = Date.now();
            if (kind === lastKind && now - lastAt < 420) return false;
            lastKind = kind;
            lastAt = now;
            try {
                const ctx = context();
                if (ctx) {
                    const start = ctx.currentTime + 0.01;
                    if (kind === 'success') {
                        tone(ctx, 760, start, 0.11, 0.16, 'sine');
                        tone(ctx, 1080, start + 0.1, 0.13, 0.14, 'sine');
                    } else if (kind === 'warning') {
                        tone(ctx, 520, start, 0.13, 0.13, 'triangle');
                        tone(ctx, 420, start + 0.12, 0.15, 0.12, 'triangle');
                    } else {
                        tone(ctx, 230, start, 0.16, 0.14, 'sawtooth');
                        tone(ctx, 170, start + 0.14, 0.18, 0.12, 'sawtooth');
                    }
                }
                if (navigator.vibrate) {
                    if (kind === 'success') navigator.vibrate(45);
                    else if (kind === 'warning') navigator.vibrate([45, 45, 45]);
                    else navigator.vibrate([70, 45, 70]);
                }
            } catch (_) {}
            return true;
        }

        function prime() {
            try { return !!context(); } catch (_) { return false; }
        }

        return {
            version: '2.6.97',
            prime,
            play,
            success: () => play('success'),
            warning: () => play('warning'),
            error: () => play('error')
        };
    })();
    global.MageStockScannerFeedback = feedback;

    function announce(message, priority) {
        if (!message) return;
        if (global.MageStockMobileUI?.announce) global.MageStockMobileUI.announce(message, priority || 'polite');
    }

    function notify(message, type) {
        if (!message) return;
        if (typeof global.toast === 'function') global.toast(message, type || 'info');
        announce(message, type === 'error' ? 'assertive' : 'polite');
    }

    function normalizeResolverOptions(options) {
        if (typeof options?.resolverOptions === 'function') {
            try { return options.resolverOptions() || {}; } catch (_) { return {}; }
        }
        return { ...(options?.resolverOptions || {}) };
    }

    function stateLabel(state) {
        const map = {
            idle: ['Prêt à scanner', 'Activez la caméra ou saisissez un code.'],
            starting: ['Démarrage de la caméra…', 'Autorisez l’accès à la caméra si le navigateur le demande.'],
            running: ['Caméra active', 'Placez le code au centre du cadre.'],
            resolving: ['Code détecté', 'Recherche du produit dans MageStock…'],
            paused: ['Caméra en pause', 'Traitez le produit puis reprenez le scan.'],
            success: ['Produit trouvé', 'Le produit est prêt pour l’action métier.'],
            warning: ['Produit hors périmètre', 'Ce produit existe mais ne peut pas être utilisé ici.'],
            error: ['Scanner indisponible', 'Utilisez la saisie manuelle ou une image.'],
            notfound: ['Code non reconnu', 'Aucun produit MageStock ne correspond à ce code.']
        };
        return map[state] || map.idle;
    }

    function buildContent(options, id) {
        const root = document.createElement('div');
        root.className = 'ms-scanner-consumer';
        root.dataset.scannerContext = options.context || 'general';
        root.innerHTML = `
            <section class="ms-scanner-consumer__camera" aria-label="Zone de scan">
                <div class="ms-scanner-consumer__status" data-scanner-status="idle" role="status" aria-live="polite">
                    <span class="ms-scanner-consumer__status-icon" aria-hidden="true">${STATUS_ICONS.idle}</span>
                    <div><strong>Prêt à scanner</strong><small>Activez la caméra ou saisissez un code.</small></div>
                </div>
                <div class="ms-scanner-consumer__preview" data-scanner-preview>
                    <div id="${id}-target" class="ms-scanner-consumer__target"></div>
                    <div class="ms-scanner-consumer__frame" aria-hidden="true"><span></span></div>
                    <div class="ms-scanner-consumer__placeholder" data-scanner-placeholder>
                        <span class="ms-scanner-consumer__placeholder-icon" aria-hidden="true">${STATUS_ICONS.running}</span>
                        <strong>Caméra prête</strong>
                        <small>Code-barres, EAN, SKU ou QR MageStock</small>
                    </div>
                </div>
                <div class="ms-scanner-consumer__toolbar" aria-label="Outils du scanner">
                    <button type="button" data-scanner-toggle-manual aria-label="Saisir un code manuellement">
                        <span class="ms-scanner-consumer__toolbar-icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M4 4h16v16H4z"></path><path d="M8 9h8M8 13h5"></path></svg></span><small>Saisir</small>
                    </button>
                    <button type="button" class="ms-scanner-consumer__camera-toggle" data-scanner-camera-toggle aria-pressed="false" aria-label="Activer la caméra">
                        <span class="ms-scanner-consumer__toolbar-icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"></path><circle cx="12" cy="13" r="4"></circle></svg></span><small>Caméra</small>
                    </button>
                    ${options.allowImage === false ? '<span aria-hidden="true"></span>' : '<button type="button" data-scanner-image aria-label="Lire un code depuis une image"><span class="ms-scanner-consumer__toolbar-icon" aria-hidden="true"><svg viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><path d="m21 15-5-5L5 21"></path></svg></span><small>Image</small></button>'}
                    <input type="file" accept="image/*" data-scanner-image-input hidden>
                </div>
            </section>
            <section class="ms-scanner-consumer__result" data-scanner-result aria-live="polite"></section>
            <section class="ms-scanner-consumer__manual" data-scanner-manual hidden>
                <label for="${id}-manual">Code-barres, EAN, SKU ou QR</label>
                <div>
                    <input id="${id}-manual" class="form-input" type="search" inputmode="search" enterkeyhint="search" autocomplete="off" autocapitalize="off" spellcheck="false" placeholder="Scanner ou saisir un code">
                    <button type="button" class="btn btn-primary" data-scanner-manual-submit>Rechercher</button>
                </div>
            </section>`;
        return root;
    }

    function openPanel(options, content) {
        const panelOptions = {
            title: options.title || 'Scanner un produit',
            description: options.description || '',
            content,
            trigger: options.trigger,
            className: 'ms-scanner-consumer-panel ' + (options.className || ''),
            closeOnBackdrop: options.closeOnBackdrop !== false,
            closeOnEscape: options.closeOnEscape !== false,
            onClose: payload => options._onPanelClose?.(payload)
        };
        if (global.MobileDetailPage?.open) return global.MobileDetailPage.open(panelOptions);
        if (global.ActionSheet?.open) return global.ActionSheet.open(panelOptions);
        if (global.Modal?.show) {
            const host = document.createElement('div');
            host.appendChild(content);
            global.Modal.show(panelOptions.title, host.innerHTML, [
                { label: 'Fermer', cls: 'btn-secondary', onclick: () => global.Modal.close() }
            ], 'info');
            return {
                body: document.getElementById('modalBody'),
                panel: document.getElementById('modalBox'),
                close: () => global.Modal.close()
            };
        }
        throw new Error('Aucun composant de panneau accessible n’est disponible.');
    }

    function open(rawOptions) {
        const options = {
            context: 'general',
            owner: 'scanner-consumer',
            mode: 'auto',
            fps: 12,
            autoStart: true,
            pauseOnResult: true,
            allowManual: true,
            allowImage: true,
            ...rawOptions
        };
        feedback.prime?.();
        const core = scannerCore();
        if (!core?.start || !core?.resolve) {
            notify('Le noyau scanner commun est indisponible.', 'error');
            return null;
        }

        if (activeHandle) activeHandle.close('replaced');

        const id = 'msScannerConsumer' + (++sequence);
        const content = buildContent(options, id);
        let panel = null;
        let closed = false;
        let starting = false;
        let pausedForResult = false;
        let lastResult = null;
        let processingResult = false;
        let restartPromise = null;

        const handle = {
            version: VERSION,
            options,
            content,
            panel: null,
            root: content,
            target: content.querySelector('#' + id + '-target'),
            result: content.querySelector('[data-scanner-result]'),
            status: content.querySelector('[data-scanner-status]'),
            get closed() { return closed; },
            get running() { return core.getState ? core.getState().running : !!core._running; },
            get lastResult() { return lastResult; },
            setStatus,
            setResult,
            clearResult,
            start,
            stop,
            pause,
            restart,
            resolve,
            close,
            focusManual,
            chooseProduct
        };

        options._onPanelClose = async payload => {
            if (closed) return;
            closed = true;
            try { await core.stop({ reason: payload?.reason || 'panel-close' }); } catch (_) {}
            if (activeHandle === handle) activeHandle = null;
            try { options.onClose?.({ ...(payload || {}), handle }); } catch (error) { console.error(error); }
        };

        panel = openPanel(options, content);
        handle.panel = panel;

        const cameraToggle = content.querySelector('[data-scanner-camera-toggle]');
        const manualSection = content.querySelector('[data-scanner-manual]');
        const manualInput = content.querySelector('#' + id + '-manual');
        const imageInput = content.querySelector('[data-scanner-image-input]');
        const placeholder = content.querySelector('[data-scanner-placeholder]');
        const preview = content.querySelector('[data-scanner-preview]');

        function syncButtons(running) {
            if (placeholder) placeholder.hidden = !!running;
            if (preview) {
                preview.classList.toggle('is-running', !!running);
                preview.dataset.scannerSweep = running ? 'active' : 'idle';
                const sweep = preview.querySelector('.ms-scanner-consumer__frame span');
                if (sweep) {
                    sweep.classList.remove('is-sweep-armed');
                    if (running) {
                        void sweep.offsetWidth;
                        sweep.classList.add('is-sweep-armed');
                    }
                }
            }
            if (cameraToggle) {
                cameraToggle.classList.toggle('is-active', !!running);
                cameraToggle.setAttribute('aria-pressed', running ? 'true' : 'false');
                cameraToggle.setAttribute('aria-label', running ? 'Arrêter la caméra' : 'Activer la caméra');
                const label = cameraToggle.querySelector('small');
                if (label) label.textContent = running ? 'Arrêter' : 'Caméra';
            }
        }

        function setStatus(state, title, detail) {
            const fallback = stateLabel(state);
            const node = handle.status;
            if (!node) return;
            node.dataset.scannerStatus = state || 'idle';
            const icon = node.querySelector('.ms-scanner-consumer__status-icon');
            if (icon) icon.innerHTML = STATUS_ICONS[state] || STATUS_ICONS.idle;
            const strong = node.querySelector('strong');
            const small = node.querySelector('small');
            if (strong) strong.textContent = title || fallback[0];
            if (small) small.textContent = detail || fallback[1];
            announce((title || fallback[0]) + '. ' + (detail || fallback[1]), state === 'error' ? 'assertive' : 'polite');
        }

        function setResult(value) {
            if (!handle.result) return;
            handle.result.innerHTML = '';
            if (value instanceof Node) handle.result.appendChild(value);
            else handle.result.innerHTML = String(value || '');
            handle.result.hidden = !handle.result.childNodes.length;
        }

        function clearResult() {
            lastResult = null;
            if (handle.result) {
                handle.result.innerHTML = '';
                handle.result.hidden = true;
            }
        }

        function currentResolverOptions() {
            return normalizeResolverOptions(options);
        }

        async function routeResult(result, source) {
            if (closed || !result || processingResult) return result;
            processingResult = true;
            lastResult = result;
            if (options.pauseOnResult !== false) await pause('result');
            setStatus('resolving', 'Code détecté', 'Traitement du résultat…');

            try {
                if (result.status === 'found') {
                    setStatus('success', result.product?.name || 'Produit trouvé', 'Le produit est prêt pour la suite.');
                    const response = await options.onResolved?.(result, handle, source || 'camera');
                    feedback.success();
                    if (response !== undefined) return response;
                    renderDefaultProduct(result);
                    return result;
                }
                if (result.status === 'out_of_scope') {
                    feedback.warning();
                    setStatus('warning', result.product?.name || 'Produit hors périmètre', options.outOfScopeMessage || 'Ce produit ne peut pas être utilisé dans ce parcours.');
                    const response = await options.onOutOfScope?.(result, handle, source || 'camera');
                    if (response !== undefined) return response;
                    renderMessage('Produit hors périmètre', options.outOfScopeMessage || 'Ce produit existe dans MageStock, mais il n’appartient pas au périmètre courant.', 'warning');
                    return result;
                }
                if (result.status === 'ambiguous') {
                    setStatus('warning', 'Plusieurs produits correspondent', 'Choisissez le produit exact.');
                    const response = await options.onAmbiguous?.(result, handle, source || 'camera');
                    feedback.warning();
                    if (response !== undefined) return response;
                    renderAmbiguous(result);
                    return result;
                }
                setStatus('notfound', 'Code non reconnu', result.raw || 'Aucun produit correspondant.');
                const response = await options.onNotFound?.(result, handle, source || 'camera');
                feedback.error();
                if (response !== undefined) return response;
                renderMessage('Code non reconnu', result.raw ? `Aucun produit ne correspond à ${result.raw}.` : 'Aucun produit ne correspond à ce code.', 'notfound');
                return result;
            } catch (error) {
                console.error('[MageStockScannerConsumer] routeResult', error);
                feedback.error();
                setStatus('error', 'Erreur de traitement', error?.message || 'Le résultat n’a pas pu être traité.');
                options.onError?.(error, handle);
                return result;
            } finally {
                processingResult = false;
            }
        }

        function renderMessage(title, detail, state) {
            const node = document.createElement('article');
            node.className = 'ms-scanner-consumer__message';
            node.dataset.state = state || 'info';
            node.innerHTML = `<strong>${esc(title)}</strong><p>${esc(detail)}</p><button type="button" class="btn btn-primary" data-scanner-retry>Scanner le suivant</button>`;
            node.querySelector('[data-scanner-retry]')?.addEventListener('click', () => restart());
            setResult(node);
        }

        function renderDefaultProduct(result) {
            const product = result.product || {};
            const node = document.createElement('article');
            node.className = 'ms-scanner-consumer__product';
            const reference = [product.sku || '', product.category || '', product.brand || ''].filter(Boolean).join(' · ');
            node.innerHTML = `<span class="ms-scanner-consumer__product-icon" aria-hidden="true">✓</span><div class="ms-scanner-consumer__product-copy"><small>Produit trouvé</small><strong>${esc(product.name || 'Produit')}</strong><span>${esc(reference || product.barcode || result.raw || '')}</span></div><button type="button" class="btn btn-primary" data-scanner-retry>Scanner le suivant</button>`;
            node.querySelector('[data-scanner-retry]')?.addEventListener('click', () => restart());
            setResult(node);
        }

        function renderAmbiguous(result) {
            const node = document.createElement('div');
            node.className = 'ms-scanner-consumer__choices';
            node.innerHTML = (result.products || []).map((product, index) => `
                <button type="button" data-scanner-product-index="${index}">
                    <span aria-hidden="true">📦</span>
                    <div><strong>${esc(product.name || 'Produit')}</strong><small>${esc(product.sku || product.barcode || '')}</small></div>
                </button>`).join('');
            node.addEventListener('click', event => {
                const button = event.target.closest('[data-scanner-product-index]');
                if (!button) return;
                chooseProduct(Number(button.dataset.scannerProductIndex));
            });
            setResult(node);
        }

        async function chooseProduct(index) {
            const product = lastResult?.products?.[index];
            if (!product) return false;
            const result = { ...lastResult, status: 'found', product, products: [product] };
            return routeResult(result, 'choice');
        }

        async function start() {
            if (closed || starting) return false;
            starting = true;
            pausedForResult = false;
            clearResult();
            setStatus('starting');
            syncButtons(false);
            try {
                await core.start({
                    owner: options.owner,
                    context: options.context,
                    target: handle.target,
                    mode: options.mode,
                    fps: options.fps,
                    managedLifecycle: true,
                    autoResume: true,
                    autoResolve: true,
                    continuous: true,
                    resolverOptions: currentResolverOptions(),
                    onResolved: result => routeResult(result, 'camera'),
                    onOutOfScope: result => routeResult(result, 'camera'),
                    onAmbiguous: result => routeResult(result, 'camera'),
                    onNotFound: result => routeResult(result, 'camera'),
                    onError: error => {
                        setStatus('error', error?.message || 'Caméra indisponible', 'Utilisez la saisie manuelle ou une image.');
                        syncButtons(false);
                        try { options.onError?.(error, handle); } catch (_) {}
                    },
                    onStateChange: state => {
                        if (state.phase === 'running') {
                            setStatus('running');
                            syncButtons(true);
                        } else if (state.phase === 'suspended') {
                            setStatus('paused', 'Caméra suspendue', 'MageStock reprendra la caméra au retour dans l’application.');
                            syncButtons(false);
                        }
                    }
                });
                setStatus('running');
                syncButtons(true);
                return true;
            } catch (error) {
                setStatus('error', error?.message || 'Caméra indisponible', 'Utilisez la saisie manuelle ou une image.');
                syncButtons(false);
                return false;
            } finally {
                starting = false;
            }
        }

        async function stop(reason) {
            try { await core.stop({ reason: reason || 'consumer-stop' }); } catch (_) {}
            syncButtons(false);
            if (!closed) setStatus('idle');
            return true;
        }

        async function pause(reason) {
            pausedForResult = true;
            try { await core.stop({ reason: reason || 'consumer-pause' }); } catch (_) {}
            syncButtons(false);
            if (!closed) setStatus('paused');
            return true;
        }

        async function restart() {
            if (restartPromise) return restartPromise;
            restartPromise = (async () => {
                pausedForResult = false;
                processingResult = false;
                clearResult();
                try { await core.stop({ reason: 'consumer-restart' }); } catch (_) {}
                syncButtons(false);
                await new Promise(resolve => global.setTimeout(resolve, 90));
                return start();
            })();
            try { return await restartPromise; }
            finally { restartPromise = null; }
        }

        async function resolve(raw, source) {
            const value = String(raw || '').trim();
            if (!value) {
                focusManual();
                return null;
            }
            setStatus('resolving', 'Recherche en cours', value);
            try {
                const result = await core.resolve(value, { ...currentResolverOptions(), context: options.context });
                return routeResult(result, source || 'manual');
            } catch (error) {
                setStatus('error', 'Recherche impossible', error?.message || 'Le code n’a pas pu être recherché.');
                options.onError?.(error, handle);
                return null;
            }
        }

        function focusManual() {
            if (!options.allowManual) return;
            manualSection.hidden = false;
            manualInput?.focus();
            manualInput?.select?.();
        }

        function close(reason) {
            if (closed) return;
            panel?.close?.(reason || 'manual');
        }

        cameraToggle?.addEventListener('click', () => { feedback.prime?.(); return handle.running ? stop('button') : start(); });
        content.querySelector('[data-scanner-toggle-manual]')?.addEventListener('click', () => {
            manualSection.hidden = !manualSection.hidden;
            if (!manualSection.hidden) focusManual();
        });
        content.querySelector('[data-scanner-manual-submit]')?.addEventListener('click', () => { feedback.prime?.(); return resolve(manualInput?.value, 'manual'); });
        manualInput?.addEventListener('keydown', event => {
            if (event.key === 'Enter') {
                event.preventDefault();
                feedback.prime?.();
                resolve(manualInput.value, 'manual');
            }
        });
        content.querySelector('[data-scanner-image]')?.addEventListener('click', () => { feedback.prime?.(); imageInput?.click(); });
        imageInput?.addEventListener('change', async () => {
            const file = imageInput.files?.[0];
            if (!file) return;
            setStatus('resolving', 'Analyse de l’image…', 'Recherche d’un code lisible.');
            try {
                const result = await core.scanImage(file, {
                    owner: options.owner,
                    context: options.context,
                    autoResolve: true,
                    resumeCamera: false,
                    resolverOptions: currentResolverOptions()
                });
                await routeResult(result, 'image');
            } catch (error) {
                setStatus('error', error?.message || 'Image illisible', 'Essayez une image plus nette ou la saisie manuelle.');
                options.onError?.(error, handle);
            } finally {
                imageInput.value = '';
            }
        });

        activeHandle = handle;
        if (options.autoStart !== false) global.setTimeout(() => start(), 80);
        else setStatus('idle');
        try { options.onOpen?.(handle); } catch (error) { console.error(error); }
        return handle;
    }

    const api = {
        version: VERSION,
        open,
        get active() { return activeHandle; },
        close(reason) { activeHandle?.close(reason || 'api'); },
        async stop(reason) { return activeHandle?.stop(reason || 'api') || scannerCore()?.stop?.({ reason: reason || 'api' }); },
        feedback
    };

    global.MageStockScannerConsumer = api;
    global.ScannerConsumer = api;
})(window, document);
