// Estación Creativa — kiosk runtime
// Boots straight into the 7-screen flow on the touchscreen.
// No design canvas, no tweaks panel — those were prototyping tools.

const { useState, useEffect } = React;

function readConfig() {
  const qs = new URLSearchParams(window.location.search);
  const variant = (qs.get('v') || qs.get('variant') || 'a').toLowerCase() === 'b' ? 'b' : 'a';
  const brandName = qs.get('brand') || (variant === 'b' ? 'DIBUJÍN' : 'CREATIVA');
  const tagline   = qs.get('tagline') || (variant === 'b' ? '¡imprimí, pintá, jugá!' : 'imaginá, creá, dibujá…');
  const narration = (qs.get('audio') || 'on') !== 'off';
  const demo = qs.has('demo') && qs.get('demo') !== '0' && qs.get('demo') !== 'off';
  // Screensaver por inactividad: ?saver=90 (segundos), ?saver=off para desactivarlo. Default 60s.
  const saverParam = qs.get('saver');
  const saver = (saverParam === 'off' || saverParam === '0') ? 0 : Math.max(5, parseInt(saverParam, 10) || 60);
  return { variant, brandName, tagline, narration, demo, saver };
}

// Agente local de impresión (misma PC que el kiosko). Si responde /health, el
// PDF se imprime sin salir a internet. http://127.0.0.1 es "secure context" aun
// desde una página HTTPS, así que el navegador no lo bloquea por mixed-content.
const LOCAL_AGENT = 'http://127.0.0.1:9123';

function fetchTimeout(url, opts = {}, ms = 1500) {
  const ctrl = new AbortController();
  const t = setTimeout(() => ctrl.abort(), ms);
  return fetch(url, { ...opts, signal: ctrl.signal }).finally(() => clearTimeout(t));
}

async function localAgentUp() {
  try { const r = await fetchTimeout(`${LOCAL_AGENT}/health`, {}, 700); return r.ok; }
  catch { return false; }
}

// Duración de la pantalla "IMPRIMIENDO…" antes de pasar a "retirá tu dibujo".
// Default 8s (la inkjet tarda en expulsar la hoja física tras el spool). Ajustable
// en el lugar con ?printms=12000 sin tocar código. Si el backend tarda MÁS, la
// pantalla espera igual (la barra se frena al 92% hasta la confirmación).
const PRINT_MS = (() => {
  const v = parseInt(new URLSearchParams(window.location.search).get('printms'), 10);
  return Number.isFinite(v) && v >= 1000 ? v : 8000;
})();

function blobToBase64(blob) {
  return new Promise((resolve, reject) => {
    const fr = new FileReader();
    fr.onerror = () => reject(fr.error);
    fr.onload = () => resolve(String(fr.result).split(',')[1] || '');
    fr.readAsDataURL(blob);
  });
}

function KioskFlow({ variant, brandName, tagline, narration, demo }) {
  const [screen, setScreen]     = useState('idle');
  const [category, setCategory] = useState(null);
  const [theme, setTheme]       = useState(null);
  const [themeName, setThemeName] = useState(null);
  const [drawing, setDrawing]   = useState(null);
  const [coupon, setCoupon]     = useState(null);
  const [scanError, setScanError] = useState(null);
  const [sessionId, setSessionId] = useState(null);
  const [printResult, setPrintResult] = useState(null);   // null = en curso; {ok:true} | {ok:false, error}

  useEffect(() => { speak(screen, narration); }, [screen, narration]);

  // Trazabilidad: cada vez que se usa un QR se abre una sesión y se registra el recorrido
  // completo (cada pantalla que se visita, incluidos los "atrás"). Ver /api/navigation.
  const startSession = code => {
    const sid = newSessionId();
    setSessionId(sid);
    trackNav({ sid, screen: 'scan', coupon: code });
    return sid;
  };
  useEffect(() => {
    if (!sessionId || screen === 'idle' || screen === 'scan') return;
    trackNav({
      sid: sessionId, screen, coupon,
      category, categoryLabel: category ? catLabelById(category) : null,
      theme: themeName,
      drawingId: drawing?.id || null,
      drawingName: drawing?.name || null,
    });
  }, [screen]);

  // Physical QR scanner (Unnion BR70, HID keyboard). Only acts on the scan screen.
  const reasonText = r => ({ vencido: 'Cupón vencido', agotado: 'Cupón sin usos disponibles' }[r] || 'Cupón no válido');
  const ok = code => { sfx.scan(); setScanError(null); startSession(code); setCoupon(code); setScreen('category'); };
  const bad = msg => { blip(220, 0.12); setScanError(msg); };
  useScanner(code => {
    if (screen !== 'scan') return;
    // Test modes via URL
    if (COUPON.override === 'any') return ok(code || 'any');
    if (COUPON.override) return code === COUPON.override ? ok(code) : bad('Cupón no válido');
    // Validate against the backoffice (limits / expiry)
    fetch('/api/validate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ code }) })
      .then(r => r.json())
      .then(v => v.valid ? ok(code) : bad(reasonText(v.reason)))
      .catch(() => isValidCoupon(code) ? ok(code) : bad('Cupón no válido'));
  }, { enabled: screen === 'scan' });

  // Clear any previous error when (re)entering the scan screen.
  useEffect(() => { if (screen === 'scan') setScanError(null); }, [screen]);

  const reset = () => { setScreen('idle'); setCategory(null); setTheme(null); setThemeName(null); setDrawing(null); setCoupon(null); setSessionId(null); setPrintResult(null); };

  // Dispara la impresión y entra a la pantalla "printing", que esperará el resultado.
  const startPrint = d => { setPrintResult(null); setScreen('printing'); sendToPrinter(d).then(setPrintResult); };

  // Devuelve { ok, error?, reqId? } para que la UI pueda avisar al usuario si falló.
  // Dos caminos: LOCAL (agente en esta PC, el PDF no sale a internet) y NUBE
  // (fallback: el server arma y empuja el PDF al agente por relay/S3).
  const sendToPrinter = async d => {
    const variantName = d?.name || d?.id;
    const fullName = (themeName && themeName !== variantName) ? `${themeName} · ${variantName}` : (themeName || variantName);
    const payload = { drawingId: d?.id, drawingName: fullName, drawingKind: d?.draw, cat: d?.cat, base: d?.base, fullPage: d?.fullPage, coupon, demo };

    if (await localAgentUp()) return sendToLocalAgent(d, fullName, payload);
    return sendViaCloud(payload);
  };

  // Camino LOCAL. IMPORTANTE: una vez reservado el cupo NO caemos a nube (sería
  // doble impresión / doble conteo). Si falla luego de reservar, liberamos el cupo.
  const sendToLocalAgent = async (d, fullName, payload) => {
    let reqId = null;
    try {
      const rr = await fetch('/api/print/reserve', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
      const rj = await rr.json().catch(() => ({}));
      if (!rr.ok || !rj.ok) {
        console.warn('[print] reserva rechazada:', rj.error || rr.status);
        return { ok: false, error: rj.error || `HTTP ${rr.status}`, reason: rj.reason, reqId: rj.reqId || null };
      }
      reqId = rj.reqId;
      if (rj.dryRun) {                                   // demo: no imprime de verdad
        await confirmPrint(reqId, true);
        console.log(`[print] LOCAL dry-run OK (req ${reqId})`);
        return { ok: true, reqId };
      }
      // Bajar el PDF (el service worker lo sirve de cache la 2ª vez) y entregarlo
      // al agente local en base64.
      const q = new URLSearchParams({ cat: d?.cat || '', base: d?.base || '', drawingName: fullName, drawingKind: d?.draw || '', fullPage: d?.fullPage ? '1' : '' });
      const pdfResp = await fetch(`/api/pdf/${encodeURIComponent(d?.id)}?${q.toString()}`);
      if (!pdfResp.ok) throw new Error(`pdf HTTP ${pdfResp.status}`);
      const b64 = await blobToBase64(await pdfResp.blob());
      const pr = await fetchTimeout(`${LOCAL_AGENT}/print`, {
        method: 'POST', headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ jobId: reqId, document: { type: 'pdf-base64', data: b64 } }),
      }, 30000);
      const pj = await pr.json().catch(() => ({}));
      if (!pr.ok || !pj.ok) throw new Error(pj.error || `agente HTTP ${pr.status}`);
      await confirmPrint(reqId, true);
      console.log(`[print] LOCAL OK (req ${reqId}, impresora: ${pj.printer})`);
      return { ok: true, reqId, printer: pj.printer };
    } catch (e) {
      console.warn('[print] camino local falló:', e);
      if (reqId) await confirmPrint(reqId, false, String(e?.message || e));   // liberar cupo
      return { ok: false, error: 'no se pudo imprimir localmente', reqId };
    }
  };

  const confirmPrint = (reqId, ok, error) =>
    fetch('/api/print/confirm', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ reqId, ok, error }) }).catch(() => {});

  // Camino NUBE (fallback).
  const sendViaCloud = async payload => {
    try {
      const r = await fetch('/api/print', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
      const j = await r.json().catch(() => ({}));
      if (!r.ok || !j.ok) {
        console.warn(`[print] el servidor rechazó la impresión (HTTP ${r.status}, req ${j.reqId || '?'}):`, j.error || r.statusText, '— buscá', j.reqId || 'el reqId', 'en data/print-log.jsonl');
        return { ok: false, error: j.error || `HTTP ${r.status}`, reqId: j.reqId || null };
      }
      console.log(`[print] enviado OK vía nube (req ${j.reqId}, impresora: ${j.printer}${j.dryRun ? ', DRY-RUN' : ''})`);
      return { ok: true, reqId: j.reqId || null };
    } catch (e) {
      console.warn('[print] error de red al contactar /api/print (¿servidor caído?):', e);
      return { ok: false, error: 'no se pudo contactar al servidor', reqId: null };
    }
  };

  const props = {
    idle:     { onStart: () => setScreen('scan'), brandName, tagline },
    scan:     { onScanned: () => { const c = demo ? '(demo)' : '(manual)'; startSession(c); setCoupon(c); setScreen('category'); }, onBack: () => setScreen('idle'), scanError, demo },
    category: { onPick: id => { setCategory(id); setScreen('catalog'); }, onBack: () => setScreen('scan') },
    catalog:  { category, onPick: th => {
                  const vs = th.variants || [th];
                  setThemeName(th.name);
                  if (vs.length > 1) { setTheme(th); setScreen('variants'); }
                  else { setTheme(null); setDrawing(vs[0]); setScreen('confirm'); }
                }, onBack: () => setScreen('category') },
    variants: { theme, onPick: d => { setDrawing(d); setScreen('confirm'); }, onBack: () => setScreen('catalog') },
    confirm:  { drawing, onPrint: () => startPrint(drawing), onBack: () => setScreen(theme ? 'variants' : 'catalog') },
    printing: { drawing, result: printResult, printMs: PRINT_MS, onDone: () => {
                  if (printResult && !printResult.ok) { blip(220, 0.18); setScreen('error'); }
                  else { sfx.done(); setScreen('pickup'); }
                } },
    error:    { drawing, error: printResult?.error, reqId: printResult?.reqId,
                onRetry: () => startPrint(drawing), onRestart: reset },
    pickup:   { drawing, onRestart: reset },
  };

  const Screens = variant === 'a'
    ? { idle: AIdle, scan: AScan, category: ACategory, catalog: ACatalog, variants: ACatalog, confirm: AConfirm, printing: APrinting, error: AError, pickup: APickup }
    : { idle: BIdle, scan: BScan, category: BCategory, catalog: BCatalog, variants: BCatalog, confirm: BConfirm, printing: BPrinting, error: BError, pickup: BPickup };

  const Current = Screens[screen];
  return <Current {...props[screen]} />;
}

// Scales the 720×1080 design canvas to fit any portrait/landscape touchscreen
// while preserving aspect ratio (letterboxed against the variant's background).
function FitToViewport({ children, bg }) {
  const DESIGN_W = 720, DESIGN_H = 1080;
  const [box, setBox] = useState({ scale: 1, top: 0, left: 0 });

  useEffect(() => {
    const fit = () => {
      const vw = window.innerWidth, vh = window.innerHeight;
      const scale = Math.min(vw / DESIGN_W, vh / DESIGN_H);
      const w = DESIGN_W * scale, h = DESIGN_H * scale;
      setBox({ scale, top: (vh - h) / 2, left: (vw - w) / 2 });
    };
    fit();
    window.addEventListener('resize', fit);
    window.addEventListener('orientationchange', fit);
    return () => {
      window.removeEventListener('resize', fit);
      window.removeEventListener('orientationchange', fit);
    };
  }, []);

  return (
    <div style={{ position: 'fixed', inset: 0, background: bg, overflow: 'hidden' }}>
      <div style={{
        position: 'absolute',
        top: box.top, left: box.left,
        width: DESIGN_W, height: DESIGN_H,
        transform: `scale(${box.scale})`,
        transformOrigin: 'top left',
      }}>
        {children}
      </div>
    </div>
  );
}

// Pantalla de descanso: video en loop a pantalla completa. Cualquier toque la cierra.
// Va fuera de FitToViewport (position:fixed dentro de un transform no cubre el viewport).
function Screensaver({ onWake }) {
  return (
    <div onPointerDown={onWake} style={{ position: 'fixed', inset: 0, zIndex: 1000, background: '#000' }}>
      <video src="assets/screensaver.mp4" autoPlay loop muted playsInline
             style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
      <div style={{ position: 'absolute', bottom: '8%', left: 0, right: 0, textAlign: 'center',
                    color: '#fff', fontSize: 32, fontWeight: 600,
                    textShadow: '0 2px 12px rgba(0,0,0,0.7)',
                    animation: 'saver-pulse 2.6s ease-in-out infinite' }}>
        Tocá la pantalla para crear
      </div>
    </div>
  );
}

function App() {
  const cfg = readConfig();
  const bg = cfg.variant === 'a' ? '#fafaf6' : '#FFF8E7';

  // El catálogo se carga desde /api/catalog antes de renderizar el flujo, para no mostrar
  // una grilla de categorías vacía mientras llega. Si falla, loadCatalog deja el fallback.
  const [ready, setReady] = useState(false);
  useEffect(() => { loadCatalog().finally(() => setReady(true)); }, []);

  // Kiosk hardening: disable context menu + text selection + pinch zoom
  useEffect(() => {
    const noMenu = e => e.preventDefault();
    document.addEventListener('contextmenu', noMenu);
    document.body.style.userSelect = 'none';
    document.body.style.webkitUserSelect = 'none';
    document.body.style.touchAction = 'manipulation';
    return () => document.removeEventListener('contextmenu', noMenu);
  }, []);

  // Screensaver: tras cfg.saver segundos sin toques ni teclado (el lector QR emite
  // teclas, así que escanear también cuenta como actividad) se muestra el video.
  // Al despertar, el flujo se remonta (flowKey) para arrancar limpio en "idle".
  const [saverOn, setSaverOn] = useState(false);
  const [flowKey, setFlowKey] = useState(0);
  useEffect(() => {
    if (!cfg.saver) return;
    let timer;
    const arm = () => { clearTimeout(timer); timer = setTimeout(() => setSaverOn(true), cfg.saver * 1000); };
    const events = ['pointerdown', 'touchstart', 'keydown'];
    events.forEach(ev => window.addEventListener(ev, arm, { passive: true }));
    arm();
    return () => { clearTimeout(timer); events.forEach(ev => window.removeEventListener(ev, arm)); };
  }, [cfg.saver]);
  const wake = () => { setSaverOn(false); setFlowKey(k => k + 1); };

  return (
    <React.Fragment>
      <FitToViewport bg={bg}>
        {ready ? <KioskFlow key={flowKey} {...cfg} /> : null}
      </FitToViewport>
      {saverOn ? <Screensaver onWake={wake} /> : null}
    </React.Fragment>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<App />);
