// Dubizzle Desk — scraper console
// Adds Run + Settings to the design's viewer App.
// State, filtering, view switching, tweaks; loads listings via /api/listings,
// falls back to window.LISTINGS_DATA (data.mock.js) if the server isn't running.

const { useState: useS, useMemo: useM, useEffect: useE, useCallback: useC, useRef: useR } = React;

const DEFAULT_FILTERS = () => ({
  priceMin: 0,
  priceMax: 250000,
  sizeMin: 0,
  sizeMax: 50000,
  beds: new Set(),
  baths: new Set(),
  balcony: false,
  waterView: false,
  furnished: false,
  hideDelisted: true,
  neighbourhoods: new Set(),
  types: new Set(),
});

const DENSITY = "cozy";  // grid density was previously a tweak; cozy is the default
const THEME_KEY = "dubz-theme";

function useTheme() {
  const [dark, setDark] = useS(() => {
    try {
      const stored = localStorage.getItem(THEME_KEY);
      if (stored === "dark") return true;
      if (stored === "light") return false;
    } catch {}
    return window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
  });
  useE(() => {
    document.documentElement.dataset.theme = dark ? "dark" : "light";
    try { localStorage.setItem(THEME_KEY, dark ? "dark" : "light"); } catch {}
  }, [dark]);
  return [dark, () => setDark((d) => !d)];
}

// ---------- data loading ----------
function useDataHub() {
  // Available datasets on disk + active slug.
  const [datasets, setDatasets] = useS([]);
  const [activeSlug, setActiveSlug] = useS(null);  // null = follow server's active
  const [data, setData] = useS(null);
  const [loading, setLoading] = useS(true);
  const [error, setError] = useS(null);
  const [missing, setMissing] = useS(false);  // server reachable but no dataset yet
  const [missingSlug, setMissingSlug] = useS(null);

  const refreshDatasets = useC(async () => {
    try {
      const r = await fetch("/api/datasets", { cache: "no-store" });
      if (!r.ok) throw new Error(`HTTP ${r.status}`);
      const j = await r.json();
      setDatasets(j.datasets || []);
      if (activeSlug == null) setActiveSlug(j.active_slug || null);
      return j;
    } catch {
      return null;
    }
  }, [activeSlug]);

  const loadSlug = useC(async (slug) => {
    setLoading(true); setError(null); setMissing(false); setMissingSlug(null);
    try {
      const url = slug ? `/api/listings?slug=${encodeURIComponent(slug)}` : "/api/listings";
      const res = await fetch(url, { cache: "no-store" });
      if (res.status === 404) {
        let body = {};
        try { body = await res.json(); } catch {}
        setMissing(true);
        setMissingSlug(body.slug || slug);
        setData(null);
        return;
      }
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      const json = await res.json();
      setData(json);
      if (json.slug) setActiveSlug(json.slug);
    } catch (e) {
      setError(String(e.message || e));
      setData(null);
    } finally {
      setLoading(false);
    }
  }, []);

  // initial + whenever active slug changes
  useE(() => { refreshDatasets(); }, [refreshDatasets]);
  useE(() => { loadSlug(activeSlug); }, [activeSlug, loadSlug]);

  const reload = useC(async () => {
    await refreshDatasets();
    await loadSlug(activeSlug);
  }, [refreshDatasets, loadSlug, activeSlug]);

  return { data, datasets, activeSlug, setActiveSlug,
           loading, error, missing, missingSlug, reload };
}

// ---------- App ----------
function Splash() {
  return (
    <div className="splash">
      <div className="splash__stack">
        <div className="splash__mark">
          <div className="splash__ping"/>
          <div className="splash__core"><span/></div>
        </div>
        <div>
          <div className="splash__name">Dubizzle Desk</div>
          <div className="splash__sub">Starting up…</div>
        </div>
        <div className="splash__bar"><span/></div>
      </div>
    </div>
  );
}

function App() {
  const [dark, toggleDark] = useTheme();
  const [bootDone, setBootDone] = useS(false);
  useE(() => {
    const t = setTimeout(() => setBootDone(true), 900);
    return () => clearTimeout(t);
  }, []);
  const { data, datasets, activeSlug, setActiveSlug,
          loading, error, missing, missingSlug, reload } = useDataHub();

  // state
  const [filters, setFilters] = useS(DEFAULT_FILTERS);
  const [search, setSearch] = useS("");
  const [sort, setSort] = useS("updated");
  const [view, setView] = useS("grid");
  const [selectedId, setSelectedId] = useS(null);
  const [activeMarkerId, setActiveMarkerId] = useS(null);
  const [runOpen, setRunOpen] = useS(false);

  const reset = useC(() => setFilters(DEFAULT_FILTERS()), []);

  // listings ref for the filter pipeline (empty until first load)
  const listings = data?.listings || [];

  // filtering
  const filtered = useM(() => {
    const q = search.trim().toLowerCase();
    return listings
      .filter((l) => {
        if (filters.hideDelisted && l.status === "delisted") return false;
        if (l.price < filters.priceMin || l.price > filters.priceMax) return false;
        if (l.size_sqft < filters.sizeMin || l.size_sqft > filters.sizeMax) return false;
        if (filters.beds.size) {
          const match = [...filters.beds].some((b) =>
            b === 6 ? l.bedrooms >= 6 : l.bedrooms === b);
          if (!match) return false;
        }
        if (filters.baths.size) {
          const match = [...filters.baths].some((b) =>
            b === 6 ? l.bathrooms >= 6 : l.bathrooms === b);
          if (!match) return false;
        }
        if (filters.balcony && l.balcony !== "Yes") return false;
        if (filters.waterView && l.water_view !== "Yes") return false;
        if (filters.furnished && !/Furnished/i.test(l.furnished)) return false;
        if (filters.neighbourhoods.size && !filters.neighbourhoods.has(l.neighbourhood)) return false;
        if (filters.types.size && !filters.types.has(l.property_type)) return false;
        if (q) {
          const blob = `${l.title} ${l.ref_id} ${l.neighbourhood} ${l.location}`.toLowerCase();
          if (!blob.includes(q)) return false;
        }
        return true;
      })
      .sort((a, b) => {
        switch (sort) {
          case "price_asc": return a.price - b.price;
          case "price_desc": return b.price - a.price;
          case "size_asc": return a.size_sqft - b.size_sqft;
          case "size_desc": return b.size_sqft - a.size_sqft;
          case "newest": return new Date(b.first_seen) - new Date(a.first_seen);
          case "updated":
          default: return new Date(b.last_updated) - new Date(a.last_updated);
        }
      });
  }, [listings, filters, search, sort]);

  const selected = useM(
    () => selectedId ? listings.find((l) => l.id === selectedId) : null,
    [selectedId, listings]
  );

  const hasData = !!data;
  const [route, setRoute] = useS("home");   // home | listings | settings
  const [lastStats, setLastStats] = useS(null);
  const [runPhase, setRunPhase] = useS("idle"); // idle | running | success | error

  const openRun = () => setRunOpen(true);
  const openSettings = () => setRoute("settings");

  const onRunDone = (stats) => {
    if (stats) setLastStats(stats);
    setRunPhase(stats ? "success" : "error");
    reload();
  };

  // splash gate must come AFTER every hook call (React #310 otherwise)
  if (!bootDone) return <Splash/>;

  return (
    <div className="shell">
      <NavSidebar
        route={route} setRoute={setRoute}
        dark={dark} onToggleDark={toggleDark}
        runPhase={runPhase}
        listingsBadge={hasData ? filtered.length : null}
        offline={!!error}
      />

      {route === "home" && (
        <HomeScreen
          data={data}
          hasData={hasData}
          loading={loading}
          missing={missing}
          missingSlug={missingSlug}
          error={error}
          datasets={datasets}
          activeSlug={activeSlug || data?.slug}
          lastStats={lastStats}
          onPick={setActiveSlug}
          onRun={openRun}
          onSettings={openSettings}
          onOpenFolder={openFolder}
          onGoListings={() => setRoute("listings")}
        />
      )}

      {route === "listings" && (
        <section className="screen">
          <Header
            data={data}
            results={hasData ? filtered.length : 0}
            search={search} setSearch={setSearch}
            view={view} setView={setView}
            sort={sort} setSort={setSort}
            onRun={openRun}
            onSettings={openSettings}
            dark={dark} onToggleDark={toggleDark}
            offline={!!error}
            datasets={datasets}
            activeSlug={activeSlug || data?.slug}
            onPickDataset={setActiveSlug}
            disabled={!hasData}
          />
          <div className="main">
            <FilterRail
              filters={filters} setFilters={setFilters}
              reset={reset}
              resultCount={hasData ? filtered.length : 0}
              listings={listings}
              disabled={!hasData}
            />
            <main className="content">
              {!hasData ? (
                <NoDataState
                  loading={loading} missing={missing} missingSlug={missingSlug}
                  error={error} datasets={datasets}
                  onRun={openRun} onSettings={openSettings}
                  onPickDataset={setActiveSlug}
                />
              ) : view === "grid" ? (
                <>
                  <div className="results-bar">
                    <div className="results-bar__txt">
                      Showing <b>{filtered.length}</b> {filtered.length === 1 ? "listing" : "listings"}
                      {search && <> matching "<b>{search}</b>"</>}
                      {filters.hideDelisted && <span style={{color: "var(--text-3)"}}> · delisted hidden</span>}
                    </div>
                    <div style={{fontSize: 12, color: "var(--text-3)"}}>
                      Photos and prices reflect last scrape ({new Date(data.generated_at).toLocaleDateString("en-GB", {day:"numeric", month:"short"})}).
                    </div>
                  </div>
                  <Grid
                    listings={filtered}
                    density={DENSITY}
                    onOpen={(l) => setSelectedId(l.id)}
                  />
                </>
              ) : (
                <MapView
                  listings={filtered}
                  onOpen={(l) => setSelectedId(l.id)}
                  activeId={activeMarkerId}
                  setActiveId={setActiveMarkerId}
                />
              )}
            </main>
          </div>
        </section>
      )}

      {route === "settings" && (
        <SettingsScreen onSaved={reload} onRun={openRun}/>
      )}

      {selected && <DetailModal listing={selected} onClose={() => setSelectedId(null)}/>}
      {runOpen && (
        <RunModal
          onClose={() => setRunOpen(false)}
          onStart={() => setRunPhase("running")}
          onDone={(stats) => onRunDone(stats)}
        />
      )}
    </div>
  );
}

async function openFolder() {
  try {
    const r = await fetch("/api/open-folder", { method: "POST" });
    if (!r.ok) throw new Error(`HTTP ${r.status}`);
  } catch (e) {
    alert("Couldn't open folder: " + (e.message || e));
  }
}

// ---- empty state -----------------------------------------------------
function NoDataState({ loading, missing, missingSlug, error, datasets, onRun, onSettings, onPickDataset }) {
  if (loading) {
    return (
      <div className="nodata">
        <div className="boot__ring"/>
        <div className="nodata__title">Loading listings…</div>
      </div>
    );
  }
  if (error) {
    return (
      <div className="nodata">
        <div className="nodata__title">Couldn't reach the local server.</div>
        <div className="nodata__sub">{error}</div>
      </div>
    );
  }
  // missing dataset (404)
  return (
    <div className="nodata">
      <div className="nodata__title">No scrape on disk yet</div>
      <div className="nodata__sub">
        {missingSlug ? <>Nothing saved for <code>{missingSlug}</code>.</> : "Your active criteria has no data yet."}
        {" "}Set what you want to pull in <b>Settings</b>, then click <b>Run scrape</b>.
      </div>
      <div className="nodata__cta">
        <button className="btn btn--ghost" onClick={onSettings}>Open settings</button>
        <button className="btn btn--primary" onClick={onRun}>
          <I.Play/> Run scrape
        </button>
      </div>
      {datasets && datasets.length > 0 && (
        <div className="nodata__alt">
          Or load an existing dataset:
          <div className="nodata__list">
            {datasets.map((d) => (
              <button key={d.slug} className="nodata__chip" onClick={() => onPickDataset(d.slug)}>
                <b>{d.site}</b> · {(d.category || "").replace("Property for ", "").toLowerCase()}
                <span className="nodata__chip-count">{(d.count || 0).toLocaleString()}</span>
              </button>
            ))}
          </div>
        </div>
      )}
    </div>
  );
}

// ---- nav sidebar -----------------------------------------------------
function NavSidebar({ route, setRoute, dark, onToggleDark, runPhase, listingsBadge, offline }) {
  const item = (key, label, icon, badge) => (
    <button key={key}
      className={`nav__item ${route === key ? "on" : ""}`}
      onClick={() => setRoute(key)}>
      <span className="ico">{icon}</span>
      {label}
      {badge != null && <span className="badge tnum">{badge.toLocaleString()}</span>}
    </button>
  );
  const status = runPhase === "running" ? { dot: "running", text: "Running scrape…" }
    : runPhase === "success" ? { dot: "ok", text: "Last run OK" }
    : runPhase === "error" ? { dot: "err", text: "Run failed" }
    : offline ? { dot: "err", text: "Server offline" }
    : { dot: "idle", text: "Ready" };
  return (
    <nav className="nav">
      <div className="nav__brand">
        <div className="brandmark"><span/></div>
        <div className="brand__stack">
          <span className="brand__name display" style={{fontSize: 19, fontWeight: 700, letterSpacing: "-0.02em"}}>
            Dubizzle Desk
          </span>
          <span className="brand__sub">Property tracker</span>
        </div>
      </div>
      <div className="nav__items">
        {item("home", "Home", <I.Home/>)}
        {item("listings", "Listings", <I.Grid/>, listingsBadge)}
        {item("settings", "Settings", <I.Settings/>)}
      </div>
      <div className="nav__spacer"/>
      <div className="nav__status" title={status.text}>
        <span className={`nav__dot nav__dot--${status.dot}`}/>
        <span>{status.text}</span>
      </div>
      <div className="nav__theme">
        <button className="ibtn" onClick={onToggleDark}
          title={dark ? "Switch to light theme" : "Switch to dark theme"}
          aria-label="Toggle theme">
          {dark ? <I.Sun/> : <I.Moon/>}
        </button>
      </div>
    </nav>
  );
}

// ---- Home screen -----------------------------------------------------
function HomeScreen({ data, hasData, loading, missing, missingSlug, error, datasets,
                     activeSlug, lastStats, onPick, onRun, onSettings, onOpenFolder, onGoListings }) {
  const cur = data?.criteria;
  const ds = (slug) => datasets.find((d) => d.slug === slug);
  const active = ds(activeSlug);
  return (
    <section className="screen">
      <div className="screen__head">
        <div>
          <div className="screen__title">Home</div>
          <div className="screen__sub">
            {hasData ? <>Tracking <b>{(data.listings || []).length.toLocaleString()}</b> listings · last scraped <b>{new Date(data.generated_at).toLocaleString("en-GB", { day:"numeric", month:"short", hour:"2-digit", minute:"2-digit" })}</b></>
                     : "No dataset loaded yet — pick one below or run a scrape."}
          </div>
        </div>
        <div style={{display: "flex", gap: 10}}>
          <button className="btn btn--ghost" onClick={onOpenFolder}>Show files</button>
          <button className="btn btn--ghost" onClick={onSettings}>Settings</button>
          <button className="runbtn" onClick={onRun}><I.Play/> Run scrape</button>
        </div>
      </div>

      <div className="home">
        <div className="home__hero">
          <div className="home__card">
            <h3>Last run</h3>
            {lastStats ? (
              <div className="home__stats">
                <Stat k="Fetched" v={lastStats.fetched}/>
                <Stat k="New" v={lastStats.new}/>
                <Stat k="Price changes" v={lastStats.price_changes}/>
                <Stat k="Delisted" v={lastStats.delisted}/>
              </div>
            ) : hasData ? (
              <div className="home__stats">
                <Stat k="Total tracked" v={(data.listings || []).length}/>
                <Stat k="Water view"
                  v={(data.listings || []).filter((l) => l.water_view === "Yes").length}/>
                <Stat k="With balcony"
                  v={(data.listings || []).filter((l) => l.balcony === "Yes").length}/>
                <Stat k="Delisted"
                  v={(data.listings || []).filter((l) => l.status === "delisted").length}/>
              </div>
            ) : (
              <div className="home__sub" style={{color: "var(--text-3)", marginTop: 8}}>
                Run a scrape to populate stats.
              </div>
            )}
            <div className="home__row">
              <button className="btn btn--primary" onClick={onGoListings} disabled={!hasData}>
                Browse listings
              </button>
              <button className="btn btn--ghost" onClick={onOpenFolder}>Open data folder</button>
            </div>
          </div>
          <div className="home__card">
            <h3>Current criteria</h3>
            {cur ? (
              <div className="home__criteria">
                <div><div className="k">Emirate</div><div className="v">{cur.site}</div></div>
                <div><div className="k">Category</div><div className="v">{cur.category}</div></div>
                <div><div className="k">Min size</div><div className="v">{(cur.min_size_sqft || 0).toLocaleString()} sqft</div></div>
                <div><div className="k">Max price</div><div className="v">{(cur.max_price_aed || 0).toLocaleString()} AED</div></div>
              </div>
            ) : (
              <div style={{color: "var(--text-3)", fontSize: 13, marginTop: 8}}>
                No criteria set yet — open Settings.
              </div>
            )}
            <div className="home__row">
              <button className="btn btn--ghost" onClick={onSettings}>Edit criteria</button>
            </div>
          </div>
        </div>

        <div className="home__card" style={{marginTop: 18}}>
          <h3>Datasets on disk</h3>
          {datasets.length ? (
            <div className="home__dslist">
              {datasets.map((d) => (
                <button key={d.slug}
                  className={`home__dsitem ${d.slug === activeSlug ? "on" : ""}`}
                  onClick={() => { onPick(d.slug); onGoListings(); }}>
                  <div>
                    <div className="lbl">{d.site} · {(d.category || "").replace("Property for ", "").toLowerCase()}</div>
                    <div className="sub tnum">
                      ≥{(d.min_size_sqft || 0).toLocaleString()} sqft · ≤{((d.max_price_aed || 0) / 1000).toFixed(0)}k AED
                      {d.generated_at && <> · scraped {new Date(d.generated_at).toLocaleDateString("en-GB", { day:"numeric", month:"short" })}</>}
                    </div>
                  </div>
                  <span className="cnt tnum">{(d.count || 0).toLocaleString()}</span>
                </button>
              ))}
            </div>
          ) : (
            <div style={{color: "var(--text-3)", fontSize: 13, marginTop: 8}}>
              {loading ? "Loading…" : "Nothing on disk yet. Click Run scrape to fetch your first dataset."}
            </div>
          )}
        </div>
      </div>
    </section>
  );
}

function Stat({ k, v }) {
  return (
    <div className="home__stat">
      <div className="k">{k}</div>
      <div className="v tnum">{(v ?? 0).toLocaleString()}</div>
    </div>
  );
}

// ---- Settings screen (full page; replaces the modal) ----------------
function SettingsScreen({ onSaved, onRun }) {
  const flushRef = useR(null);
  const runWithFlush = async () => {
    // make absolutely sure pending changes are saved to disk before the
    // scraper reads config.yaml — otherwise the scrape uses stale criteria.
    if (flushRef.current) {
      try { await flushRef.current(); } catch {}
    }
    onRun();
  };
  return (
    <section className="screen">
      <div className="screen__head">
        <div>
          <div className="screen__title">Settings</div>
          <div className="screen__sub">
            Edits auto-save to <code>config.yaml</code>. Click <b>Run scrape</b> to apply.
          </div>
        </div>
        <button className="runbtn" onClick={runWithFlush}><I.Play/> Run scrape</button>
      </div>
      <div className="home">
        <div className="home__card" style={{padding: 0, marginTop: 16}}>
          <InlineSettings onSaved={onSaved} flushRef={flushRef}/>
        </div>
      </div>
    </section>
  );
}

const root = ReactDOM.createRoot(document.getElementById("app-root"));
root.render(<App/>);
