// Header, FilterRail, ListingCard, Grid
// Exports to window for consumption by app.jsx

const { useState, useMemo, useEffect, useRef, useCallback } = React;

// --- icons (inline svg) ---
const I = {
  Search: (p) => (
    <svg width="16" height="16" viewBox="0 0 16 16" fill="none" {...p}>
      <circle cx="7" cy="7" r="5" stroke="currentColor" strokeWidth="1.5"/>
      <path d="M11 11l3 3" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
    </svg>
  ),
  Grid: (p) => (
    <svg width="14" height="14" viewBox="0 0 14 14" fill="none" {...p}>
      <rect x="1" y="1" width="5.5" height="5.5" rx="1" stroke="currentColor" strokeWidth="1.4"/>
      <rect x="7.5" y="1" width="5.5" height="5.5" rx="1" stroke="currentColor" strokeWidth="1.4"/>
      <rect x="1" y="7.5" width="5.5" height="5.5" rx="1" stroke="currentColor" strokeWidth="1.4"/>
      <rect x="7.5" y="7.5" width="5.5" height="5.5" rx="1" stroke="currentColor" strokeWidth="1.4"/>
    </svg>
  ),
  Map: (p) => (
    <svg width="14" height="14" viewBox="0 0 14 14" fill="none" {...p}>
      <path d="M1 3l4-2 4 2 4-2v10l-4 2-4-2-4 2V3z" stroke="currentColor" strokeWidth="1.3" strokeLinejoin="round"/>
      <path d="M5 1v10M9 3v10" stroke="currentColor" strokeWidth="1.3"/>
    </svg>
  ),
  Camera: (p) => (
    <svg width="12" height="12" viewBox="0 0 14 14" fill="none" {...p}>
      <rect x="1" y="3.5" width="12" height="8.5" rx="1.5" stroke="currentColor" strokeWidth="1.3"/>
      <path d="M4.5 3.5L5.5 2h3l1 1.5" stroke="currentColor" strokeWidth="1.3" strokeLinejoin="round"/>
      <circle cx="7" cy="8" r="2" stroke="currentColor" strokeWidth="1.3"/>
    </svg>
  ),
  Pin: (p) => (
    <svg width="11" height="13" viewBox="0 0 12 14" fill="none" {...p}>
      <path d="M6 13s4.5-4.2 4.5-7.5A4.5 4.5 0 0 0 6 1a4.5 4.5 0 0 0-4.5 4.5C1.5 8.8 6 13 6 13z" stroke="currentColor" strokeWidth="1.3"/>
      <circle cx="6" cy="5.5" r="1.5" stroke="currentColor" strokeWidth="1.3"/>
    </svg>
  ),
  Bed: (p) => (
    <svg width="14" height="11" viewBox="0 0 14 11" fill="none" {...p}>
      <path d="M1 5h12v4M1 9V2M1 5v4M13 9h-12" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round"/>
      <path d="M3 5V3.5A.5.5 0 0 1 3.5 3h3a.5.5 0 0 1 .5.5V5" stroke="currentColor" strokeWidth="1.3"/>
    </svg>
  ),
  Bath: (p) => (
    <svg width="14" height="13" viewBox="0 0 14 13" fill="none" {...p}>
      <path d="M1 7h12v2a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V7zM3 7V2.5A1.5 1.5 0 0 1 4.5 1H6" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round"/>
      <path d="M5.5 3.5h1.5" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round"/>
    </svg>
  ),
  Area: (p) => (
    <svg width="13" height="13" viewBox="0 0 13 13" fill="none" {...p}>
      <rect x="1" y="1" width="11" height="11" stroke="currentColor" strokeWidth="1.3"/>
      <path d="M3 1v2M1 3h2M10 12v-2M12 10h-2" stroke="currentColor" strokeWidth="1.3"/>
    </svg>
  ),
  ArrowDown: (p) => (
    <svg width="10" height="10" viewBox="0 0 10 10" fill="none" {...p}>
      <path d="M5 1.5v6M2.5 5L5 7.5 7.5 5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
    </svg>
  ),
  ArrowUp: (p) => (
    <svg width="10" height="10" viewBox="0 0 10 10" fill="none" {...p}>
      <path d="M5 8.5v-6M2.5 5L5 2.5 7.5 5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
    </svg>
  ),
  ChevronLeft: (p) => (
    <svg width="16" height="16" viewBox="0 0 16 16" fill="none" {...p}>
      <path d="M10 3l-5 5 5 5" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/>
    </svg>
  ),
  ChevronRight: (p) => (
    <svg width="16" height="16" viewBox="0 0 16 16" fill="none" {...p}>
      <path d="M6 3l5 5-5 5" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/>
    </svg>
  ),
  Close: (p) => (
    <svg width="14" height="14" viewBox="0 0 14 14" fill="none" {...p}>
      <path d="M3 3l8 8M11 3l-8 8" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"/>
    </svg>
  ),
  Ext: (p) => (
    <svg width="13" height="13" viewBox="0 0 14 14" fill="none" {...p}>
      <path d="M6 2H2v10h10V8M9 2h3v3M12 2L7 7" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round"/>
    </svg>
  ),
  Play: (p) => (
    <svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor" {...p}>
      <path d="M3 2v8l7-4-7-4z"/>
    </svg>
  ),
  Settings: (p) => (
    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" {...p}>
      <path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"
        stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round"/>
      <circle cx="12" cy="12" r="3" stroke="currentColor" strokeWidth="1.6"/>
    </svg>
  ),
  Refresh: (p) => (
    <svg width="13" height="13" viewBox="0 0 14 14" fill="none" {...p}>
      <path d="M12.5 7A5.5 5.5 0 1 1 7 1.5c1.6 0 3 .7 4 1.8M11 1.5v2.6h-2.6"
        stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round"/>
    </svg>
  ),
  Check: (p) => (
    <svg width="14" height="14" viewBox="0 0 14 14" fill="none" {...p}>
      <path d="M3 7.5l2.5 2.5L11 4.5" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round"/>
    </svg>
  ),
  Sun: (p) => (
    <svg width="16" height="16" viewBox="0 0 16 16" fill="none" {...p}>
      <circle cx="8" cy="8" r="3" stroke="currentColor" strokeWidth="1.5"/>
      <path d="M8 1.5v1.6M8 12.9v1.6M14.5 8h-1.6M3.1 8H1.5M12.6 3.4l-1.1 1.1M4.5 11.5l-1.1 1.1M12.6 12.6l-1.1-1.1M4.5 4.5L3.4 3.4"
        stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
    </svg>
  ),
  Moon: (p) => (
    <svg width="16" height="16" viewBox="0 0 16 16" fill="none" {...p}>
      <path d="M13.5 9.7A6 6 0 1 1 6.3 2.5a5 5 0 0 0 7.2 7.2z" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round"/>
    </svg>
  ),
  Home: (p) => (
    <svg width="16" height="16" viewBox="0 0 16 16" fill="none" {...p}>
      <path d="M2.5 7L8 2.5 13.5 7v6.5a.5.5 0 0 1-.5.5h-3v-4h-4v4H3a.5.5 0 0 1-.5-.5V7z"
        stroke="currentColor" strokeWidth="1.4" strokeLinejoin="round"/>
    </svg>
  ),
};

// --- formatters ---
const fmtPrice = (n) => `AED ${n.toLocaleString("en-US")}`;
const fmtPriceShort = (n) => {
  if (n >= 1000) return `${(n / 1000).toFixed(n % 1000 === 0 ? 0 : 1)}k`;
  return `${n}`;
};
const fmtSize = (n) => `${n.toLocaleString("en-US")} sqft`;
const fmtDate = (s) => {
  if (!s) return "";
  const d = new Date(s);
  return d.toLocaleDateString("en-GB", { day: "numeric", month: "short" });
};
const daysAgo = (s) => {
  if (!s) return 0;
  const ms = Date.now() - new Date(s).getTime();
  return Math.max(0, Math.floor(ms / 86400000));
};
const relDate = (s) => {
  const d = daysAgo(s);
  if (d === 0) return "today";
  if (d === 1) return "yesterday";
  if (d < 7) return `${d}d ago`;
  if (d < 30) return `${Math.floor(d / 7)}w ago`;
  return fmtDate(s);
};

// price_history string: "2026-05-28:175000->160000" -> [{date, from, to}]
const parsePriceHistory = (s) => {
  if (!s) return [];
  return s.split(";").map((entry) => {
    const m = entry.trim().match(/^(\d{4}-\d{2}-\d{2}):(\d+)->(\d+)$/);
    if (!m) return null;
    return { date: m[1], from: parseInt(m[2], 10), to: parseInt(m[3], 10) };
  }).filter(Boolean);
};

// --------------------------------------------------------------
// DATASET PICKER (header dropdown)
// --------------------------------------------------------------
function DatasetPicker({ datasets, activeSlug, onPick }) {
  const [open, setOpen] = useState(false);
  const wrapRef = useRef(null);
  useEffect(() => {
    if (!open) return;
    const onDoc = (e) => { if (!wrapRef.current?.contains(e.target)) setOpen(false); };
    document.addEventListener("mousedown", onDoc);
    return () => document.removeEventListener("mousedown", onDoc);
  }, [open]);

  const active = datasets.find((d) => d.slug === activeSlug) || datasets[0];
  if (!datasets.length) {
    return (
      <div className="dspicker dspicker--empty" title="No scraped datasets yet — run a scrape">
        <span className="dspicker__k">dataset</span>
        <span className="dspicker__none">none</span>
      </div>
    );
  }

  const label = (d) => {
    const cat = (d.category || "").replace("Property for ", "").toLowerCase();
    return `${d.site || "?"} · ${cat || "?"}`;
  };

  return (
    <div className="dspicker" ref={wrapRef}>
      <button className={`dspicker__btn ${open ? "open" : ""}`} onClick={() => setOpen((o) => !o)}>
        <span className="dspicker__k">dataset</span>
        <span className="dspicker__lbl">{label(active)}</span>
        <span className="dspicker__sub tnum">
          ≥{(active.min_size_sqft || 0).toLocaleString()} sqft · ≤{((active.max_price_aed || 0) / 1000).toFixed(0)}k AED
        </span>
        <span className="dspicker__count tnum">{(active.count || 0).toLocaleString()}</span>
        <svg width="10" height="10" viewBox="0 0 10 10" className="dspicker__chev">
          <path d="M2.5 4L5 6.5 7.5 4" stroke="currentColor" strokeWidth="1.4" fill="none" strokeLinecap="round" strokeLinejoin="round"/>
        </svg>
      </button>
      {open && (
        <div className="dspicker__menu">
          {datasets.map((d) => (
            <button key={d.slug}
              className={`dspicker__item ${d.slug === activeSlug ? "on" : ""}`}
              onClick={() => { onPick(d.slug); setOpen(false); }}>
              <div className="dspicker__item-main">
                <div className="dspicker__item-lbl">{label(d)}</div>
                <div className="dspicker__item-sub tnum">
                  ≥{(d.min_size_sqft || 0).toLocaleString()} sqft · ≤{((d.max_price_aed || 0) / 1000).toFixed(0)}k AED
                  · {(d.count || 0).toLocaleString()} listings
                </div>
              </div>
              {d.generated_at && (
                <div className="dspicker__item-when tnum">
                  {new Date(d.generated_at).toLocaleDateString("en-GB", { day: "numeric", month: "short" })}
                </div>
              )}
            </button>
          ))}
          <div className="dspicker__hint">
            Change scrape criteria in <b>Settings</b>, then click <b>Run scrape</b> to add new datasets.
          </div>
        </div>
      )}
    </div>
  );
}

// --------------------------------------------------------------
// HEADER
// --------------------------------------------------------------
function Header({ data, results, search, setSearch, view, setView, sort, setSort,
                  onRun, onSettings, dark, onToggleDark, offline,
                  datasets, activeSlug, onPickDataset, disabled }) {
  const generated_at = data?.generated_at;
  const totalListings = data?.listings?.length || 0;
  const syncTime = useMemo(() => {
    if (!generated_at) return null;
    const d = new Date(generated_at);
    return d.toLocaleString("en-GB", { hour: "2-digit", minute: "2-digit" });
  }, [generated_at]);

  const inputRef = useRef(null);
  useEffect(() => {
    const onKey = (e) => {
      if (e.key === "/" && document.activeElement?.tagName !== "INPUT") {
        e.preventDefault();
        inputRef.current?.focus();
      }
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, []);

  return (
    <header className="appbar">
      <div className="appbar__row1">
        <div className="brand">
          <div className="brandmark"><span/></div>
          <div className="brand__stack">
            <span className="brand__name">Dubizzle Desk</span>
            <span className="brand__sub">Local property tracker</span>
          </div>
        </div>

        <DatasetPicker
          datasets={datasets}
          activeSlug={activeSlug}
          onPick={onPickDataset}
        />

        <div className="meta">
          {data ? (
            <>
              <div className="meta__count tnum">
                {results.toLocaleString()}<em>of {totalListings.toLocaleString()}</em>
              </div>
              <span className="meta__sync">synced {syncTime}</span>
            </>
          ) : (
            <span className="meta__sync" style={{opacity: .65}}>no data yet</span>
          )}
        </div>

        <div className="appbar__actions">
          {offline && <span className="appbar__offline" title="Server not reachable">offline</span>}
          <button className="ibtn" onClick={onToggleDark}
            title={dark ? "Switch to light theme" : "Switch to dark theme"}
            aria-label={dark ? "Switch to light theme" : "Switch to dark theme"}>
            {dark ? <I.Sun/> : <I.Moon/>}
          </button>
          <button className="ibtn" onClick={onSettings} title="Settings — edit scrape criteria"
            aria-label="Settings">
            <I.Settings/>
          </button>
          <button className="runbtn" onClick={onRun}
            title="Re-pull from Dubizzle using your saved scrape criteria (Settings). The view filters on the left don't affect what gets scraped.">
            <I.Play/> Run scrape
          </button>
        </div>
      </div>

      <div className={`appbar__row2 ${disabled ? "is-disabled" : ""}`}>
        <div className="search">
          <I.Search/>
          <input
            ref={inputRef}
            type="text"
            value={search}
            disabled={disabled}
            onChange={(e) => setSearch(e.target.value)}
            placeholder={disabled ? "No dataset loaded" : "Search by title, ref ID, neighbourhood or location…"}
          />
          <kbd>/</kbd>
        </div>

        <select className="sort-select" value={sort} disabled={disabled} onChange={(e) => setSort(e.target.value)}>
          <option value="updated">Recently updated</option>
          <option value="newest">Newest first</option>
          <option value="price_asc">Price: low → high</option>
          <option value="price_desc">Price: high → low</option>
          <option value="size_desc">Size: largest first</option>
          <option value="size_asc">Size: smallest first</option>
        </select>

        <div className="view-toggle">
          <button className={view === "grid" ? "on" : ""} disabled={disabled} onClick={() => setView("grid")}>
            <I.Grid/> Grid
          </button>
          <button className={view === "map" ? "on" : ""} disabled={disabled} onClick={() => setView("map")}>
            <I.Map/> Map
          </button>
        </div>
      </div>
    </header>
  );
}

// --------------------------------------------------------------
// FILTER RAIL
// --------------------------------------------------------------
function DualRange({ min, max, step, valueMin, valueMax, onChange, fmt }) {
  const pctMin = ((valueMin - min) / (max - min)) * 100;
  const pctMax = ((valueMax - min) / (max - min)) * 100;
  return (
    <div>
      <div className="range">
        <div className="range__track"/>
        <div className="range__fill" style={{ left: `${pctMin}%`, right: `${100 - pctMax}%` }}/>
        <input
          type="range" min={min} max={max} step={step}
          value={valueMin}
          onChange={(e) => onChange(Math.min(+e.target.value, valueMax - step), valueMax)}
        />
        <input
          type="range" min={min} max={max} step={step}
          value={valueMax}
          onChange={(e) => onChange(valueMin, Math.max(+e.target.value, valueMin + step))}
        />
      </div>
      <div className="range__bounds tnum">
        <span>{fmt(min)}</span>
        <span>{fmt(max)}</span>
      </div>
    </div>
  );
}

function ChipRow({ items, selected, onToggle, valueMap }) {
  return (
    <div className="chiprow">
      {items.map((it) => (
        <button
          key={it}
          className={selected.has(it) ? "on" : ""}
          onClick={() => onToggle(it)}
        >
          {valueMap ? valueMap(it) : it}
        </button>
      ))}
    </div>
  );
}

function Toggle({ label, value, onChange }) {
  return (
    <div className={`toggle ${value ? "on" : ""}`} onClick={() => onChange(!value)}>
      <span className="toggle__label">{label}</span>
      <div className="toggle__sw"/>
    </div>
  );
}

function Checklist({ items, counts, selected, onToggle }) {
  return (
    <div className="checklist">
      {items.map((it) => (
        <div
          key={it}
          className={`checkitem ${selected.has(it) ? "on" : ""}`}
          onClick={() => onToggle(it)}
        >
          <div className="checkitem__box"/>
          <span>{it}</span>
          <span className="checkitem__count tnum">{counts[it] || 0}</span>
        </div>
      ))}
    </div>
  );
}

function FilterRail({ filters, setFilters, reset, resultCount, listings, disabled }) {
  if (disabled) {
    return (
      <aside className="rail rail--disabled">
        <div className="rail__head">
          <div className="rail__title">View filters</div>
        </div>
        <div className="rail__count" style={{color: "var(--text-3)"}}>
          Filters become active once you load a dataset.
        </div>
      </aside>
    );
  }
  const f = filters;
  const update = (patch) => setFilters({ ...f, ...patch });

  const hoodCounts = useMemo(() => {
    const m = {};
    listings.forEach((l) => { m[l.neighbourhood] = (m[l.neighbourhood] || 0) + 1; });
    return m;
  }, [listings]);
  const typeCounts = useMemo(() => {
    const m = {};
    listings.forEach((l) => { m[l.property_type] = (m[l.property_type] || 0) + 1; });
    return m;
  }, [listings]);
  const hoodList = useMemo(() => Object.keys(hoodCounts).sort(), [hoodCounts]);
  const typeList = useMemo(() => Object.keys(typeCounts).sort(), [typeCounts]);

  const toggleSet = (key, val) => {
    const next = new Set(f[key]);
    if (next.has(val)) next.delete(val); else next.add(val);
    update({ [key]: next });
  };

  return (
    <aside className="rail">
      <div className="rail__head">
        <div className="rail__title">View filters</div>
        <button className="rail__reset" onClick={reset}>Reset all</button>
      </div>
      <div className="rail__count">
        <b style={{color: "var(--text)"}}>{resultCount}</b> of {listings.length} match
      </div>
      <div className="rail__note">
        These narrow what you see locally. To change what gets scraped, edit
        <b style={{color: "var(--text-2)"}}> Settings</b>.
      </div>

      <section className="fs">
        <div className="fs__label">
          Price
          <span className="fs__value tnum">
            {fmtPriceShort(f.priceMin)}–{fmtPriceShort(f.priceMax)} AED
          </span>
        </div>
        <DualRange
          min={0} max={250000} step={5000}
          valueMin={f.priceMin} valueMax={f.priceMax}
          onChange={(a, b) => update({ priceMin: a, priceMax: b })}
          fmt={(v) => `${fmtPriceShort(v)}`}
        />
      </section>

      <section className="fs">
        <div className="fs__label">
          Size (sqft)
          <span className="fs__value tnum">
            {f.sizeMin.toLocaleString()}–{f.sizeMax.toLocaleString()}
          </span>
        </div>
        <DualRange
          min={0} max={5500} step={100}
          valueMin={f.sizeMin} valueMax={f.sizeMax}
          onChange={(a, b) => update({ sizeMin: a, sizeMax: b })}
          fmt={(v) => v.toLocaleString()}
        />
      </section>

      <section className="fs">
        <div className="fs__label">Bedrooms</div>
        <ChipRow
          items={[1, 2, 3, 4, 5, 6]}
          selected={f.beds}
          onToggle={(v) => toggleSet("beds", v)}
          valueMap={(v) => v === 6 ? "6+" : v}
        />
      </section>

      <section className="fs">
        <div className="fs__label">Bathrooms</div>
        <ChipRow
          items={[1, 2, 3, 4, 5, 6]}
          selected={f.baths}
          onToggle={(v) => toggleSet("baths", v)}
          valueMap={(v) => v === 6 ? "6+" : v}
        />
      </section>

      <section className="fs">
        <div className="fs__label">Features</div>
        <Toggle label="Balcony"
          value={f.balcony} onChange={(v) => update({ balcony: v })}/>
        <Toggle label="Water view"
          value={f.waterView} onChange={(v) => update({ waterView: v })}/>
        <Toggle label="Furnished"
          value={f.furnished} onChange={(v) => update({ furnished: v })}/>
        <Toggle label="Hide delisted"
          value={f.hideDelisted} onChange={(v) => update({ hideDelisted: v })}/>
      </section>

      <section className="fs">
        <div className="fs__label">Property type</div>
        <Checklist
          items={typeList}
          counts={typeCounts}
          selected={f.types}
          onToggle={(v) => toggleSet("types", v)}
        />
      </section>

      <section className="fs">
        <div className="fs__label">
          Neighbourhood
          <span className="fs__value tnum">{f.neighbourhoods.size || "any"}</span>
        </div>
        <Checklist
          items={hoodList}
          counts={hoodCounts}
          selected={f.neighbourhoods}
          onToggle={(v) => toggleSet("neighbourhoods", v)}
        />
      </section>
    </aside>
  );
}

// --------------------------------------------------------------
// LISTING CARD
// --------------------------------------------------------------
function badgeKind(amenity) {
  const a = amenity.toLowerCase();
  if (a.includes("sea") || a.includes("water")) return "water";
  if (a.includes("furnished")) return "furnished";
  if (a.includes("private pool") || a.includes("smart") || a.includes("concierge") || a.includes("private lift")) return "premium";
  return "neutral";
}
function pickBadges(listing) {
  const out = [];
  const seen = new Set();
  const pushIf = (text, kind) => {
    if (seen.has(text)) return;
    seen.add(text);
    out.push({ text, kind });
  };
  if (listing.water_view === "Yes") pushIf("Sea view", "water");
  if (listing.balcony === "Yes") pushIf("Balcony", "neutral");
  if (listing.furnished === "Furnished") pushIf("Furnished", "furnished");
  if (listing.furnished === "Semi-Furnished") pushIf("Semi-furnished", "furnished");
  listing.amenities.slice(0, 6).forEach((a) => {
    const k = badgeKind(a);
    if (k === "premium") pushIf(a, "premium");
  });
  return out.slice(0, 4);
}

function PriceDelta({ history }) {
  if (!history || !history.length) return null;
  const last = history[history.length - 1];
  const up = last.to > last.from;
  const pct = Math.round(((last.to - last.from) / last.from) * 100);
  return (
    <span className={`delta delta--${up ? "up" : "down"}`}>
      {up ? <I.ArrowUp/> : <I.ArrowDown/>}
      {Math.abs(pct)}%
      <span className="delta__tip tnum">
        {fmtPriceShort(last.from)} → {fmtPriceShort(last.to)} on {fmtDate(last.date)}
      </span>
    </span>
  );
}

function ListingCard({ listing, onOpen }) {
  const [imgOk, setImgOk] = useState(true);
  const history = useMemo(() => parsePriceHistory(listing.price_history), [listing.price_history]);
  const badges = useMemo(() => pickBadges(listing), [listing]);
  const isDelisted = listing.status === "delisted";

  return (
    <article
      className={`card ${isDelisted ? "is-delisted" : ""}`}
      onClick={() => onOpen(listing)}
      role="button"
      tabIndex={0}
      onKeyDown={(e) => { if (e.key === "Enter") onOpen(listing); }}
    >
      <div className="card__photo">
        {isDelisted && <div className="card__ribbon">Delisted</div>}
        <span className="card__type">{listing.property_type}</span>
        {imgOk ? (
          <img
            src={listing.photos[0]}
            alt={listing.title}
            loading="lazy"
            onError={() => setImgOk(false)}
          />
        ) : (
          <div className="card__photo-fallback">[ photo unavailable ]</div>
        )}
        <span className="card__photo-badge"><I.Camera/> {listing.photos.length}</span>
      </div>

      <div className="card__body">
        <div className="card__pricerow">
          <div className="card__price tnum">
            {fmtPriceShort(listing.price)} <em>AED/yr</em>
          </div>
          <PriceDelta history={history}/>
        </div>

        <div className="card__specs tnum">
          <span><I.Area/> {listing.size_sqft.toLocaleString()} sqft</span>
          <span className="sep"/>
          <span><I.Bed/> {listing.bedrooms}</span>
          <span className="sep"/>
          <span><I.Bath/> {listing.bathrooms}</span>
        </div>

        <div className="card__badges">
          {badges.map((b, i) => (
            <span key={i} className={`tag tag--${b.kind}`}>{b.text}</span>
          ))}
        </div>

        <div className="card__loc">
          <span className="card__loc__hood"><I.Pin/> {listing.neighbourhood}</span>
          <span className="card__loc__date">listed {relDate(listing.first_seen)}</span>
        </div>
      </div>
    </article>
  );
}

// --------------------------------------------------------------
// GRID
// --------------------------------------------------------------
function Grid({ listings, density, onOpen }) {
  if (!listings.length) {
    return (
      <div className="empty">
        <I.Search style={{width: 28, height: 28, color: "var(--text-3)"}}/>
        <h3>No listings match these filters</h3>
        <p>Try widening price, size, or clearing neighbourhoods.</p>
      </div>
    );
  }
  return (
    <div className={`grid grid--${density}`}>
      {listings.map((l) => (
        <ListingCard key={l.id} listing={l} onOpen={onOpen}/>
      ))}
    </div>
  );
}

Object.assign(window, {
  I, Header, FilterRail, Grid, ListingCard,
  fmtPrice, fmtPriceShort, fmtSize, fmtDate, relDate, parsePriceHistory, pickBadges
});
