{% extends "base.html" %}
{% block title %}Escanear{% endblock %}

{% block head %}
<style>
  body { background: #111; color: #eee; min-height: 100dvh;
         display: flex; align-items: center; justify-content: center; }

  .card { position: relative; background: #1e1e1e; border-radius: 1.5rem;
          padding: 2rem 1.75rem; width: 100%; max-width: 420px; text-align: center;
          box-shadow: 0 4px 24px #0008; }

  /* Header row: lang toggle ↔ net indicator */
  .card-header { position: absolute; top: .8rem; left: 1rem; right: 1rem;
                 display: flex; justify-content: space-between; align-items: center; }
  #netIndicator { font-size: 1.05rem; user-select: none; }

  h1 { font-size: 1.4rem; margin: 1.4rem 0 .25rem; letter-spacing: .04em; }
  .subtitle { font-size: .85rem; opacity: .6; margin-bottom: 1.5rem; min-height: 1.2em; }

  /* Big primary scan button */
  .btn-scan {
    display: block; width: 100%; padding: 1.1rem;
    background: #2563eb; color: #fff; font-size: 1.15rem;
    font-weight: 600; border: none; border-radius: 1rem; cursor: pointer;
    transition: background .2s;
  }
  .btn-scan:hover  { background: #1d4ed8; }
  .btn-scan:active { background: #1e40af; }

  #fileInput { display: none; }

  /* ── Metadata buttons row (Weight / Broken / Waste) ─────────────────── */
  .meta-row { display: grid; grid-template-columns: repeat(3, 1fr);
              gap: .5rem; margin-top: .75rem; }
  .btn-meta { padding: .65rem .25rem; border: none; border-radius: .65rem;
              font-size: .82rem; font-weight: 600; color: #fff;
              cursor: pointer; transition: filter .15s, transform .08s;
              line-height: 1.15; }
  .btn-meta:active:not(:disabled) { transform: scale(.97); }
  .btn-meta:disabled              { opacity: .35; cursor: not-allowed; }
  .btn-meta:not(:disabled):hover  { filter: brightness(1.1); }

  .btn-weight { background: #0d9488; }                  /* teal   */
  .btn-broken { background: #c2410c; }                  /* orange */
  .btn-waste  { background: #b91c1c; }                  /* red    */
  .btn-meta.active { box-shadow: inset 0 0 0 2px #fff, 0 0 0 2px #ffffff55; }

  .spinner { display: none; margin: 1.2rem auto 0; width: 2.5rem; height: 2.5rem;
             border: 4px solid #444; border-top-color: #2563eb;
             border-radius: 50%; animation: spin .8s linear infinite; }
  @keyframes spin { to { transform: rotate(360deg); } }

  /* Result panel */
  #result { margin-top: 1.25rem; padding: 1rem; border-radius: 1rem;
            font-size: .95rem; line-height: 1.45; display: none; }
  #result.PENDING { background: #1e3a8a; color: #bfdbfe; }
  #result.QUEUED  { background: #713f12; color: #fde68a; }
  #result.OK      { background: #14532d; color: #bbf7d0; }
  #result.ERROR   { background: #7f1d1d; color: #fca5a5; }
  .pending-count  { display: block; font-size: .8rem; opacity: .8; margin-top: .25rem; }

  /* ── Offline queue UI ─────────────────────────────────────────── */
  .queue-bar { margin-top: 1.25rem; padding: .75rem 1rem;
               background: #2a2a2a; border: 1px solid #444;
               border-radius: .75rem;
               display: flex; align-items: center; justify-content: space-between; }
  .queue-info  { font-size: .9rem; color: #ddd; }
  .queue-info #queueCount { font-weight: 700; color: #fbbf24; }
  .btn-queue   { background: #16a34a; color: #fff; border: none;
                 padding: .55rem 1rem; border-radius: .5rem;
                 font-weight: 600; cursor: pointer; transition: background .15s; }
  .btn-queue:hover:not(:disabled) { background: #15803d; }
  .btn-queue:disabled             { background: #555; cursor: wait; }

  .queue-list { margin-top: .5rem; display: flex; flex-direction: column; gap: .4rem; }
  .queue-item { display: flex; align-items: center; gap: .6rem;
                background: #222; border: 1px solid #333; border-radius: .5rem;
                padding: .45rem; }
  .queue-item.failed         { border-color: #7f1d1d; background: #2a1818; }
  .queue-item img { width: 44px; height: 44px; border-radius: .3rem;
                    object-fit: cover; flex-shrink: 0; }
  .qi-meta   { flex: 1; text-align: left; line-height: 1.2; min-width: 0; }
  .qi-time   { font-size: .8rem; color: #ddd; }
  .qi-status { font-size: .7rem; color: #888; }
  .queue-item.failed .qi-status { color: #fca5a5; }
  .qi-tags   { font-size: .65rem; color: #fbbf24; margin-top: .15rem; }
  .qi-retry, .qi-del {
    background: #333; color: #fff; border: none;
    padding: .4rem .55rem; border-radius: .3rem;
    font-size: .9rem; cursor: pointer; }
  .qi-retry:hover, .qi-del:hover { background: #444; }
  .qi-retry:disabled             { opacity: .5; cursor: wait; }

  /* ── Weight modal ───────────────────────────────────────────────── */
  .modal-backdrop { display: none; position: fixed; inset: 0;
                    background: rgba(0,0,0,.75); z-index: 1000;
                    align-items: center; justify-content: center; padding: 1rem; }
  .modal-backdrop.open { display: flex; }
  .modal-box  { background: #1e1e1e; border-radius: 1rem; padding: 1.25rem;
                width: 100%; max-width: 320px; box-shadow: 0 8px 32px rgba(0,0,0,.5); }
  .modal-box h3 { color: #fff; font-size: 1.05rem; margin-bottom: .9rem; text-align: center; }
  #weightInput { width: 100%; padding: .8rem; font-size: 1.4rem; text-align: center;
                 background: #2a2a2a; color: #fff; border: 1px solid #444;
                 border-radius: .5rem; }
  #weightInput:focus { border-color: #2563eb; outline: none; }
  .modal-actions { display: grid; grid-template-columns: 1fr 1fr; gap: .5rem; margin-top: 1rem; }
  .modal-actions button { padding: .7rem; border: none; border-radius: .5rem;
                          font-size: .95rem; font-weight: 600; cursor: pointer; color: #fff; }
  .modal-actions .cancel { background: #444; }
  .modal-actions .save   { background: #0d9488; }
</style>
{% endblock %}

{% block content %}
<div class="card">
  <div class="card-header">
    <div class="lang-toggle" role="group" aria-label="Language">
      <button class="lang-btn" data-lang="es">ES</button>
      <button class="lang-btn" data-lang="en">EN</button>
    </div>
    <span id="netIndicator" data-i18n-attr="title:net.checking" title="Checking…">⏳</span>
  </div>

  <h1 data-i18n="app.title">📷 Lector de códigos</h1>
  <p class="subtitle" id="subtitle"
     data-i18n="{{ 'subtitle.pending' if pending else 'subtitle.empty' }}"
     data-pending="{{ pending }}">
    {% if pending %}{{ pending }} pendientes — procesa desde el historial del escritorio
    {% else %}Haz una foto para añadir un escaneo{% endif %}
  </p>

  <button class="btn-scan" id="scanBtn" data-i18n="btn.scan">Escanear</button>
  <input type="file" id="fileInput" accept="image/*" capture="environment">

  <div class="meta-row">
    <button class="btn-meta btn-weight" id="btnWeight" disabled>
      <span data-i18n="btn.weight">Peso (kg)</span>
    </button>
    <button class="btn-meta btn-broken" id="btnBroken" disabled>
      <span data-i18n="btn.broken">Roto</span>
    </button>
    <button class="btn-meta btn-waste"  id="btnWaste"  disabled>
      <span data-i18n="btn.waste">Descarte</span>
    </button>
  </div>

  <div class="spinner" id="spinner"></div>

  <div id="result">
    <strong id="statusText"></strong>
    <span class="pending-count" id="pendingCount"></span>
  </div>

  <div id="queueBar" class="queue-bar" hidden>
    <span class="queue-info"><span id="queueCount">0</span> <span data-i18n="queue.summary_label">guardados</span></span>
    <button id="queueUpload" class="btn-queue" data-i18n="queue.upload_all">Subir todo</button>
  </div>
  <div id="queueList" class="queue-list" hidden></div>
</div>

<!-- Weight modal -->
<div id="weightModal" class="modal-backdrop" role="dialog" aria-modal="true">
  <div class="modal-box">
    <h3 data-i18n="modal.weight.title">Introduce el peso (kg)</h3>
    <input type="number" id="weightInput" step="0.01" min="0"
           inputmode="decimal" autocomplete="off" placeholder="0.00">
    <div class="modal-actions">
      <button class="cancel" id="weightCancel" data-i18n="btn.cancel">Cancelar</button>
      <button class="save"   id="weightSave"   data-i18n="btn.save">Guardar</button>
    </div>
  </div>
</div>

<script>
  // ── Translation dictionary (Spanish default, English on toggle) ────────
  const T_MOBILE = {
    en: {
      'page.title':           'Scan',
      'app.title':            '📷 Barcode Scanner',
      'subtitle.pending':     '{n} pending — process from the desktop history page',
      'subtitle.empty':       'Take a photo to queue a scan',
      'btn.scan':             'Scan',
      'btn.weight':           'Weight (kg)',
      'btn.weight.with_value':'Weight: {value} kg',
      'btn.broken':           'Broken',
      'btn.broken.active':    '✓ Broken',
      'btn.waste':            'Waste',
      'btn.waste.active':     '✓ Waste',
      'result.queued':        '✓ Queued for processing',
      'result.saved_offline': '📵 Saved on phone — tap "Upload all" when online',
      'result.total_pending': '{n} total pending',
      'net.online':           'Online',
      'net.offline':          'Offline',
      'net.checking':         'Checking connection…',
      'queue.summary_label':  'saved locally',
      'queue.upload_all':     'Upload all',
      'queue.uploading':      'Uploading {i}/{n}…',
      'queue.batch_ok':       '✓ {n} uploaded',
      'queue.batch_mixed':    '{ok} uploaded · {fail} failed — tap a row to retry',
      'queue.item.queued':    '⏳ Queued',
      'queue.item.failed':    '⚠ Failed ({n} attempts)',
      'queue.item.failed_1':  '⚠ Failed (1 attempt)',
      'queue.upload_one':     '✓ Uploaded',
      'queue.confirm_delete': 'Delete this photo from the local queue? It cannot be recovered.',
      'modal.weight.title':   'Enter weight (kg)',
      'btn.save':             'Save',
      'btn.cancel':           'Cancel',
      'toast.weight_saved':   '✓ Weight saved',
      'toast.broken_on':      '✓ Marked as Broken',
      'toast.broken_off':     '✗ Broken cleared',
      'toast.waste_on':       '✓ Marked as Waste',
      'toast.waste_off':      '✗ Waste cleared',
      'toast.save_failed':    '⚠ Could not save — try again',
      'toast.scan_first':     'Take a photo first',
    },
    es: {
      'page.title':           'Escanear',
      'app.title':            '📷 Lector de códigos',
      'subtitle.pending':     '{n} pendientes — procesa desde el historial del escritorio',
      'subtitle.empty':       'Haz una foto para añadir un escaneo',
      'btn.scan':             'Escanear',
      'btn.weight':           'Peso (kg)',
      'btn.weight.with_value':'Peso: {value} kg',
      'btn.broken':           'Roto',
      'btn.broken.active':    '✓ Roto',
      'btn.waste':            'Descarte',
      'btn.waste.active':     '✓ Descarte',
      'result.queued':        '✓ En cola para procesar',
      'result.saved_offline': '📵 Guardado en el móvil — toca «Subir todo» al volver',
      'result.total_pending': '{n} pendientes en total',
      'net.online':           'En línea',
      'net.offline':          'Sin conexión',
      'net.checking':         'Comprobando conexión…',
      'queue.summary_label':  'guardados localmente',
      'queue.upload_all':     'Subir todo',
      'queue.uploading':      'Subiendo {i}/{n}…',
      'queue.batch_ok':       '✓ {n} subidos',
      'queue.batch_mixed':    '{ok} subidos · {fail} con error — toca una fila para reintentar',
      'queue.item.queued':    '⏳ En cola',
      'queue.item.failed':    '⚠ Error ({n} intentos)',
      'queue.item.failed_1':  '⚠ Error (1 intento)',
      'queue.upload_one':     '✓ Subido',
      'queue.confirm_delete': '¿Eliminar esta foto de la cola local? No se puede recuperar.',
      'modal.weight.title':   'Introduce el peso (kg)',
      'btn.save':             'Guardar',
      'btn.cancel':           'Cancelar',
      'toast.weight_saved':   '✓ Peso guardado',
      'toast.broken_on':      '✓ Marcado como Roto',
      'toast.broken_off':     '✗ Roto eliminado',
      'toast.waste_on':       '✓ Marcado como Descarte',
      'toast.waste_off':      '✗ Descarte eliminado',
      'toast.save_failed':    '⚠ No se pudo guardar — reintenta',
      'toast.scan_first':     'Haz una foto primero',
    }
  };

  const $ = (id) => document.getElementById(id);

  // ── IndexedDB queue (unchanged structure; +metadata fields on each record) ──
  const DB_NAME = 'eco-codes-scanner', DB_VERSION = 1, STORE = 'pending';
  let _dbPromise = null;

  function openDB() {
    if (_dbPromise) return _dbPromise;
    _dbPromise = new Promise((resolve, reject) => {
      const req = indexedDB.open(DB_NAME, DB_VERSION);
      req.onupgradeneeded = () => {
        const db = req.result;
        if (!db.objectStoreNames.contains(STORE))
          db.createObjectStore(STORE, { keyPath: 'id', autoIncrement: true });
      };
      req.onsuccess = () => resolve(req.result);
      req.onerror   = () => reject(req.error);
    });
    return _dbPromise;
  }

  function txn(mode, op) {
    return new Promise(async (resolve, reject) => {
      try {
        const db = await openDB();
        const t  = db.transaction(STORE, mode);
        let result;
        const req = op(t.objectStore(STORE));
        if (req) req.onsuccess = () => { result = req.result; };
        t.oncomplete = () => resolve(result);
        t.onerror    = () => reject(t.error);
        t.onabort    = () => reject(t.error);
      } catch (e) { reject(e); }
    });
  }
  const qAll = ()    => txn('readonly',  s => s.getAll());
  const qGet = (id)  => txn('readonly',  s => s.get(id));
  const qAdd = (rec) => txn('readwrite', s => s.add(rec));
  const qPut = (rec) => txn('readwrite', s => s.put(rec));
  const qDel = (id)  => txn('readwrite', s => s.delete(id));

  if (navigator.storage?.persist) navigator.storage.persist().catch(() => {});

  // ── State ───────────────────────────────────────────────────────────────
  // currentTarget = the scan that metadata buttons apply to. Null until first
  // upload completes (or queues offline). Cleared while a new upload is in flight.
  //   { kind: 'server', scanId, weight_kg, broken, waste }
  //   { kind: 'queue',  idbId,  weight_kg, broken, waste }
  let currentTarget = null;

  // ── UI helpers ──────────────────────────────────────────────────────────
  function showResult(cls, message, count) {
    const r = $('result');
    r.className = cls;
    $('statusText').textContent   = message;
    $('pendingCount').textContent = (count != null)
        ? t('result.total_pending', { n: count }) : '';
    r.style.display = 'block';
  }

  function updateSubtitle(pendingCount) {
    const sub = $('subtitle');
    if (pendingCount > 0) {
      sub.dataset.i18n    = 'subtitle.pending';
      sub.dataset.pending = pendingCount;
      sub.textContent     = t('subtitle.pending', { n: pendingCount });
    } else {
      sub.dataset.i18n    = 'subtitle.empty';
      sub.dataset.pending = 0;
      sub.textContent     = t('subtitle.empty');
    }
  }

  function updateMetadataButtonState() {
    const has = currentTarget !== null;
    for (const id of ['btnWeight', 'btnBroken', 'btnWaste']) $(id).disabled = !has;

    const wSpan = $('btnWeight').querySelector('span');
    if (!has || currentTarget.weight_kg == null) {
      wSpan.textContent = t('btn.weight');
    } else {
      wSpan.textContent = t('btn.weight.with_value', { value: currentTarget.weight_kg });
    }

    const bSpan = $('btnBroken').querySelector('span');
    const wsSpan = $('btnWaste').querySelector('span');
    $('btnBroken').classList.toggle('active', !!(has && currentTarget.broken));
    $('btnWaste') .classList.toggle('active', !!(has && currentTarget.waste));
    bSpan.textContent  = has && currentTarget.broken ? t('btn.broken.active') : t('btn.broken');
    wsSpan.textContent = has && currentTarget.waste  ? t('btn.waste.active')  : t('btn.waste');
  }

  // ── Connectivity: poll /ping every 5s (more reliable than navigator.onLine) ──
  const PING_INTERVAL_MS = 5000;
  async function pingServer() {
    try {
      const ctrl = new AbortController();
      const tm = setTimeout(() => ctrl.abort(), 3000);
      const resp = await fetch('/ping', { cache: 'no-store', signal: ctrl.signal });
      clearTimeout(tm);
      setOnline(resp.ok);
    } catch { setOnline(false); }
  }
  function setOnline(up) {
    const el = $('netIndicator');
    el.textContent = up ? '🟢' : '📵';
    el.setAttribute('title', up ? t('net.online') : t('net.offline'));
    el.dataset.i18nAttr = up ? 'title:net.online' : 'title:net.offline';
  }

  // ── Upload pipeline ─────────────────────────────────────────────────────
  async function uploadBlob(blob, filename, metadata) {
    const fd = new FormData();
    fd.append('image', blob, filename);
    if (metadata) {
      if (metadata.weight_kg != null && metadata.weight_kg !== '')
        fd.append('weight_kg', String(metadata.weight_kg));
      if (metadata.broken) fd.append('broken', '1');
      if (metadata.waste)  fd.append('waste',  '1');
    }
    const res = await fetch('/scan', { method: 'POST', body: fd });
    if (!res.ok) throw new Error('HTTP ' + res.status);
    return res.json();
  }

  function makeFilename(file) {
    const raw  = (file.name || '').toLowerCase();
    const ext  = raw.includes('.') ? raw.split('.').pop() : 'jpg';
    const safe = ['jpg', 'jpeg', 'png', 'webp'].includes(ext) ? ext : 'jpg';
    return `phone-${Date.now()}.${safe}`;
  }

  async function handleNewPhoto(file) {
    $('result').style.display  = 'none';
    $('spinner').style.display = 'block';
    currentTarget = null;
    updateMetadataButtonState();

    const filename = makeFilename(file);
    try {
      const data = await uploadBlob(file, filename);
      $('spinner').style.display = 'none';
      currentTarget = {
        kind: 'server', scanId: data.id,
        weight_kg: data.weight_kg, broken: !!data.broken, waste: !!data.waste,
      };
      updateMetadataButtonState();
      updateSubtitle(data.pending);
      showResult('PENDING', t('result.queued'), data.pending);
    } catch {
      const idbId = await qAdd({
        blob: file, filename,
        mimeType: file.type || 'image/jpeg',
        queuedAt: Date.now(), attempts: 0, lastError: null,
        weight_kg: null, broken: false, waste: false,
      });
      $('spinner').style.display = 'none';
      currentTarget = { kind: 'queue', idbId,
                        weight_kg: null, broken: false, waste: false };
      updateMetadataButtonState();
      showResult('QUEUED', t('result.saved_offline'), null);
      await refreshQueueUI();
    }
  }

  // ── Metadata writes (with optimistic UI + revert on failure) ────────────
  async function applyMetadataChange(changes) {
    if (!currentTarget) { showResult('ERROR', t('toast.scan_first'), null); return; }
    const target = currentTarget;
    const prev   = { weight_kg: target.weight_kg, broken: target.broken, waste: target.waste };
    Object.assign(target, changes);
    if (target === currentTarget) updateMetadataButtonState();

    try {
      if (target.kind === 'server') {
        const r = await fetch(`/scan/${target.scanId}/metadata`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(changes),
        });
        if (!r.ok) throw new Error('HTTP ' + r.status);
      } else {
        const item = await qGet(target.idbId);
        if (item) { Object.assign(item, changes); await qPut(item); }
        if (target === currentTarget) await refreshQueueUI();
      }
      // Confirmation toast
      let msg;
      if ('weight_kg' in changes) msg = t('toast.weight_saved');
      else if ('broken' in changes) msg = changes.broken ? t('toast.broken_on') : t('toast.broken_off');
      else if ('waste'  in changes) msg = changes.waste  ? t('toast.waste_on')  : t('toast.waste_off');
      showResult('OK', msg, null);
    } catch {
      Object.assign(target, prev);
      if (target === currentTarget) updateMetadataButtonState();
      showResult('ERROR', t('toast.save_failed'), null);
    }
  }

  // ── Weight modal ────────────────────────────────────────────────────────
  function openWeightModal() {
    if (!currentTarget) { showResult('ERROR', t('toast.scan_first'), null); return; }
    const inp = $('weightInput');
    inp.value = currentTarget.weight_kg != null ? String(currentTarget.weight_kg) : '';
    $('weightModal').classList.add('open');
    setTimeout(() => { inp.focus(); inp.select(); }, 0);
  }
  function closeWeightModal() { $('weightModal').classList.remove('open'); }
  async function saveWeight() {
    const raw = $('weightInput').value.trim();
    if (raw === '') { await applyMetadataChange({ weight_kg: null }); closeWeightModal(); return; }
    const val = parseFloat(raw);
    if (isNaN(val) || val < 0) { $('weightInput').focus(); return; }
    closeWeightModal();
    await applyMetadataChange({ weight_kg: val });
  }

  // ── Queue UI (with metadata tags on each row) ──────────────────────────
  let _blobUrls = [];
  function revokeBlobUrls() { for (const u of _blobUrls) URL.revokeObjectURL(u); _blobUrls = []; }

  async function refreshQueueUI() {
    let items = [];
    try { items = await qAll(); } catch (e) { console.warn('Queue read failed:', e); return; }
    const bar  = $('queueBar'), list = $('queueList');
    revokeBlobUrls();
    if (items.length === 0) {
      bar.hidden = true; list.hidden = true; list.innerHTML = ''; return;
    }
    bar.hidden = false; $('queueCount').textContent = items.length;
    list.hidden = false; list.innerHTML = '';
    for (const it of items) {
      const url = URL.createObjectURL(it.blob);
      _blobUrls.push(url);
      const time = new Date(it.queuedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
      const status = it.lastError
        ? t(it.attempts === 1 ? 'queue.item.failed_1' : 'queue.item.failed', { n: it.attempts })
        : t('queue.item.queued');
      // Tag line: shows accumulated metadata so user sees what's stored.
      const tags = [];
      if (it.weight_kg != null) tags.push(`${it.weight_kg} kg`);
      if (it.broken) tags.push('🔴 ' + t('btn.broken'));
      if (it.waste)  tags.push('🗑 ' + t('btn.waste'));
      const row = document.createElement('div');
      row.className = 'queue-item' + (it.lastError ? ' failed' : '');
      row.innerHTML = `
        <img src="${url}" alt="">
        <div class="qi-meta">
          <div class="qi-time">${time}</div>
          <div class="qi-status">${status}</div>
          ${tags.length ? `<div class="qi-tags">${tags.join(' · ')}</div>` : ''}
        </div>
        <button class="qi-retry" data-id="${it.id}">▲</button>
        <button class="qi-del"   data-id="${it.id}">🗑️</button>
      `;
      list.appendChild(row);
    }
  }

  async function uploadQueueItem(id) {
    const item = await qGet(id);
    if (!item) return { ok: false };
    try {
      await uploadBlob(item.blob, item.filename, {
        weight_kg: item.weight_kg, broken: item.broken, waste: item.waste,
      });
      await qDel(id);
      // If we just uploaded the currentTarget's queue item, drop the target —
      // metadata is now server-side, but we don't have the scan_id back here
      // without complicating the API. User can just take another scan.
      if (currentTarget?.kind === 'queue' && currentTarget.idbId === id) {
        currentTarget = null; updateMetadataButtonState();
      }
      return { ok: true };
    } catch (err) {
      item.attempts  = (item.attempts || 0) + 1;
      item.lastError = String(err.message || err);
      await qPut(item);
      return { ok: false, error: err };
    }
  }

  async function uploadAllQueued() {
    const btn = $('queueUpload');
    btn.disabled = true;
    const items = await qAll();
    let ok = 0, fail = 0;
    for (let i = 0; i < items.length; i++) {
      btn.textContent = t('queue.uploading', { i: i + 1, n: items.length });
      const r = await uploadQueueItem(items[i].id);
      r.ok ? ok++ : fail++;
    }
    btn.disabled    = false;
    btn.textContent = t('queue.upload_all');
    await refreshQueueUI();
    if (fail === 0) showResult('PENDING', t('queue.batch_ok', { n: ok }), null);
    else            showResult('ERROR',   t('queue.batch_mixed', { ok, fail }), null);
  }

  // ── Event wiring ────────────────────────────────────────────────────────
  $('scanBtn')    .addEventListener('click', () => $('fileInput').click());
  $('fileInput')  .addEventListener('change', async (e) => {
    const file = e.target.files[0];
    if (file) await handleNewPhoto(file);
    e.target.value = '';
  });

  $('btnWeight').addEventListener('click', openWeightModal);
  $('btnBroken').addEventListener('click', () => applyMetadataChange({ broken: !currentTarget?.broken }));
  $('btnWaste') .addEventListener('click', () => applyMetadataChange({ waste:  !currentTarget?.waste  }));

  $('weightCancel').addEventListener('click', closeWeightModal);
  $('weightSave')  .addEventListener('click', saveWeight);
  $('weightInput') .addEventListener('keydown', (e) => {
    if (e.key === 'Enter')  { e.preventDefault(); saveWeight(); }
    if (e.key === 'Escape') { closeWeightModal(); }
  });
  $('weightModal') .addEventListener('click', (e) => {
    if (e.target.id === 'weightModal') closeWeightModal();
  });

  $('queueUpload').addEventListener('click', uploadAllQueued);

  document.body.addEventListener('click', async (e) => {
    const retry = e.target.closest('.qi-retry');
    const del   = e.target.closest('.qi-del');
    if (retry) {
      const id = parseInt(retry.dataset.id, 10);
      retry.disabled = true; retry.textContent = '…';
      const r = await uploadQueueItem(id);
      if (r.ok) showResult('PENDING', t('queue.upload_one'), null);
      await refreshQueueUI();
    } else if (del) {
      const id = parseInt(del.dataset.id, 10);
      if (!confirm(t('queue.confirm_delete'))) return;
      if (currentTarget?.kind === 'queue' && currentTarget.idbId === id) {
        currentTarget = null; updateMetadataButtonState();
      }
      await qDel(id);
      await refreshQueueUI();
    }
  });

  // ── Init ────────────────────────────────────────────────────────────────
  // Listener wired before initI18n so the initial langchange dispatch also
  // rebuilds the interpolated labels (subtitle's {n}, metadata buttons,
  // queue-item tags) — these can't be expressed as plain data-i18n keys.
  document.addEventListener('DOMContentLoaded', () => {
    window.addEventListener('langchange', () => {
      updateSubtitle(parseInt($('subtitle').dataset.pending || '0', 10));
      updateMetadataButtonState();
      refreshQueueUI();
    });
    initI18n(T_MOBILE);
    pingServer();
    setInterval(pingServer, PING_INTERVAL_MS);
    window.addEventListener('online',  pingServer);
    window.addEventListener('offline', () => setOnline(false));
  });
</script>
{% endblock %}