// =================================================================
// pw-list.jsx — server-driven lists for the Operator Dashboard.
//
//   useListQuery(name, opts)  → one page of a paged list, plus everything a screen needs to drive it:
//                               debounced search, filters, header sort, pager, reload. State is kept
//                               in the URL hash (#/route?page=2&q=acme&status=new) so a filtered view
//                               survives a reload and can be shared.
//   ListToolbar               → the search box, filter selects and removable filter chips.
//   useSummary(name, watch)   → a KPI summary endpoint (tiles computed over everything, not the page).
//   useRefList(names)         → make sure the picker lists a screen reads (data.publishers, …) are
//                               loaded; they are no longer fetched at startup.
//
// Works identically in live and demo mode: `__api.list` pages the fixtures in paywall-data.js when
// there is no server.
// =================================================================

const LIST_SEARCH_DEBOUNCE_MS = 300;
const LIST_HASH_KEYS = ["page", "page_size", "q", "sort", "dir"];

/** Read this table's state out of the hash. `prefix` keeps two tables on one screen apart. */
const readListHash = (prefix, filterKeys) => {
  const params = (parseHash().params) || {};
  const out = {};
  for (const k of [...LIST_HASH_KEYS, ...filterKeys]) {
    const v = params[prefix + k];
    if (v !== undefined && v !== "") out[k] = v;
  }
  return out;
};

/** Write it back without a history entry — paging is not navigation — and without a hashchange. */
const writeListHash = (prefix, state, defaults) => {
  const h = window.location.hash || "";
  const m = h.match(/^#\/([^?]*)(?:\?(.*))?$/);
  if (!m) return;
  const params = new URLSearchParams(m[2] || "");
  for (const [k, v] of Object.entries(state)) {
    const key = prefix + k;
    if (v === undefined || v === null || v === "" || String(v) === String(defaults[k] ?? "")) params.delete(key);
    else params.set(key, String(v));
  }
  const qs = params.toString();
  const next = `#/${m[1]}${qs ? "?" + qs : ""}`;
  if (next !== h) history.replaceState(null, "", next);
};

// A create page saves and navigates straight back to its list, which reads page 1 on mount — often
// before the create has reached the server. Firing this once the write resolves re-reads every
// mounted list so the new row appears.
const LISTS_STALE_EVENT = "pw:lists-stale";
const pwListsStale = () => { window.dispatchEvent(new Event(LISTS_STALE_EVENT)); };

/**
 * @param name   endpoint name from paywall-api-config.js (must be in pw-api.jsx LIST_SPECS)
 * @param opts   { sort, dir, pageSize, filters: {key: default}, stateKey, watch, enabled }
 *   watch   — any value; when it changes the page is fetched again (pass the store key a write
 *             updates, e.g. data.publishers, so a create/edit shows up without a manual reload)
 */
function useListQuery(name, opts = {}) {
  const filterDefaults = opts.filters || {};
  const filterKeys = Object.keys(filterDefaults);
  const prefix = opts.stateKey ? `${opts.stateKey}_` : "";
  const defaults = { page: 1, page_size: opts.pageSize || 25, q: "", sort: opts.sort || "created_at", dir: opts.dir || "desc", ...filterDefaults };

  const [state, setState] = React.useState(() => ({ ...defaults, ...readListHash(prefix, filterKeys) }));
  const [searchInput, setSearchInput] = React.useState(state.q || "");
  const [result, setResult] = React.useState({ rows: [], total: 0, page: 1, page_size: defaults.page_size, has_more: false, extra: {} });
  const [loading, setLoading] = React.useState(true);
  const [error, setError] = React.useState(null);
  const [tick, setTick] = React.useState(0);
  const seq = React.useRef(0);

  // Debounce: the box updates instantly, the query ~300ms after typing stops, and a new search
  // always starts from page 1 (page 4 of the old results is meaningless for the new ones).
  React.useEffect(() => {
    if ((searchInput || "") === (state.q || "")) return undefined;
    const t = setTimeout(() => setState((s) => ({ ...s, q: searchInput.trim(), page: 1 })), LIST_SEARCH_DEBOUNCE_MS);
    return () => clearTimeout(t);
  }, [searchInput, state.q]);

  // Re-fetch when the data layer switches mode or environment, or a create page's write lands.
  React.useEffect(() => window.__api.on("change", () => setTick((n) => n + 1)), []);
  React.useEffect(() => {
    const onStale = () => setTick((n) => n + 1);
    window.addEventListener(LISTS_STALE_EVENT, onStale);
    return () => window.removeEventListener(LISTS_STALE_EVENT, onStale);
  }, []);

  const queryKey = JSON.stringify(state);
  React.useEffect(() => {
    if (opts.enabled === false) return;
    const mine = ++seq.current;
    setLoading(true);
    writeListHash(prefix, state, defaults);
    window.__api.list(name, state)
      .then((r) => {
        if (mine !== seq.current) return; // a newer request superseded this one
        // Deleted the last row of the last page: step back rather than show an empty page.
        if (r.rows.length === 0 && r.total > 0 && state.page > 1) {
          setState((s) => ({ ...s, page: Math.max(1, Math.ceil(r.total / s.page_size)) }));
          return;
        }
        setResult(r); setError(null); setLoading(false);
      })
      .catch((e) => {
        if (mine !== seq.current) return;
        setError(e); setLoading(false);
      });
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [name, queryKey, tick, opts.watch, opts.enabled]);

  const setFilter = React.useCallback((key, value) => setState((s) => ({ ...s, [key]: value ?? "", page: 1 })), []);
  const clearFilters = React.useCallback(() => {
    setSearchInput("");
    setState((s) => ({ ...s, ...filterDefaults, q: "", page: 1 }));
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [queryKey]);
  const setSort = React.useCallback((sort, dir) => setState((s) => ({ ...s, sort, dir, page: 1 })), []);
  const setPage = React.useCallback((page) => setState((s) => ({ ...s, page })), []);
  const reload = React.useCallback(() => setTick((n) => n + 1), []);

  return {
    rows: result.rows,
    total: result.total,
    hasMore: result.has_more,
    extra: result.extra || {},
    loading, error,
    state, search: searchInput, setSearch: setSearchInput,
    filters: Object.fromEntries(filterKeys.map((k) => [k, state[k]])),
    setFilter, clearFilters, setSort, setPage, reload,
    // Spread straight into <DataTable server={...}/>.
    server: {
      total: result.total,
      page: state.page ? Number(state.page) : 1,
      pageSize: Number(state.page_size) || 25,
      sort: { key: state.sort, dir: state.dir },
      onSort: setSort,
      onPage: setPage,
      loading,
    },
  };
}

/**
 * Search box + filter selects + active-filter chips for a useListQuery list.
 * filters: [{ key, label, options: [{ label, value }] }]
 */
const ListToolbar = ({ list, placeholder, filters = [], actions, children }) => {
  const active = filters.filter((f) => list.filters[f.key]);
  return (
    <div className="pw-listbar">
      <div className="pw-listbar-row">
        <Input prefix={<IconSearch size={14}/>} placeholder={placeholder || "Search"} value={list.search}
               onChange={(e) => list.setSearch(e.target.value)} style={{ minWidth: 220, maxWidth: 320, flex: 1 }}
               aria-label={placeholder || "Search"}/>
        {filters.map((f) => (
          <div key={f.key} style={{ minWidth: 160 }}>
            <Select value={list.filters[f.key] || ""} onChange={(v) => list.setFilter(f.key, v)}
                    options={[{ label: `All ${f.label.toLowerCase()}`, value: "" }, ...f.options]}/>
          </div>
        ))}
        {children}
        {actions && <div className="pw-listbar-actions">{actions}</div>}
      </div>
      {(active.length > 0 || list.state.q) && (
        <div className="pw-listbar-chips">
          {list.state.q && (
            <button type="button" className="pw-fchip on" onClick={() => list.setSearch("")}>Search: “{list.state.q}” ×</button>
          )}
          {active.map((f) => {
            const opt = f.options.find((o) => String(o.value) === String(list.filters[f.key]));
            return (
              <button key={f.key} type="button" className="pw-fchip on" onClick={() => list.setFilter(f.key, "")}>
                {f.label}: {opt ? opt.label : list.filters[f.key]} ×
              </button>
            );
          })}
          <button type="button" className="pw-fchip" onClick={list.clearFilters}>Clear all</button>
          <span className="pw-listbar-count">{fmt.number(list.total)} match{list.total === 1 ? "" : "es"}</span>
        </div>
      )}
      <style>{`
        .pw-listbar { display: flex; flex-direction: column; gap: 8px; }
        .pw-listbar-row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
        .pw-listbar-actions { margin-left: auto; display: flex; gap: 8px; }
        .pw-listbar-chips { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
        .pw-listbar-count { font-size: 12.5px; color: var(--muted); margin-left: 4px; }
        .pw-fchip { display: inline-flex; align-items: center; gap: 5px; background: var(--panel); border: 1px solid var(--line); border-radius: 999px; padding: 4px 11px; font: inherit; font-size: 12px; color: var(--ink-2); cursor: pointer; }
        .pw-fchip:hover { border-color: var(--accent); }
        .pw-fchip.on { background: var(--accent-soft); border-color: var(--accent); color: var(--accent-strong); font-weight: 500; }
        @media (max-width: 640px) { .pw-listbar-row > .hx-input-wrap { flex-basis: 100%; max-width: 100%; } }
      `}</style>
    </div>
  );
};

/** A KPI summary, refetched when `watch` changes (e.g. after a write). Null until it lands. */
function useSummary(name, watch) {
  const [s, setS] = React.useState(null);
  const [tick, setTick] = React.useState(0);
  React.useEffect(() => window.__api.on("change", () => setTick((n) => n + 1)), []);
  React.useEffect(() => {
    let live = true;
    window.__api.summary(name).then((r) => { if (live) setS(r); }).catch(() => { if (live) setS(null); });
    return () => { live = false; };
  }, [name, watch, tick]);
  return s;
}

/**
 * How many rows match `filters` on a paged list — a one-row page, read for its `total`. For a
 * headline count ("Active brands") that has no summary endpoint of its own. Null until it lands.
 */
function useListTotal(name, filters = {}, watch) {
  const [n, setN] = React.useState(null);
  const key = JSON.stringify(filters);
  React.useEffect(() => {
    let live = true;
    window.__api.list(name, { ...filters, page: 1, page_size: 1 })
      .then((r) => { if (live) setN(r.total); })
      .catch(() => { if (live) setN(null); });
    return () => { live = false; };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [name, key, watch]);
  return n;
}

/** Load the picker lists a screen reads from the store. No-op in demo mode (fixtures are there). */
function useRefList(names) {
  const key = [].concat(names).join(",");
  React.useEffect(() => {
    [].concat(names).forEach((n) => window.__api.ensureRef(n));
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [key]);
}

/** Options for a status filter from a fixed list of values. */
const statusOptions = (values) => values.map((v) => ({ label: String(v).replace(/_/g, " ").replace(/^\w/, (c) => c.toUpperCase()), value: v }));

Object.assign(window, { pwListsStale, useListQuery, ListToolbar, useSummary, useListTotal, useRefList, statusOptions });
