// ─── Compras: typeaheads compartidos (Expediente / Compra) ───────────────
// ExpedienteCombobox: input con portal que sugiere EXP-… mientras tipeás — lo usan
// Compras (vincular a un expediente), Logística (ídem) y Caja Chica (ídem, opcional).
// CompraCombobox: typeahead de OC — lo usa Logística para vincular un ingreso a su
// compra de origen. expedienteLabelFromId: resuelve el uuid real (columna
// expediente_id, FK real en compras/logistica/caja_chica — P2) a su publicId visible
// (EXP-…) para mostrarlo en listas y metaceldas de solo lectura, mismo patrón que la
// resolución compraId→numero (screens/logistica/lista.jsx) y documentoLabel/
// expedienteLabel de Aprobaciones (dominio/margen.js). fetchExpedienteById/
// useExpedienteSeed/resolveExpedienteIdForSave/expedienteMetaField: fix de raíz (P9,
// review rechazada) — ver bloque de abajo. Expone a window: ExpedienteCombobox,
// CompraCombobox, expedienteLabelFromId, fetchExpedienteById, useExpedienteSeed,
// resolveExpedienteIdForSave, expedienteMetaField. Consume window.EXPEDIENTES/COMPRAS
// (colecciones hidratadas) + COMPRA_ESTADOS (screens/compras/lista.jsx) + formatMoney
// (dominio/formato.js).
//
// Extraído tal cual de screens-others.jsx (P9, split del cajón de 5 módulos) — cero
// cambio de comportamiento; expedienteLabelFromId es nuevo (ver P9: el circuito de
// escritura real necesita separar "uuid que se persiste" de "texto que se edita/
// muestra", el mismo problema que proveedorId/proveedor ya resuelve el resto del
// archivo con PartySelectorModal).

function expedienteLabelFromId(id, fallback) {
  if (fallback === undefined) fallback = '—';
  if (!id) return fallback;
  const exp = (window.EXPEDIENTES || []).find((e) => e.id === id);
  return exp ? exp.publicId : fallback;
}

// ─── Fix de raíz (P9, review rechazada): resolución honesta de FKs fuera de la
// ventana hidratada ──────────────────────────────────────────────────────────
// window.EXPEDIENTES solo hidrata ~500 de ~2100 reales (BIG_LIMIT, core/store.js —
// los más recientes). Compras/Logística/Caja Chica resolvían el expediente
// SIEMPRE por `find` sobre esa ventana: (a) tipear un publicId real pero fuera de
// ventana guardaba `expediente_id=null` con toast de éxito; (b) DESTRUCTIVO — el
// draft se sembraba con expedienteLabelFromId(...,'') y, si el uuid no estaba
// hidratado, sembraba '' → guardar SIN TOCAR NADA pisaba en silencio un FK válido
// con null. Fix: /api/expedientes/[id] acepta uuid U publicId (ver idColumn en
// rest.ts) — se usa esa MISMA ruta en las dos direcciones (siembra: uuid→publicId;
// guardado de texto nuevo: publicId→uuid), igual que openExpedienteFor
// (core/router.jsx). Nunca se sembra/guarda '' o null cuando el FK original existe.

// Trae un expediente por uuid o publicId (auto-detectado en el backend) y lo
// hidrata en window.EXPEDIENTES si no estaba. Devuelve el expediente o null
// (404 / error de red) — nunca rechaza, así el caller solo chequea el resultado.
function fetchExpedienteById(id) {
  if (!id) return Promise.resolve(null);
  return fetch(`/api/expedientes/${encodeURIComponent(id)}`, { credentials: 'same-origin' })
    .then((r) => (r.ok ? r.json() : Promise.reject(r.status)))
    .then((exp) => {
      if (!exp || !exp.id) throw new Error('sin expediente');
      const col = window.EXPEDIENTES || (window.EXPEDIENTES = []);
      if (!col.some((x) => x.id === exp.id)) col.push(exp);
      return exp;
    })
    .catch((err) => {
      console.warn('[fetchExpedienteById]', id, err);
      return null;
    });
}

// Siembra honesta del campo Expediente al entrar a editar: si `expedienteId`
// (uuid del original) existe pero no resuelve en la ventana hidratada, lo trae
// por API y actualiza el draft con el publicId real — nunca deja el campo en ''
// (vacío editable). Mientras resuelve (`loading`) o si la resolución falla
// (`notFound` — expediente inaccesible/borrado) el caller debe mostrar el campo
// BLOQUEADO (ver expedienteMetaField). `seedLabel` es el texto "de referencia":
// si al guardar el draft sigue siendo EXACTAMENTE este valor, el usuario no tocó
// el campo y se conserva el `expedienteId` real sin re-resolverlo por texto
// (resolveExpedienteIdForSave, caso ii) — así ni el loading ni un 404 pasajero
// arriesgan pisar el FK de un documento que el usuario no tocó.
function useExpedienteSeed(expedienteId, editing, patch) {
  const [seedLabel, setSeedLabel] = React.useState(() => expedienteLabelFromId(expedienteId, ''));
  const [loading, setLoading] = React.useState(false);
  const [notFound, setNotFound] = React.useState(false);
  React.useEffect(() => {
    if (!editing || !expedienteId) return;
    if (expedienteLabelFromId(expedienteId, null)) return; // ya hidratado, nada que resolver
    setLoading(true);
    setNotFound(false);
    fetchExpedienteById(expedienteId).then((exp) => {
      setLoading(false);
      if (exp) { setSeedLabel(exp.publicId); patch('expedienteId', exp.publicId); }
      else { setNotFound(true); }
    });
  }, [editing, expedienteId]);
  return { seedLabel, loading, notFound };
}

// Resuelve el texto tipeado/elegido del draft al uuid real a guardar. Nunca
// null/texto-libre cuando el original tenía un expediente real: (i) matchea en
// la ventana hidratada → su uuid; (ii) no matchea pero es EXACTAMENTE el label
// sembrado del original (el usuario no tocó el campo, ver useExpedienteSeed) →
// conserva `originalId`; (iii) texto nuevo → se intenta resolver contra el
// backend por publicId. Si (iii) no existe, devuelve `notFoundText` — el caller
// debe abortar el guardado y mostrar el error inline (nunca guardar null).
async function resolveExpedienteIdForSave(draftText, seedLabel, originalId) {
  const text = (draftText || '').trim();
  if (!text) return { id: null }; // campo vaciado a propósito → desvincular
  const local = (window.EXPEDIENTES || []).find((e) => e.publicId === text);
  if (local) return { id: local.id };
  if (originalId && text === seedLabel) return { id: originalId };
  const exp = await fetchExpedienteById(text);
  if (exp) return { id: exp.id };
  return { id: undefined, notFoundText: text };
}

// Arma el `it` de MicosMetaCell para el campo Expediente — mismo combobox +
// mismo bloqueo honesto en los 3 write-paths (Compras/Logística/Caja Chica).
// Centralizado para que un fix futuro no quede aplicado en 2 de los 3 archivos.
function expedienteMetaField({ editing, expSeed, draftValue, shownValue, patch, placeholder, mutedWhenEmpty, error, onClearError }) {
  const blocked = expSeed.loading || expSeed.notFound;
  return {
    label: 'Expediente',
    editable: editing && !blocked,
    locked: editing && blocked,
    lockedTitle: expSeed.loading
      ? 'Cargando el expediente vinculado…'
      : 'No se pudo cargar el expediente vinculado — se conserva el vínculo actual',
    input: 'text', mono: true, combobox: 'expediente',
    accent: !!shownValue,
    value: editing
      ? (expSeed.loading ? 'Cargando…' : expSeed.notFound ? 'Vinculado (no disponible)' : draftValue)
      : (shownValue || '—'),
    muted: mutedWhenEmpty ? (!editing && !shownValue) : undefined,
    placeholder: placeholder || 'EXP-…',
    error,
    onChange: (v) => { if (onClearError) onClearError(); patch('expedienteId', v); },
    onPick: (e) => { if (onClearError) onClearError(); patch('expedienteId', e.publicId); },
  };
}

// ─── Placement viewport-aware del popover (portal position:fixed) ──────────────
// Mismo criterio que ComprobanteCombobox (screens/finanzas/imputacion.jsx): si abajo
// del input NO entra el popover (cae bajo el fold), se abre HACIA ARRIBA, así las filas
// quedan SIEMPRE dentro del viewport. Un `position:fixed` anclado siempre a `rect.bottom`
// era inalcanzable con el input bajo — el navegador no scrollea un fijo a la vista y la
// fila quedaba fuera de pantalla (hallazgo flujo stock-ingreso-recepcion: la fila de
// Compras existía y era estable pero "element is outside of the viewport" al clickearla).
// `.inv-pop` (assets/styles.css) ya trae `max-height:320px; overflow-y:auto`, así que si
// hay muchas filas scrollea solo; NO se fija zIndex inline a propósito, para conservar el
// `z-index:9999` del CSS (bajarlo a mano lo mandaba detrás de otros overlays).
function popoverStyleParaInput(rect, matchesLen, minWidth) {
  const GAP = 5;
  const estH = Math.min(320, 34 + matchesLen * 52);
  const spaceBelow = window.innerHeight - rect.bottom - GAP;
  const spaceAbove = rect.top - GAP;
  const openUp = spaceBelow < estH && spaceAbove > spaceBelow;
  const base = { position: 'fixed', left: rect.left, width: Math.max(rect.width, minWidth) };
  return openUp
    ? { ...base, bottom: window.innerHeight - rect.top + GAP, maxHeight: Math.max(140, spaceAbove) }
    : { ...base, top: rect.bottom + GAP, maxHeight: Math.max(140, spaceBelow) };
}

// ─── Typeahead de Expedientes (mismo patrón que InventoryDescCell) ──
// Input con popover en portal que sugiere los EXP-… existentes; filtra por
// ID, paciente o entidad mientras tipeás. Al elegir, setea el publicId.
function ExpedienteCombobox({ value, onChange, onPick }) {
  const [open, setOpen] = React.useState(false);
  const [rect, setRect] = React.useState(null);
  const inputRef = React.useRef(null);
  const exps = window.EXPEDIENTES || [];
  const query = (value || '').trim().toLowerCase();
  const matches = exps.filter((e) => {
    const hay = `${e.publicId} ${e.particular ? e.particular.nombre + ' ' + e.particular.apellido : ''} ${e.entidad ? e.entidad.razonSocial : ''}`.toLowerCase();
    return hay.includes(query);
  }).slice(0, 8);

  const place = () => { if (inputRef.current) setRect(inputRef.current.getBoundingClientRect()); };
  React.useEffect(() => {
    if (!open) return;
    place();
    const close = () => setOpen(false);
    window.addEventListener('scroll', close, true);
    window.addEventListener('resize', close);
    const onDoc = (e) => {
      const inPop = e.target.closest && e.target.closest('.inv-pop');
      if (inputRef.current && !inputRef.current.contains(e.target) && !inPop) setOpen(false);
    };
    document.addEventListener('mousedown', onDoc);
    return () => {
      window.removeEventListener('scroll', close, true);
      window.removeEventListener('resize', close);
      document.removeEventListener('mousedown', onDoc);
    };
  }, [open]);

  const pop = (open && rect && matches.length > 0) ? ReactDOM.createPortal(
    <div className="inv-pop" style={popoverStyleParaInput(rect, matches.length, 340)}>
      <p className="inv-pop__head">Expedientes · elegí uno</p>
      {matches.map((e) => (
        <button key={e.id} type="button" className="inv-pop__row"
          onMouseDown={(ev) => { ev.preventDefault(); onPick(e); setOpen(false); }}>
          <span className="inv-pop__sku">{e.publicId}</span>
          <span className="inv-pop__desc">{e.particular ? `${e.particular.nombre} ${e.particular.apellido}` : '—'}</span>
          <span className="inv-pop__meta">
            <span className="inv-pop__stock">{e.entidad ? e.entidad.razonSocial : ''}</span>
          </span>
        </button>
      ))}
    </div>, document.body) : null;

  return (
    <div style={{ position: 'relative' }}>
      <input ref={inputRef} value={value || ''}
        onChange={(e) => { onChange(e.target.value); if (!open) setOpen(true); }}
        onFocus={() => setOpen(true)}
        placeholder="EXP-… o buscar"
        className="edit-input w-full text-[17px] font-extrabold leading-none tracking-tight font-mono tabular-nums"
        style={{ color: 'var(--accent)' }} />
      {pop}
    </div>
  );
}

// ─── Typeahead de Compras (origen de un ingreso de logística) ──
function CompraCombobox({ value, onPick }) {
  const [text, setText] = React.useState(value || '');
  React.useEffect(() => { setText(value || ''); }, [value]);
  const [open, setOpen] = React.useState(false);
  const [rect, setRect] = React.useState(null);
  const inputRef = React.useRef(null);
  const compras = window.COMPRAS || [];
  const query = text.trim().toLowerCase();
  const matches = compras
    .filter((c) => `${c.numero} ${c.proveedor}`.toLowerCase().includes(query))
    .slice(0, 8);

  const place = () => { if (inputRef.current) setRect(inputRef.current.getBoundingClientRect()); };
  React.useEffect(() => {
    if (!open) return;
    place();
    const close = () => setOpen(false);
    window.addEventListener('scroll', close, true);
    window.addEventListener('resize', close);
    const onDoc = (e) => {
      const inPop = e.target.closest && e.target.closest('.inv-pop');
      if (inputRef.current && !inputRef.current.contains(e.target) && !inPop) setOpen(false);
    };
    document.addEventListener('mousedown', onDoc);
    return () => {
      window.removeEventListener('scroll', close, true);
      window.removeEventListener('resize', close);
      document.removeEventListener('mousedown', onDoc);
    };
  }, [open]);

  const pop = (open && rect && matches.length > 0) ? ReactDOM.createPortal(
    <div className="inv-pop" style={popoverStyleParaInput(rect, matches.length, 340)}>
      <p className="inv-pop__head">Compras · elegí la OC de origen</p>
      {matches.map((c) => (
        <button key={c.id} type="button" className="inv-pop__row"
          onMouseDown={(ev) => { ev.preventDefault(); onPick(c); setText(c.numero); setOpen(false); }}>
          <span className="inv-pop__sku">{c.numero}</span>
          <span className="inv-pop__desc">{c.proveedor}</span>
          <span className="inv-pop__meta">
            <span className="inv-pop__price">{formatMoney(c.total)}</span>
            <span className="inv-pop__stock">{(COMPRA_ESTADOS[c.estado] || {}).label || c.estado}</span>
          </span>
        </button>
      ))}
    </div>, document.body) : null;

  return (
    <div style={{ position: 'relative' }}>
      <input ref={inputRef} value={text}
        onChange={(e) => { setText(e.target.value); if (!open) setOpen(true); }}
        onFocus={() => setOpen(true)}
        placeholder="OC-… o buscar"
        className="edit-input w-full text-[17px] font-extrabold leading-none tracking-tight font-mono tabular-nums"
        style={{ color: 'var(--accent)' }} />
      {pop}
    </div>
  );
}

Object.assign(window, {
  ExpedienteCombobox, CompraCombobox, expedienteLabelFromId,
  fetchExpedienteById, useExpedienteSeed, resolveExpedienteIdForSave, expedienteMetaField,
});
