const { Button, IconButton, Card, Icon, Input, Badge } = window.MiradorDesignSystem_4bae5c;

const TIPOS = ['Casa', 'Departamento', 'Terreno', 'Local comercial', 'Oficina', 'Villa'];
const FINS = ['BIESS', 'Crédito VIP', 'Miti-Miti', 'Banca privada'];
const ORDENES = [
  { v: 'recientes', l: 'Más recientes' }, { v: 'precio-asc', l: 'Precio: menor a mayor' },
  { v: 'precio-desc', l: 'Precio: mayor a menor' }, { v: 'area-desc', l: 'Área: mayor a menor' }
];
const EMPTY = { op: '', tipo: [], min: '', max: '', hab: 0, ban: 0, parq: 0, areaMin: '', sector: '', ciudad: '', fin: [], amen: [], orden: 'recientes', q: '' };

function readUrl() {
  const p = new URLSearchParams(location.search), f = { ...EMPTY };
  if (p.get('q')) f.q = p.get('q');
  if (p.get('tipo')) f.tipo = [p.get('tipo')];
  if (p.get('pmin')) f.min = p.get('pmin');
  if (p.get('pmax')) f.max = p.get('pmax');
  return f;
}
const matches = (p, f, saved, onlySaved) => {
  if (onlySaved && !saved[p.id]) return false;
  const hay = (p.titulo + ' ' + p.sector + ' ' + p.ciudad + ' ' + (p.direccion || '')).toLowerCase();
  if (f.q && !hay.includes(f.q.toLowerCase())) return false;
  if (f.op && p.operacion !== f.op) return false;
  if (f.tipo.length && !f.tipo.includes(p.tipo)) return false;
  if (f.min && p.precio < +f.min) return false;
  if (f.max && p.precio > +f.max) return false;
  if (f.hab && p.habitaciones < f.hab) return false;
  if (f.ban && p.banos < f.ban) return false;
  if (f.parq && p.parqueaderos < f.parq) return false;
  if (f.areaMin && p.area < +f.areaMin) return false;
  if (f.sector && p.sector !== f.sector) return false;
  if (f.ciudad && p.ciudad !== f.ciudad) return false;
  if (f.fin.length && !f.fin.some(x => (p.financiamiento || '').toLowerCase().includes(x.toLowerCase().split(' ')[0]))) return false;
  if (f.amen.length && !f.amen.every(a => (p.amenidades || []).some(x => x.toLowerCase().includes(a.toLowerCase())))) return false;
  return true;
};
const sortBy = (rows, orden) => {
  const r = [...rows];
  if (orden === 'precio-asc') r.sort((a, b) => a.precio - b.precio);
  else if (orden === 'precio-desc') r.sort((a, b) => b.precio - a.precio);
  else if (orden === 'area-desc') r.sort((a, b) => b.area - a.area);
  else r.sort((a, b) => (b.fecha || '').localeCompare(a.fecha || ''));
  return r;
};
const countActive = (f) => (f.op ? 1 : 0) + f.tipo.length + (f.min || f.max ? 1 : 0) + (f.hab ? 1 : 0) + (f.ban ? 1 : 0) +
  (f.parq ? 1 : 0) + (f.areaMin ? 1 : 0) + (f.sector ? 1 : 0) + (f.ciudad ? 1 : 0) + f.fin.length + f.amen.length;

/* memo: sin esto, mover el mouse por la lista cambia el estado "hover" del
   padre y repinta TODAS las tarjetas — de ahí el parpadeo de las fotos. */
const ResultCard = React.memo(function ResultCard({ p, saved, onSave, active, onHover }) {
  const [i, setI] = React.useState(0);
  const fotos = p.fotos || [];
  /* Solo tres capas montadas (la vista y sus vecinas): el cambio de foto es un
     display, sin transición ni descarga. El resto se precarga en memoria al
     acercarse a la tarjeta, así el primer clic ya no espera nada. */
  const vecinas = (n) => fotos.length < 2 ? [0] : [n, (n + 1) % fotos.length, (n - 1 + fotos.length) % fotos.length];
  const montadas = vecinas(i);
  const ir = (k) => setI((k + fotos.length) % fotos.length);
  const step = (d) => (e) => { e.preventDefault(); e.stopPropagation(); ir(i + d); };
  const precargado = React.useRef(false);
  const precargar = () => {
    if (precargado.current || fotos.length < 2) return;
    precargado.current = true;
    fotos.forEach(src => { const im = new Image(); im.src = src; });
  };
  const tocar = React.useRef(null);
  const onTouchStart = (e) => { precargar(); tocar.current = e.touches[0].clientX; };
  const onTouchEnd = (e) => {
    if (tocar.current == null || fotos.length < 2) return;
    const d = e.changedTouches[0].clientX - tocar.current;
    tocar.current = null;
    if (Math.abs(d) > 40) { e.preventDefault(); ir(i + (d < 0 ? 1 : -1)); }
  };
  const meta = [p.habitaciones && p.habitaciones + ' dorm', p.banos && p.banos + ' baños', p.area && num(p.area) + ' m²'].filter(Boolean).join(' · ');
  return (
    <a className={'cm-card cm-res' + (active ? ' is-active' : '')} href={detailUrl(p.id, p.titulo)}
      onMouseEnter={() => onHover(p.id)} onMouseLeave={() => onHover(null)}>
      <div className={'cm-card-photo' + (fotos.length > 1 ? ' cm-slider' : '')} onMouseEnter={precargar} onTouchStart={onTouchStart} onTouchEnd={onTouchEnd}>
        {fotos.length > 1
          ? fotos.map((src, k) => montadas.indexOf(k) >= 0
            ? <img key={src} src={src} alt={k === i ? p.titulo : ''} className={k === i ? 'is-on' : ''} loading={k ? 'lazy' : 'eager'} decoding="async" />
            : null)
          : <img src={fotos[0]} alt={p.titulo} loading="lazy" />}
        {p.financiamiento ? <span className="cm-card-badge"><Badge tone={finTone(p.financiamiento)} icon={finIcon(p.financiamiento)}>{p.financiamiento}</Badge></span> : null}
        <span className="cm-card-heart">
          <IconButton icon="heart" variant="overlay" size={32} active={saved} label="Guardar" onClick={(e) => { e.preventDefault(); e.stopPropagation(); onSave(); }} />
        </span>
        {fotos.length > 1 ? (
          <React.Fragment>
            <button className="cm-arrow is-prev" aria-label="Foto anterior" onClick={step(-1)}><Icon name="chevron-left" size={16} /></button>
            <button className="cm-arrow is-next" aria-label="Siguiente foto" onClick={step(1)}><Icon name="chevron-right" size={16} /></button>
            <span className="cm-dots">{fotos.slice(0, 6).map((_, k) => <span key={k} className={k === i ? 'is-on' : ''} onClick={(e) => { e.preventDefault(); e.stopPropagation(); ir(k); }}></span>)}</span>
          </React.Fragment>) : null}
      </div>
      <div className="cm-card-body">
        <span className="cm-card-title">{p.titulo}</span>
        <span className="cm-card-sub">{p.sector}, {p.ciudad}</span>
        <span className="cm-card-sub">{meta}</span>
        <span className="cm-card-price">{money(p.precio)}</span>
      </div>
    </a>
  );
});

/* Agrupa propiedades cercanas en espacio de pantalla, con la misma matemática
   de Mercator que usa Google, para que cada precio caiga en su ubicación real
   y dos propiedades a menos de ~110×34 px no se pisen entre sí. Puro: no toca
   el DOM ni crea objetos Marker, así que recalcularlo nunca "parpadea". */
function agruparPines(rows, map) {
  const maps = window.google?.maps;
  if (!maps || !map) return [];
  const proj = map.getProjection();
  const bounds = map.getBounds();
  if (!proj || !bounds) return [];
  const scale = Math.pow(2, map.getZoom());
  const ne = proj.fromLatLngToPoint(bounds.getNorthEast());
  const sw = proj.fromLatLngToPoint(bounds.getSouthWest());
  const originX = sw.x * scale, originY = ne.y * scale;
  const pts = rows.filter(p => p.lat && p.lng).map(p => {
    const w = proj.fromLatLngToPoint(new maps.LatLng(p.lat, p.lng));
    return { p, x: w.x * scale - originX, y: w.y * scale - originY };
  });
  const used = new Set(), groups = [];
  const W = 116, H = 34;
  pts.forEach((a, i) => {
    if (used.has(i)) return;
    used.add(i);
    const g = [a];
    let cx = a.x, cy = a.y, grew = true;
    while (grew) {
      grew = false;
      pts.forEach((b, j) => {
        if (used.has(j)) return;
        if (Math.abs(cx - b.x) < W && Math.abs(cy - b.y) < H) {
          used.add(j); g.push(b); grew = true;
          cx = g.reduce((s, o) => s + o.x, 0) / g.length;
          cy = g.reduce((s, o) => s + o.y, 0) / g.length;
        }
      });
    }
    groups.push(g);
  });
  return groups.map(g => {
    const cx = g.reduce((s, o) => s + o.x, 0) / g.length;
    const cy = g.reduce((s, o) => s + o.y, 0) / g.length;
    return { key: g.map(o => o.p.id).sort().join('|'), left: cx, top: cy, items: g.map(o => o.p) };
  });
}

/* Mapa vivo con píldoras de precio en HTML normal (las mismas que usa
   MapaEstatico), no con google.maps.Marker. Google solo dibuja los tiles;
   React coloca y actualiza las cápsulas. Antes cada movimiento del mouse
   borraba y recreaba TODOS los marcadores del mapa, lo que hacía que las
   cápsulas parpadearan y desaparecieran al pasar el cursor cerca. Con
   elementos de React, un cambio de hover solo cambia una clase CSS: no hay
   nada que destruir ni volver a crear. */
function ResultsMap({ rows, hover }) {
  const wrap = React.useRef(null);
  const node = React.useRef(null);
  const mapRef = React.useRef(null);
  const fittedRef = React.useRef('');
  const rafRef = React.useRef(0);
  const [failed, setFailed] = React.useState(false);
  const [tick, setTick] = React.useState(0);
  const [openGroup, setOpenGroup] = React.useState(null);
  const [caja, setCaja] = React.useState(null);

  /* Una sola actualización por frame, aunque Google dispare bounds_changed
     decenas de veces durante un arrastre o una animación de zoom. */
  const recompute = React.useCallback(() => {
    if (rafRef.current) return;
    rafRef.current = requestAnimationFrame(() => { rafRef.current = 0; setTick(t => t + 1); });
  }, []);

  React.useEffect(() => {
    let alive = true, vigia = null;
    loadMaps().then(maps => {
      if (!alive || !node.current) return;
      const map = new maps.Map(node.current, {
        center: { lat: -0.15, lng: -78.47 }, zoom: 11, styles: MAP_STYLE,
        mapTypeControl: false, streetViewControl: false, fullscreenControl: false,
        clickableIcons: false, gestureHandling: 'greedy'
      });
      mapRef.current = map;
      /* La key puede cargar el script y aun así no pintar nada (dominio no
         autorizado, cuota agotada): si a los dos segundos el contenedor no
         tiene los nodos de Google, se pasa al mapa estático en vez de dejar
         un panel vacío. */
      let pintado = false;
      map.addListener('tilesloaded', () => { pintado = true; });
      vigia = setTimeout(() => {
        if (!alive) return;
        if (!pintado && !node.current?.querySelector('.gm-style')) setFailed(true);
      }, 2000);
      map.addListener('idle', recompute);
      map.addListener('bounds_changed', recompute);
      map.addListener('zoom_changed', () => setOpenGroup(null));
      recompute();
    }).catch(() => { if (alive) setFailed(true); });
    const previo = window.gm_authFailure;
    window.gm_authFailure = () => { setFailed(true); if (typeof previo === 'function') previo(); };
    return () => {
      alive = false; clearTimeout(vigia);
      if (rafRef.current) cancelAnimationFrame(rafRef.current);
      window.gm_authFailure = previo;
    };
  }, [recompute]);

  React.useEffect(() => {
    if (!wrap.current) return;
    const medir = () => { if (wrap.current) setCaja({ w: wrap.current.clientWidth, h: wrap.current.clientHeight }); };
    medir();
    const ro = new ResizeObserver(() => { medir(); recompute(); });
    ro.observe(wrap.current);
    return () => ro.disconnect();
  }, [recompute]);

  React.useEffect(() => {
    const map = mapRef.current, maps = window.google?.maps;
    if (!map || !maps) return;
    /* Reencuadrar solo cuando cambia el conjunto de resultados, no al hacer zoom. */
    const key = rows.map(r => r.id).join(',');
    if (key !== fittedRef.current) {
      fittedRef.current = key;
      const b = new maps.LatLngBounds();
      rows.forEach(p => { if (p.lat && p.lng) b.extend({ lat: p.lat, lng: p.lng }); });
      if (!b.isEmpty()) { map.fitBounds(b, 56); if (rows.length === 1) map.setZoom(15); }
    }
    recompute();
  }, [rows, recompute]);

  const groups = React.useMemo(() => agruparPines(rows, mapRef.current), [rows, tick]);

  /* Mismo cálculo de posición para la lista emergente de un grupo que usa
     MapaEstatico: nunca sale por arriba ni por abajo del contenedor. */
  const popTop = (top, n) => {
    if (!caja) return top + 20;
    const alto = Math.min(n * 63 + 12, caja.h - 28);
    const abajo = top + 20 + alto <= caja.h - 14;
    const deseado = abajo ? top + 20 : top - 20 - alto;
    return Math.min(Math.max(deseado, 14), Math.max(14, caja.h - 14 - alto));
  };

  /* Cerrar la tarjeta al hacer clic en cualquier parte fuera de ella: en el
     resto del mapa, en la lista de propiedades, en la barra, donde sea. Antes
     solo se cerraba si el mouse salía por completo del mapa, así que un clic
     afuera —o cualquier toque en móvil— la dejaba abierta para siempre. */
  React.useEffect(() => {
    if (!openGroup) return;
    const cerrar = () => setOpenGroup(null);
    document.addEventListener('mousedown', cerrar);
    document.addEventListener('touchstart', cerrar);
    return () => {
      document.removeEventListener('mousedown', cerrar);
      document.removeEventListener('touchstart', cerrar);
    };
  }, [openGroup]);

  /* Sin mapa interactivo (la key no tiene permiso para este dominio) el mapa
     sigue siendo un mapa: los mismos tiles como imagen y las mismas cápsulas de
     precio sobre su ubicación real. */
  if (failed) return <MapaEstatico rows={rows} />;

  return (
    <div ref={wrap} key="mapa-vivo" className="cm-map-box">
      <div ref={node} className="cm-map-canvas"></div>
      <div className="cm-map-pins">
        {groups.map(g => {
          const pos = { left: g.left + 'px', top: g.top + 'px' };
          const abierto = openGroup === g.key;
          const solo = g.items.length === 1;
          const label = solo ? money(g.items[0].precio) : g.items.length + ' propiedades';
          return (
            <React.Fragment key={g.key}>
              <button type="button" className={'cm-map-pill' + (abierto || (solo && g.items[0].id === hover) ? ' is-on' : '')} style={pos}
                onClick={(e) => { e.stopPropagation(); setOpenGroup(abierto ? null : g.key); }}>
                {label}
              </button>
              {abierto ? (
                <div className="cm-map-pop" onMouseDown={(e) => e.stopPropagation()} onTouchStart={(e) => e.stopPropagation()}
                  style={{ left: (caja ? Math.min(Math.max(g.left, 146), Math.max(146, caja.w - 146)) : g.left) + 'px', top: popTop(g.top, g.items.length) + 'px' }}>
                  {g.items.map(p => (
                    <a key={p.id} href={detailUrl(p.id, p.titulo)}>
                      <strong>{money(p.precio)}</strong>
                      <span>{p.titulo}</span>
                      <em>{p.sector}</em>
                    </a>
                  ))}
                </div>
              ) : null}
            </React.Fragment>
          );
        })}
      </div>
    </div>
  );
}

function MapaEstatico({ rows }) {
  /* La caja se mide con un ref de callback: así el observer se ata al nodo que
     React realmente monta, y no a uno que pudo quedar atrás. Sin límite de
     intentos: si el panel gana alto tarde, la medida llega igual. */
  const [nodo, setNodo] = React.useState(null);
  const [caja, setCaja] = React.useState(null);
  const box = React.useRef(null);
  const poner = React.useCallback((n) => { box.current = n; setNodo(n); }, []);
  React.useEffect(() => {
    if (!nodo) return;
    const medir = () => {
      const r = nodo.getBoundingClientRect();
      if (r.width > 40 && r.height > 40) {
        setCaja(v => (v && v.w === Math.round(r.width) && v.h === Math.round(r.height))
          ? v : { w: Math.round(r.width), h: Math.round(r.height) });
      }
    };
    medir();
    const ro = new ResizeObserver(medir);
    ro.observe(nodo);
    const t = setInterval(medir, 250);
    return () => { ro.disconnect(); clearInterval(t); };
  }, [nodo]);
  const [sinTiles, setSinTiles] = React.useState(false);
  const [grupo, setGrupo] = React.useState(null);
  /* Mismo cierre por clic afuera que usa el mapa vivo: sin esto, la tarjeta
     abierta solo se cerraba si el clic caía justo en el fondo del contenedor. */
  React.useEffect(() => {
    if (grupo === null) return;
    const cerrar = () => setGrupo(null);
    document.addEventListener('mousedown', cerrar);
    document.addEventListener('touchstart', cerrar);
    return () => {
      document.removeEventListener('mousedown', cerrar);
      document.removeEventListener('touchstart', cerrar);
    };
  }, [grupo]);
  /* El contenedor recorta lo que sobresale, así que la lista de un grupo se
     coloca con un top explícito, ya sujeto a la caja: nunca sale por arriba ni
     por abajo. La altura se estima por fila (63px) y se topa al alto útil; si
     no cabe, la lista hace scroll dentro. */
  const popTop = (top, n) => {
    if (!caja) return top + 20;
    const alto = Math.min(n * 63 + 12, caja.h - 28);
    const abajo = top + 20 + alto <= caja.h - 14;
    const deseado = abajo ? top + 20 : top - 20 - alto;
    return Math.min(Math.max(deseado, 14), Math.max(14, caja.h - 14 - alto));
  };

  const pts = rows.filter(p => p.lat && p.lng);
  /* La imagen estática se pide con el MISMO aspecto que el contenedor (la API
     tope es 640px de lado), y las cápsulas se proyectan en ese espacio y luego
     se escalan por k. Así el mapa y los precios viven en la misma geometría. */
  const pedido = React.useMemo(() => {
    if (!caja) return null;
    const w = Math.min(640, caja.w);
    return { w, h: Math.max(80, Math.min(640, Math.round(w * caja.h / caja.w))), k: caja.w / w };
  }, [caja && caja.w, caja && caja.h]);

  const vista = React.useMemo(() => {
    if (!pedido || !pts.length) return null;
    const { w, h, k } = pedido;
    /* Mercator normalizado: x,y en 0..1 sobre el mundo entero. */
    const proj = (p) => {
      const s = Math.sin(p.lat * Math.PI / 180);
      return { x: (p.lng + 180) / 360, y: 0.5 - Math.log((1 + s) / (1 - s)) / (4 * Math.PI) };
    };
    const ps = pts.map(p => ({ p, ...proj(p) }));
    const x0 = Math.min(...ps.map(o => o.x)), x1 = Math.max(...ps.map(o => o.x));
    const y0 = Math.min(...ps.map(o => o.y)), y1 = Math.max(...ps.map(o => o.y));
    /* Zoom que encuadra todo, con un margen mínimo para que las cápsulas del
       borde no se salgan. */
    const anchoUtil = Math.max(120, w - 72), altoUtil = Math.max(120, h - 40);
    let z = 15;
    if (x1 > x0 || y1 > y0) {
      z = Math.min(
        Math.log2(anchoUtil / 256 / Math.max(x1 - x0, 1e-9)),
        Math.log2(altoUtil / 256 / Math.max(y1 - y0, 1e-9))
      );
    }
    z = Math.max(4, Math.min(16, Math.floor(z)));
    const escala = 256 * Math.pow(2, z);
    const cx = (x0 + x1) / 2, cy = (y0 + y1) / 2;
    const lat = (2 * Math.atan(Math.exp((0.5 - cy) * 2 * Math.PI)) - Math.PI / 2) * 180 / Math.PI;
    const lng = cx * 360 - 180;
    /* Cápsulas en coordenadas del contenedor. Las que se encimarían se juntan
       en un grupo que guarda TODAS sus propiedades, no solo la primera: al
       abrirlo se listan una por una. La distancia de agrupado es el tamaño real
       de la cápsula, así que dos cápsulas nunca se tapan entre sí. */
    const pines = [];
    ps.forEach(o => {
      const left = ((o.x - cx) * escala + w / 2) * k;
      const top = ((o.y - cy) * escala + h / 2) * k;
      if (left < 10 || left > caja.w - 10 || top < 10 || top > caja.h - 10) return;
      const choque = pines.find(q => Math.abs(q.left - left) < 100 && Math.abs(q.top - top) < 32);
      if (choque) { choque.items.push(o.p); return; }
      pines.push({ left, top, items: [o.p] });
    });
    return { lat, lng, z, pines };
  }, [pedido, pts.map(p => p.id + ':' + p.lat).join(',')]);

  const src = pedido && vista
    ? 'https://maps.googleapis.com/maps/api/staticmap?center=' + vista.lat.toFixed(6) + ',' + vista.lng.toFixed(6)
      + '&zoom=' + vista.z + '&size=' + pedido.w + 'x' + pedido.h
      + '&scale=2&maptype=roadmap&language=es&region=EC&style=feature:poi|visibility:off&key=' + BACKEND.mapsKey
    : null;

  return (
    <div ref={poner} className={'cm-map-static' + (sinTiles ? ' is-plain' : '')} key="mapa-estatico">
      {src && !sinTiles ? (
        <img className="cm-map-static-img" src={src} alt="Mapa de las propiedades del listado"
          onError={() => setSinTiles(true)}
          onLoad={(e) => { if (e.target.naturalWidth < 100) setSinTiles(true); }} />
      ) : null}
      {vista ? vista.pines.map(({ left, top, items }, i) => {
        const abierto = grupo === i;
        const pos = { left: left + 'px', top: top + 'px' };
        const label = items.length === 1 ? money(items[0].precio) : items.length + ' propiedades';
        return (
          <React.Fragment key={'g' + i}>
            <button type="button" className={'cm-map-pill' + (abierto ? ' is-on' : '')} style={pos}
              onClick={(e) => { e.stopPropagation(); setGrupo(abierto ? null : i); }}>
              {label}
            </button>
            {abierto ? (
              <div className="cm-map-pop" onMouseDown={(e) => e.stopPropagation()} onTouchStart={(e) => e.stopPropagation()}
                style={{ left: Math.min(Math.max(left, 146), Math.max(146, caja.w - 146)) + 'px', top: popTop(top, items.length) + 'px' }}>
                {items.map(p => (
                  <a key={p.id} href={detailUrl(p.id, p.titulo)}>
                    <strong>{money(p.precio)}</strong>
                    <span>{p.titulo}</span>
                    <em>{p.sector}</em>
                  </a>
                ))}
              </div>
            ) : null}
          </React.Fragment>
        );
      }) : null}
      <p className="cm-map-static-msg"><Icon name="map-pin" size={14} />Zona aproximada de cada propiedad</p>
    </div>
  );
}

function FilterPanel({ f, set, sectores, amenidades, onClear }) {
  const toggle = (key, v) => set({ ...f, [key]: f[key].includes(v) ? f[key].filter(x => x !== v) : [...f[key], v] });
  const stepRow = (key, label, icon) => (
    <div className="cm-frow">
      <span className="cm-flabel"><Icon name={icon} size={15} />{label}</span>
      <div className="cm-steps">
        {[0, 1, 2, 3, 4].map(n => (
          <button key={n} type="button" className={'cm-step' + (f[key] === n ? ' is-on' : '')} onClick={() => set({ ...f, [key]: n })}>{n === 0 ? 'Todos' : n + '+'}</button>
        ))}
      </div>
    </div>
  );
  return (
    <div className="cm-filters">
      <div className="cm-fgroup">
        <span className="cm-flabel"><Icon name="tag" size={15} />Operación</span>
        <div className="cm-chips">
          {['', 'Venta', 'Arriendo'].map(o => <button key={o || 'todas'} type="button" className={'cm-chip' + (f.op === o ? ' is-on' : '')} onClick={() => set({ ...f, op: o })}>{o || 'Todas'}</button>)}
        </div>
      </div>
      <div className="cm-fgroup">
        <span className="cm-flabel"><Icon name="house" size={15} />Tipo de propiedad</span>
        <div className="cm-chips">
          {TIPOS.map(t => <button key={t} type="button" className={'cm-chip' + (f.tipo.includes(t) ? ' is-on' : '')} onClick={() => toggle('tipo', t)}>{t}</button>)}
        </div>
      </div>
      <div className="cm-fgroup">
        <span className="cm-flabel"><Icon name="dollar-sign" size={15} />Rango de precio</span>
        <div className="cm-two">
          <Input label="Mínimo" type="number" min="0" step="1000" placeholder="0" value={f.min} onChange={e => set({ ...f, min: e.target.value })} />
          <Input label="Máximo" type="number" min="0" step="1000" placeholder="500.000" value={f.max} onChange={e => set({ ...f, max: e.target.value })} />
        </div>
      </div>
      {stepRow('hab', 'Habitaciones', 'bed-double')}
      {stepRow('ban', 'Baños', 'bath')}
      {stepRow('parq', 'Parqueaderos', 'car')}
      <div className="cm-fgroup">
        <span className="cm-flabel"><Icon name="ruler" size={15} />Área mínima (m²)</span>
        <Input type="number" min="0" step="10" placeholder="Cualquiera" value={f.areaMin} onChange={e => set({ ...f, areaMin: e.target.value })} />
      </div>
      <div className="cm-fgroup">
        <span className="cm-flabel"><Icon name="map-pin" size={15} />Sector</span>
        <select className="cm-select" value={f.sector} onChange={e => set({ ...f, sector: e.target.value })}>
          <option value="">Todos los sectores</option>
          {sectores.map(s => <option key={s} value={s}>{s}</option>)}
        </select>
      </div>
      <div className="cm-fgroup">
        <span className="cm-flabel"><Icon name="building-2" size={15} />Ciudad</span>
        <select className="cm-select" value={f.ciudad} onChange={e => set({ ...f, ciudad: e.target.value })}>
          <option value="">Todas</option><option value="Quito">Quito</option><option value="Ibarra">Ibarra</option>
        </select>
      </div>
      <div className="cm-fgroup">
        <span className="cm-flabel"><Icon name="landmark" size={15} />Financiamiento</span>
        <div className="cm-chips">
          {FINS.map(x => <button key={x} type="button" className={'cm-chip' + (f.fin.includes(x) ? ' is-on' : '')} onClick={() => toggle('fin', x)}>{x}</button>)}
        </div>
      </div>
      <div className="cm-fgroup">
        <span className="cm-flabel"><Icon name="sparkles" size={15} />Amenidades</span>
        <div className="cm-chips">
          {amenidades.map(a => <button key={a} type="button" className={'cm-chip' + (f.amen.includes(a) ? ' is-on' : '')} onClick={() => toggle('amen', a)}>{a}</button>)}
        </div>
      </div>
      <div className="cm-fclear"><Button variant="ghost" size="sm" onClick={onClear}>Limpiar filtros</Button></div>
    </div>
  );
}

function App() {
  const [all, catalogStatus] = useCatalog(true);
  const [saved, toggleSave] = useSaved();
  const [f, setF] = React.useState(readUrl);
  const [onlySaved, setOnlySaved] = React.useState(() => new URLSearchParams(location.search).has('guardadas'));
  const [hover, setHover] = React.useState(null);
  const [panel, setPanel] = React.useState(false);
  const [mapOn, setMapOn] = React.useState(false);
  const [join, setJoin] = React.useState(false);

  const sectores = React.useMemo(() => [...new Set(all.map(p => p.sector))].sort((a, b) => a.localeCompare(b, 'es')), [all]);
  const amenidades = React.useMemo(() => {
    const c = {};
    all.forEach(p => (p.amenidades || []).forEach(a => { c[a] = (c[a] || 0) + 1; }));
    return Object.keys(c).sort((a, b) => c[b] - c[a]).slice(0, 10);
  }, [all]);
  const rows = React.useMemo(() => sortBy(all.filter(p => matches(p, f, saved, onlySaved)), f.orden), [all, f, saved, onlySaved]);
  const active = countActive(f);
  React.useEffect(() => {
    const zona = f.sector || f.ciudad || 'Quito y los valles';
    const tipo = f.tipo.length ? f.tipo.join(', ').toLowerCase() + 's' : 'casas, departamentos y terrenos';
    const op = (f.op || 'Venta').toLowerCase();
    setSeo({
      title: `${f.tipo.length ? f.tipo.join(', ') : 'Propiedades'} en ${op} en ${zona} — casamia.ec`,
      description: `${rows.length} ${tipo} en ${op} en ${zona}. Filtra por precio, dormitorios, baños, área, sector y financiamiento BIESS, VIP, Miti-Miti o banca privada. Fotos, mapa y contacto directo en casamia.ec.`,
      canonical: SEO.site + SEO.paths.propiedades + (f.sector ? '?sector=' + encodeURIComponent(f.sector) : ''),
      keywords: ['casas en venta ' + zona, 'departamentos en venta ' + zona, 'propiedades ' + zona, 'inmobiliaria Quito'].join(', ')
    });
    jsonLd('lista', itemListLd(rows, `Propiedades en ${op} en ${zona}`, SEO.paths.propiedades));
    jsonLd('ruta', breadcrumbLd([['Inicio', SEO.paths.inicio], ['Propiedades', SEO.paths.propiedades]].concat(f.sector ? [[f.sector, SEO.paths.propiedades + '?sector=' + encodeURIComponent(f.sector)]] : [])));
    jsonLd('organizacion', orgLd());
    jsonLd('sitio', siteLd());
  }, [rows.length, f.sector, f.ciudad, f.tipo, f.op]);
  React.useEffect(() => { document.body.classList.toggle('cm-sheet-open', panel || mapOn); }, [panel, mapOn]);
  const savedCount = Object.values(saved).filter(Boolean).length;

  return (
    <div className="cm-wide">
      <Header onJoin={() => setJoin(true)} savedCount={savedCount}>
        <div className="cm-hsearch">
          <Icon name="search" size={16} />
          <input placeholder="Sector, ciudad o nombre de la propiedad" value={f.q} onChange={e => setF({ ...f, q: e.target.value })} />
          {f.q ? <button aria-label="Limpiar" onClick={() => setF({ ...f, q: '' })}><Icon name="x" size={15} /></button> : null}
        </div>
      </Header>
      {catalogStatus === 'loading' && <p role="status">Cargando propiedades…</p>}
      {catalogStatus === 'error' && <p role="status">No pudimos actualizar las propiedades. Reintentaremos la conexión automáticamente.</p>}


      <div className="cm-toolbar">
        <div className="cm-toolbar-in">
          <button className={'cm-tool' + (active ? ' is-on' : '')} onClick={() => setPanel(true)}>
            <Icon name="sliders-horizontal" size={16} />Filtros{active ? <span className="cm-count">{active}</span> : null}
          </button>
          <button className={'cm-tool' + (onlySaved ? ' is-on' : '')} onClick={() => setOnlySaved(!onlySaved)}>
            <Icon name="heart" size={16} />Guardadas{savedCount ? <span className="cm-count">{savedCount}</span> : null}
          </button>
          <span className="cm-tool-count">{rows.length} {rows.length === 1 ? 'propiedad' : 'propiedades'}</span>
          <select className="cm-select cm-sort" value={f.orden} onChange={e => setF({ ...f, orden: e.target.value })}>
            {ORDENES.map(o => <option key={o.v} value={o.v}>{o.l}</option>)}
          </select>
          <button className="cm-tool cm-map-toggle" onClick={() => setMapOn(true)}><Icon name="map-pin" size={16} />Mapa</button>
        </div>
      </div>

      <main className="cm-split">
        <section className="cm-results">
          {rows.length ? (
            <div className="cm-res-grid">
              {rows.map(p => <ResultCard key={p.id} p={p} saved={!!saved[p.id]} onSave={() => toggleSave(p.id)} active={hover === p.id} onHover={setHover} />)}
            </div>
          ) : (
            <div className="cm-empty">
              <Icon name="search-x" size={28} />
              <h2>{catalogStatus === 'loading' ? 'Cargando propiedades…' : catalogStatus === 'error' ? 'No pudimos cargar las propiedades' : 'Sin resultados con estos filtros'}</h2>
              <p>{catalogStatus === 'error' ? 'Hay un problema de conexión. Reintentaremos automáticamente.' : catalogStatus === 'loading' ? 'Espera un momento.' : 'Prueba ampliar la zona, subir el presupuesto o quitar algún filtro.'}</p>
              <Button variant="outline" onClick={() => { setF({ ...EMPTY }); setOnlySaved(false); }}>Limpiar todo</Button>
            </div>
          )}
        </section>
        <aside className={'cm-mapcol' + (mapOn ? ' is-open' : '')}>
          <button className="cm-map-close" onClick={() => setMapOn(false)} aria-label="Cerrar mapa"><Icon name="x" size={18} />Ver lista</button>
          <ResultsMap rows={rows} hover={hover} />
        </aside>
      </main>

      {panel ? (
        <React.Fragment>
          <div className="cm-pop-scrim" onClick={() => setPanel(false)}></div>
          <div className="cm-panel">
            <div className="cm-panel-head">
              <span>Filtros</span>
              <button aria-label="Cerrar" onClick={() => setPanel(false)}><Icon name="x" size={18} /></button>
            </div>
            <div className="cm-panel-body">
              <FilterPanel f={f} set={setF} sectores={sectores} amenidades={amenidades} onClear={() => setF({ ...EMPTY, q: f.q, orden: f.orden })} />
            </div>
            <div className="cm-panel-foot">
              <Button block onClick={() => setPanel(false)}>Ver {rows.length} {rows.length === 1 ? 'propiedad' : 'propiedades'}</Button>
            </div>
          </div>
        </React.Fragment>) : null}

      <Footer onJoin={() => setJoin(true)} />
      <MobileBar />
      <JoinDialog open={join} onClose={() => setJoin(false)} />
    </div>
  );
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
