// Panel de resultados multi-súper del SmartInput. Referencia visual:
// docs/design/multi-super/Main.dc.html. Busca en todas las cadenas activas a
// la vez (`useStoreSearch`); cada cadena aparece al llegar.
//
//   StoreResultsList  — tarjeta: chips, orden, grupos/lista, estados (5.2a)
//   StoreResultsPanel — entrada/salida, velo y altura disponible (5.2b)
//   StoreResultsLive  — región viva con el resumen para lectores (5.2c)

// Subtítulo de una fila de resultado: «{cadena} · {precio general} · {marca ·
// envase}» (los tramos vacíos se descartan). Compartida por el panel de
// resultados y `StoreLinkSheet` (Task 6.1 fix round 1) — una sola fuente para
// no arriesgar que la etiqueta diverja entre las dos hojas. `chainPref` =
// `app.stores.chains[p.chain]`: hace falta para saber si la cadena es
// genérica por estar activa sin zona (`StoresCore.isGenericPrices`), no solo
// por naturaleza (Dia/Eroski).
function subOf(p, withChain, ts, chainPref) {
  const meta = StoresCore.chainMeta(p.chain) || { name: '' };
  const size = [p.brand, p.packaging].filter(Boolean).join(' · ');
  const generic = StoresCore.isGenericPrices(p.chain, chainPref);
  const chainPart = withChain ? [meta.name, generic ? ts.genericPrice : ''].filter(Boolean).join(' · ') : '';
  return [chainPart, size].filter(Boolean).join(' · ');
}

function StoreResultsList({ app, theme, query, inline, maxHeight, onPick }) {
  const c = theme.c;
  const ts = app.t.stores;
  const state = useStoreSearch(query, app.stores);
  const [filter, setFilter] = React.useState('all');
  const [sort, setSort] = React.useState(app.stores.sort);
  const [pickedKey, setPickedKey] = React.useState(null);
  const pickTimer = React.useRef(null);
  // «Por precio»: si el dedo tocó la lista hace <400 ms, las filas ya
  // pintadas no cambian de sitio al llegar otra cadena (se reordena después).
  const touchAt = React.useRef(0);
  const prevKeys = React.useRef([]);
  const holdMs = React.useRef(0);
  const [, setTick] = React.useState(0);
  React.useEffect(() => () => clearTimeout(pickTimer.current), []);
  React.useEffect(() => { setPickedKey(null); }, [state && state.reqId]);
  React.useEffect(() => {
    if (!holdMs.current) return undefined;
    const tm = setTimeout(() => setTick(x => x + 1), holdMs.current);
    return () => clearTimeout(tm);
  });

  if (!state) return null;

  const motion = !!app.animations && !SLMotion.reducedMotion();
  const card = (children) => (
    <div id="store-results" data-testid="store-results" role="region" aria-label={ts.resultsLabel} style={{
      background: c.bgCard, border: `1.5px solid ${c.border}`, borderRadius: theme.radius.lg,
      boxShadow: '0 10px 30px rgba(0,0,0,0.15)', overflow: 'hidden',
      marginBottom: inline ? 0 : 8, marginTop: inline ? 8 : 0,
    }}>{children}</div>
  );

  if (state.offline) {
    return card(<MessageRow theme={theme} testid="results-offline">{ts.offline}</MessageRow>);
  }

  const chains = Object.keys(state.chains);
  const counts = {};
  for (const id of chains) counts[id] = state.chains[id].products.length;
  const summary = StoresCore.searchSummary(state);
  const all = StoresCore.searchProducts(state, filter);
  const unit = StoresCore.dominantUnit(all);
  const best = StoresCore.bestKey(all);
  const bestLabel = tpl(ts.best, { unit: unit || 'kg' });
  const loadingOf = (id) => state.chains[id].status === 'loading';
  let rowIndex = 0; // escalonado continuo entre grupos

  function pick(p) {
    if (pickedKey) return;
    setPickedKey(StoresCore.productKey(p));
    SLMotion.haptic.light();
    // Deja ver el sello ✓ un instante antes de añadir (sin movimiento: ya).
    pickTimer.current = setTimeout(() => onPick(p), motion ? 220 : 0);
  }

  const row = (p, withChain) => {
    const key = StoresCore.productKey(p);
    const isBest = key === best;
    return (
      <StoreProductRow key={key} theme={theme} product={p} sub={subOf(p, withChain, ts, app.stores.chains[p.chain])} index={rowIndex++} animate={motion}
        best={isBest} bestLabel={bestLabel} right2={StoresCore.fmtBulk(p)}
        dim={loadingOf(p.chain)} picked={pickedKey === key} onPick={pick}
        ariaLabel={StoresCore.productLabel(p, isBest ? bestLabel : '')} />
    );
  };

  const skeletons = (id) => [0, 1].map(n => <SkeletonRow key={`sk-${id}-${n}`} theme={theme} />);

  const header = (id) => {
    const meta = StoresCore.CHAIN_META[id];
    const entry = state.chains[id];
    const group = StoresCore.groupByChain(entry.products)[0];
    let right = null;
    if (entry.status === 'loading') right = <StoreSpinner theme={theme} label={ts.loading} motion={motion} />;
    else if (entry.status !== 'done') right = <span>{ts.unavailableShort}</span>;
    else if (group && group.from != null) right = <span>{tpl(ts.from, { price: StoresCore.fmtBulk({ bulk_price: group.from, bulk_unit: group.fromUnit }) })}</span>;
    return (
      <div key={`h-${id}`} data-testid={`results-group-${id}`} style={{
        position: 'sticky', top: 0, zIndex: 1, display: 'flex', alignItems: 'center', gap: 8,
        padding: '10px 14px 6px', background: c.bgCard + 'eb',
        backdropFilter: 'blur(12px)', WebkitBackdropFilter: 'blur(12px)',
        fontSize: 11, color: c.inkSub, fontFamily: theme.fontMono,
      }}>
        <ChainBadge theme={theme} chain={id} size={20} />
        <span style={{ fontWeight: 700, textTransform: 'uppercase', letterSpacing: 1.5 }}>{meta.name}</span>
        {StoresCore.isGenericPrices(id, app.stores.chains[id]) && <span>· {ts.genericPrice}</span>}
        <span style={{ flex: 1, height: 1, background: c.border }} />
        {right}
      </div>
    );
  };

  let body;
  holdMs.current = 0;
  if (summary.settled && all.length === 0) {
    body = summary.allFailed
      ? <MessageRow theme={theme} testid="results-all-failed">{ts.allFailed}</MessageRow>
      : <MessageRow theme={theme} testid="results-empty">{tpl(ts.noResultsFor, { q: state.query })}</MessageRow>;
  } else if (sort === 'store') {
    body = chains.filter(id => filter === 'all' || filter === id).map(id => {
      const entry = state.chains[id];
      if (entry.status === 'done' && entry.products.length === 0) return null;
      const items = StoresCore.sortByBulk(entry.products);
      return (
        <React.Fragment key={id}>
          {header(id)}
          {items.length === 0 && entry.status === 'loading' ? skeletons(id) : items.map(p => row(p, false))}
        </React.Fragment>
      );
    });
  } else {
    const parts = StoresCore.partitionByUnit(all);
    let ordered = parts.main.concat(parts.other);
    const since = Date.now() - touchAt.current;
    if (since < 400 && prevKeys.current.length) {
      const byKey = {};
      for (const p of ordered) byKey[StoresCore.productKey(p)] = p;
      ordered = StoresCore.mergeOrder(prevKeys.current, ordered.map(StoresCore.productKey)).map(k => byKey[k]);
      holdMs.current = 400 - since;
    }
    prevKeys.current = ordered.map(StoresCore.productKey);
    const firstOther = parts.main.length ? ordered.findIndex(p => !parts.main.includes(p)) : -1;
    const loadingEmpty = chains.some(id => loadingOf(id) && state.chains[id].products.length === 0 && (filter === 'all' || filter === id));
    body = (
      <>
        {ordered.map((p, i) => (
          <React.Fragment key={StoresCore.productKey(p)}>
            {i === firstOther && (
              <div data-testid="results-other-units" style={{
                padding: '10px 14px 4px', fontSize: 11, fontWeight: 700, color: c.inkSub,
                textTransform: 'uppercase', letterSpacing: 1.5, fontFamily: theme.fontMono,
              }}>{ts.otherUnits}</div>
            )}
            {row(p, true)}
          </React.Fragment>
        ))}
        {loadingEmpty && skeletons('price')}
      </>
    );
  }

  const genericShown = chains.filter(id => StoresCore.isGenericPrices(id, app.stores.chains[id]) && state.chains[id].products.length > 0 && (filter === 'all' || filter === id));

  return card(
    <>
      <StoreResultsLive app={app} state={state} />
      <div style={{ padding: '12px 12px 10px', display: 'flex', flexDirection: 'column', gap: 10, borderBottom: `1px solid ${c.border}` }}>
        {chains.length > 1 && (
          <ChainChips theme={theme} chains={chains} counts={counts} value={filter} onChange={setFilter}
            allLabel={ts.all} label={ts.filterLabel} />
        )}
        <Segmented theme={theme} testid="results-sort" label={ts.sortLabel} value={sort} onChange={setSort}
          options={[{ value: 'store', label: ts.sortStore }, { value: 'price', label: ts.sortPrice }]} />
      </div>
      <div className="sl-scroll" data-testid="results-scroll" onPointerDownCapture={() => { touchAt.current = Date.now(); }}
        style={{ height: maxHeight, overflowY: 'auto' }}>
        {body}
        {genericShown.length > 0 && (
          <div data-testid="results-generic-note" style={{ padding: '10px 14px 14px', fontSize: 11, color: c.inkSub, fontFamily: theme.fontBody, lineHeight: 1.4 }}>
            {tpl(ts.genericNote, { chains: genericShown.map(StoresCore.chainName).join(', ') })}
          </div>
        )}
      </div>
    </>
  );
}

// Entrada .34s desde translateY(14px) scale(.98) (hacia arriba si el panel
// cuelga bajo el campo: `inline`); salida con TRANSICIÓN .2s (no keyframes)
// para que sea reversible si el campo recupera el foco. Se mantiene montado
// 200 ms al cerrar, sin eventos de puntero. Sin movimiento: aparece y
// desaparece al instante. Velo `c.ink` al 10 % solo en móvil (flotante).
function StoreResultsPanel({ app, theme, open, query, inline, kbOffset = 0, onPick }) {
  const c = theme.c;
  const motion = !!app.animations && !SLMotion.reducedMotion();
  const [mounted, setMounted] = React.useState(open);
  const [shown, setShown] = React.useState(false);
  const shownOnce = React.useRef(false);
  const lastQuery = React.useRef(query);
  if (open) lastQuery.current = query;

  React.useEffect(() => {
    if (open) { setMounted(true); return undefined; }
    setShown(false);
    if (!motion) { setMounted(false); shownOnce.current = false; return undefined; }
    const tm = setTimeout(() => { setMounted(false); shownOnce.current = false; }, 200);
    return () => clearTimeout(tm);
  }, [open, motion]);

  React.useEffect(() => {
    if (!open || !mounted) return undefined;
    // Reabrir a mitad de la salida: vuelve directamente (la transición CSS
    // invierte el movimiento desde donde esté).
    if (!motion || shownOnce.current) { shownOnce.current = true; setShown(true); return undefined; }
    let r2 = 0;
    const r1 = requestAnimationFrame(() => { r2 = requestAnimationFrame(() => { shownOnce.current = true; setShown(true); }); });
    return () => { cancelAnimationFrame(r1); cancelAnimationFrame(r2); };
  }, [open, mounted, motion]);

  if (!mounted) return null;

  const dir = inline ? -1 : 1;
  const enterT = 'opacity .34s cubic-bezier(.32,.72,0,1), transform .34s cubic-bezier(.32,.72,0,1)';
  const exitT = 'opacity .2s cubic-bezier(.4,0,1,1), transform .2s cubic-bezier(.4,0,1,1)';
  // Reabrir a mitad de la salida: `shownOnce` ya está a true, así que
  // `visible` pasa a true en el mismo render y la transición CSS invierte
  // el movimiento desde su pose interpolada actual (sin saltar a la pose
  // de partida de la entrada).
  const visible = shown || (open && shownOnce.current);
  const style = visible
    ? { opacity: 1, transform: 'none', transition: motion ? enterT : 'none' }
    : open
      ? { opacity: 0, transform: `translateY(${14 * dir}px) scale(.98)`, transition: 'none' }
      : { opacity: 0, transform: `translateY(${18 * dir}px) scale(.97)`, transition: motion ? exitT : 'none' };
  const viewport = typeof window !== 'undefined' ? window.innerHeight : 800;
  const maxHeight = Math.max(160, Math.min(356, viewport - kbOffset - 260));

  return (
    <>
      {!inline && (
        <div aria-hidden="true" data-testid="store-results-dim" style={{
          position: 'fixed', inset: 0, pointerEvents: 'none',
          background: c.ink + '1a', opacity: visible ? 1 : 0,
          transition: motion ? 'opacity .2s ease' : 'none',
        }} />
      )}
      <div data-testid="store-results-panel" style={{
        ...style, position: 'relative', transformOrigin: inline ? '50% 0' : '50% 100%',
        pointerEvents: open ? 'auto' : 'none',
      }}>
        <StoreResultsList app={app} theme={theme} query={lastQuery.current} inline={inline}
          maxHeight={maxHeight} onPick={onPick} />
      </div>
    </>
  );
}
// Resumen para lectores de pantalla. Solo se actualiza cuando todas las
// cadenas han respondido, para no leer cada llegada parcial.
function StoreResultsLive({ app, state }) {
  const ts = app.t.stores;
  const [text, setText] = React.useState('');
  const summary = state && !state.offline ? StoresCore.searchSummary(state) : null;
  const key = summary && summary.settled ? `${state.reqId}:${summary.count}:${summary.unavailable}` : '';
  React.useEffect(() => {
    if (key) setText(tpl(ts.resultsCount, { count: summary.count, unavailable: summary.unavailable }));
  }, [key]);
  return <div data-testid="results-live" role="status" aria-live="polite" style={SR_ONLY}>{text}</div>;
}
Object.assign(window, { StoreResultsList, StoreResultsPanel, StoreResultsLive, subOf });
