// NO PROD JSON API (bucket B — feed is a DataTables render payload or
//   {"html":...}, not data) — mock until backend (see PROD_API_INVENTORY.md)
// Represents: GET /reports/network_liabilities/ · ReportsController::networkLiabilities — see docs/ISYSTEM_REFERENCE.md §Batch 9.2
/* Report ▾ → Network liabilities.
   NEW SCREEN, and the most consequential of the four Batch 9 gaps: it is the
   ONLY missing screen the real sidebar actually renders. sidebar.blade.php:262-265,
   under Report, gated by `support_report_network_liabilities`. Every other
   missing screen is URL-only or behind a commented-out entry.

   It stayed invisible because the coverage check ran at controller granularity:
   ReportsController was "partial → report-*", and its 14 live report actions
   were never enumerated. It serves 20 view actions in total.

   Real surface:
     · GET  /reports/network_liabilities/                    ReportsController::networkLiabilities
     · GET  /reports/network_liabilities/getNetworkLiabilitiesReport   ::getNetworkLiabilitiesReport  (named .level)
     · ANY  /reports/network_liabilities/excel               ::excelExportNetworkLiabilities (named .export)
     Blades: admin/reports/network_liabilities.blade.php (page)
             admin/reports/network_liabilities/network_liabilities.blade.php (rows, returned as {html})

   PERMISSION IS CHECKED TWICE AND DIFFERENTLY — surfaced in the gate note
   because it is a real trap:
     page + data : isCustomCare() && (!support_report || !support_report_network_liabilities) -> 403
     data ONLY   : additionally !support_report_daily_report -> authorize('view', User::find($user_id))
   So a Custom Care operator can hold `support_report_network_liabilities`,
   load the page, and be refused every single expansion. Modelled below as a
   toggle, because it is otherwise impossible to see.

   NOT A FLAT TABLE. Each row whose count_users > 0 renders its username as a
   link calling elenco_utenti_sottostanti(id, 0), which fetches that user's
   children into a hidden <tr> underneath. The real page nests a whole table
   per expansion, one totals row per depth. This renders the same tree as a
   flattened, indented row list on the shared HrsTable — same data, same
   totals per depth, one table implementation instead of a 24th hand-rolled one.

   THE DATE FILTER CHANGES THE SOURCE TABLE, it does not merely filter:
     no date -> users
     date    -> user_balance_histories JOIN users, via getBalancesAtDate()
   so the report is point-in-time. A user with no history row on that date
   reads 0, not their current balance. Called out on screen — reading it as a
   filter is how someone concludes a network "lost" its money overnight.

   Aggregate scope, verbatim from getUserAgentsReport():
     WHERE (user_path LIKE '<path>' OR user_path LIKE '<path>/%')
       AND user_path <> '<path>'
     [AND users.test_user = 0]  unless include_test_users
   A node's own row is excluded from its own subtree sums. Every value is
   number_format(v, 2, '.', '') — dot decimal, NO thousands separator. Kept.

   Deliberately NOT added (the real screen has none): sorting (no ORDER BY
   anywhere in the endpoint — rows arrive in the order getChildUsers returns
   them), pagination, KPI cards, charts, a date RANGE (it is a single date),
   or per-row actions. */

const HNL_LEVELS = [
  { id: 0,  key: "admin",     label: "Admin",       initial: "A" },
  { id: 2,  key: "admin",     label: "Skin admin",  initial: "S" },
  { id: 8,  key: "master",    label: "Master",      initial: "M" },
  { id: 10, key: "agent",     label: "Agent",       initial: "A" },
  { id: 15, key: "promoter",  label: "Promoter",    initial: "P" },
  { id: 20, key: "shop",      label: "Shop",        initial: "S" },
  { id: 30, key: "player",    label: "Player",      initial: "P" },
];
const hnlLevel = (id) => HNL_LEVELS.find(l => l.id === id) || HNL_LEVELS[0];

const HNL_SKINS = [
  { id: 0, name: "All skins" },
  { id: 7, name: "Iwakiri BR" },
  { id: 8, name: "Iwakiri AR" },
  { id: 9, name: "Iwakiri CL" },
];

/* number_format($v, 2, '.', '') — dot decimal, no thousands separator. The
   real screen looks like this and the difference matters when someone pastes
   a column into a spreadsheet. */
const hnlNum = (v) => (Math.round((Number(v) || 0) * 100) / 100).toFixed(2);

/* THE INVENTED NETWORK IS GONE. `hnlBuildTree` fabricated the whole hierarchy —
   usernames, brands, a fan-out per level — and gave every node an own_balance,
   own_credits and a bag of player balances from a seeded PRNG. This is a
   LIABILITY report: the figure it prints is what the platform owes its network.

   `report_network_liability` (055) is the real aggregate, and it exists because
   the split is the whole point:

     own      — what this operator holds
     subnet   — non-player descendants: float still inside the network
     players  — player descendants: money owed to the public

   Those are different liabilities. An agent holding 10,000 is float; 10,000 in
   players' withdrawable balances is a debt that can be called tomorrow.
   `user_subnet_balance` collapses all three into one number, which is why this
   report needed its own view rather than that one.

   The view is security_invoker, so every figure is already scoped to the
   subtree the caller can see — the sums are not computed in the browser, which
   would mean fetching every descendant's wallet to the client.

   THE TREE IS REBUILT FROM `path`. Each row carries its ltree path, so parent
   and child are derived from containment rather than from a shape the client
   guessed — and a node whose parent is outside the caller's subtree simply has
   no parent row here, which is the honest rendering of a partial view. */

/* One row per operator; children are the rows whose path is this path plus one
   more label. Depth comes from the path, not from a counter. */
const hnlPathDepth = (p) => String(p || "").split(".").length;
const hnlIsChildOf = (child, parent) =>
  String(child || "").indexOf(String(parent || "") + ".") === 0 &&
  hnlPathDepth(child) === hnlPathDepth(parent) + 1;

/* The report's own column names, mapped onto the view's. Kept as a table so the
   export, the totals and the cells cannot drift apart. */
const HNL_FROM_VIEW = (r) => {
  const own = Number(r.own_balance) || 0;
  const ownCred = Number(r.own_credits) || 0;
  const subBal = Number(r.subnet_balance) || 0;
  const subCred = Number(r.subnet_credits) || 0;
  const plBal = Number(r.players_balance) || 0;
  const plWd = Number(r.players_withdrawable) || 0;
  const plBonus = Number(r.players_bonus) || 0;
  return {
    balance: own,
    credits: ownCred,
    balance_subagents: subBal,
    credits_subagents: subCred,
    /* players_balance from the view is ALREADY balance + balance_withdrawable —
       001 makes that sum a generated column so the two halves are not added by
       hand in five places and mixed up in one. The old code added `wd` a second
       time here, double-counting the withdrawable half into the total. */
    balance_players: plBal,
    withdrawable_players: plWd,
    bonus_players: plBonus,
    total_balance: own + subBal + plBal,
    total_credits: ownCred + subCred,
  };
};

const HNL_COLS = [
  { key: "lv",        label: "",                     width: 34 },
  { key: "id",        label: "ID",                   width: 62 },
  { key: "username",  label: "Username",             width: "16%" },
  { key: "total_balance",        label: "Total balance",        align: "right" },
  { key: "total_credits",        label: "Total credits",        align: "right" },
  { key: "balance",              label: "User balance",         align: "right" },
  { key: "credits",              label: "User credits",         align: "right" },
  { key: "balance_subagents",    label: "Subnet balance",       align: "right" },
  { key: "credits_subagents",    label: "Subnet credits",       align: "right" },
  { key: "balance_players",      label: "Players balance",      align: "right" },
  { key: "withdrawable_players", label: "Players withdrawable", align: "right" },
  { key: "bonus_players",        label: "Players total bonus",  align: "right" },
];
const HNL_MONEY_KEYS = HNL_COLS.filter(c => c.align === "right").map(c => c.key);
const HNL_EXPORT_HEADERS = [
  { key: "level", label: "Level" }, { key: "id", label: "ID" },
  { key: "username", label: "Username" }, { key: "parent", label: "Parent" },
  ...HNL_COLS.filter(c => c.align === "right").map(c => ({ key: c.key, label: c.label })),
];

const hnlToast = (t, d) => window.hrsToast && window.hrsToast(t, d);

const HostReportNetworkLiab = () => {
  const HNL_BLANK = { skin_id: "0", user_type: "", user_id: "", filter_user_id: "", date: "", include_test_users: false };
  const [filters, setFilters] = React.useState(HNL_BLANK);
  const [applied, setApplied] = React.useState(HNL_BLANK);
  /* The report IS the feed. Filters that the view can answer are sent to the
     server; `include_test_users` and the level filter are applied to what comes
     back, because both narrow a set the caller can already see. */
  const feed = useHrsFetch(() => {
    const f = {};
    if (applied.skin_id && applied.skin_id !== "0") f.skin = applied.skin_id;
    if (applied.user_id) f.subtree = applied.user_id;
    if (applied.filter_user_id) f.user = applied.filter_user_id;
    return window.sb.list("reportNetworkLiability", { limit: 3000, filters: f });
  }, [applied.skin_id, applied.user_id, applied.filter_user_id]);
  const skinFeed = useHrsFetch(() => window.sb.list("skins", { limit: 200 }), []);
  const opFeed = useHrsFetch(() => window.sb.list("networkUsers", { limit: 2000 }), []);
  const [expanded, setExpanded] = React.useState({});
  const [settingsOpen, setSettingsOpen] = React.useState(false);
  const [hidden, setHidden] = usePbStored("iwk-hnl-hidden-cols", []);
  // The permission trap, made visible. Off = the operator holds
  // support_report_daily_report, which is what an admin has.
  const [ccNoDaily, setCcNoDaily] = React.useState(false);

  const matches = (n) => {
    if (applied.skin_id !== "0" && String(n.skin_id) !== applied.skin_id) return false;
    if (applied.user_type && String(n.user_level) !== applied.user_type) return false;
    if (applied.filter_user_id && String(n.id) !== applied.filter_user_id) return false;
    if (!applied.include_test_users && n.test_user) return false;
    return true;
  };

  /* The rows the view returned, indexed by path so parent/child is derived from
     ltree containment rather than from a shape the client guessed. */
  const nodes = React.useMemo(
    () => (feed.data || []).map(r => Object.assign({}, r, {
      id: Number(r.user_id), user_level: Number(r.user_level),
      parent_username: r.parent_username || "—",
      rep: HNL_FROM_VIEW(r),
    })), [feed.data]);

  const kidsOf = React.useMemo(() => {
    const m = {};
    nodes.forEach(n => {
      const parent = nodes.find(x => hnlIsChildOf(n.path, x.path));
      const key = parent ? parent.path : "__root";
      (m[key] = m[key] || []).push(n);
    });
    Object.keys(m).forEach(k => m[k].sort((a, z) => a.username.localeCompare(z.username)));
    return m;
  }, [nodes]);

  /* Flatten to the rows currently visible: the top level, plus the children of
     anything expanded. One totals row per expanded depth, mirroring the real
     page's per-level totals.

     THE TOP LEVEL IS WHATEVER HAS NO VISIBLE PARENT. A node whose parent sits
     outside the caller's subtree has no parent row here, and that is the honest
     rendering of a partial view — not an error and not a hidden row. */
  const rows = React.useMemo(() => {
    const out = [];
    const push = (parentPath, depth, label) => {
      const kids = (kidsOf[parentPath] || []).filter(matches);
      const t = { __total: true, __depth: depth, id: `t-${parentPath}`, username: `Totals · ${label}` };
      HNL_MONEY_KEYS.forEach(k => { t[k] = 0; });
      kids.forEach(n => {
        out.push(Object.assign({}, n, { __depth: depth, __kids: (kidsOf[n.path] || []).length }));
        HNL_MONEY_KEYS.forEach(k => { t[k] += n.rep[k]; });
        if (expanded[n.id] && (kidsOf[n.path] || []).length) push(n.path, depth + 1, n.username);
      });
      if (kids.length) out.push(t);
    };
    push("__root", 0, "network");
    return out;
  }, [nodes, kidsOf, applied, expanded, hidden]);

  const cols = HNL_COLS.filter(c => !hidden.includes(c.key)).map(c => ({
    ...c,
    cellClass: (r) => r.__total ? "hnl-total" : "",
    render: (r) => {
      if (c.key === "lv") {
        if (r.__total) return null;
        const lv = hnlLevel(r.user_level);
        return <span className={`hnl-badge hnl-badge--${lv.key}`} title={lv.label}>{lv.initial}</span>;
      }
      if (c.key === "id") return r.__total ? null : r.id;
      if (c.key === "username") {
        const pad = { paddingLeft: r.__depth * 16 };
        if (r.__total) return <span className="hnl-totlabel" style={pad}>{r.username}</span>;
        const canExpand = r.__kids > 0;
        if (!canExpand) return <span className="hnl-user" style={pad} title={`Parent: ${r.parent_username}`}>{r.username}</span>;
        if (ccNoDaily) {
          return (
            <span style={pad}>
              <NoBackend
                need="support_report_daily_report"
                what={`Expand ${r.username}`}
                className="hnl-expand hnl-expand--denied">
                {r.username} <Icon name="chevron_right" size={11}/>
              </NoBackend>
            </span>
          );
        }
        return (
          <button className={`hnl-expand${expanded[r.id] ? " open" : ""}`} style={pad}
            title={`${expanded[r.id] ? "Collapse" : "Expand"} — ${r.__kids} user(s) below`}
            onClick={() => setExpanded(m => ({ ...m, [r.id]: !m[r.id] }))}>
            {r.username} <Icon name={expanded[r.id] ? "chevron_down" : "chevron_right"} size={11}/>
          </button>
        );
      }
      const v = r.__total ? r[c.key] : r.rep[c.key];
      return <span className="hnl-money">{hnlNum(v)}</span>;
    },
  }));

  const grand = React.useMemo(() => {
    const t = {};
    HNL_MONEY_KEYS.forEach(k => { t[k] = 0; });
    rows.filter(r => !r.__total && r.__depth === 0).forEach(r => {
      HNL_MONEY_KEYS.forEach(k => { t[k] += r.rep[k]; });
    });
    const out = { lv: "", id: "", username: "Total" };
    HNL_MONEY_KEYS.forEach(k => { out[k] = <span className="hnl-money">{hnlNum(t[k])}</span>; });
    return out;
  }, [rows]);

  const exportRows = rows.filter(r => !r.__total).map(r => {
    const o = { level: hnlLevel(r.user_level).label, id: r.id, username: r.username, parent: r.parent_username };
    HNL_MONEY_KEYS.forEach(k => { o[k] = hnlNum(r.rep[k]); });
    return o;
  });

  const reset = () => {
    setFilters(HNL_BLANK); setApplied(HNL_BLANK); setExpanded({});
  };

  const allUsers = React.useMemo(
    () => (opFeed.data || []).map(u => ({ id: Number(u.id), username: u.username, user_level: Number(u.user_level) })),
    [opFeed.data]);

  return (
    <HrsShell
      title="Network liabilities"
      subtitle="Money held across the whole user tree — each node's own balance, its subnetwork's, and its players'."
      gate={["auth", "admin", "2fa", "g2fa"]}
      gateNote={<> Custom Care additionally needs <code>support_report</code> <b>and</b> <code>support_report_network_liabilities</code> for the page, and <code>support_report_daily_report</code> on top of those before any row will expand — so an operator can legitimately hold the network-liabilities permission, load this page, and be refused every drill-down. Toggle it below to see that state.</>}
      explainer={{
        title: "What this report shows, in plain English",
        bullets: [
          "One row per user under the selected root. Click a username to pull its children in beneath it.",
          "A node's own row is EXCLUDED from its own subnet sums — 'Subnet balance' is what is below it, not including it.",
          "Players are never listed as rows. They are aggregated into the last three columns.",
          "Setting a date does not filter — it switches the source to user_balance_histories, so the whole report becomes point-in-time. A user with no history row that day reads 0.00, not their current balance.",
        ],
      }}
      actions={<HrsExport count={exportRows.length} filename="network-liabilities.csv"
        onCsv={() => hrsCsv(exportRows, HNL_EXPORT_HEADERS, "network-liabilities.csv")}
        note="The real button posts to ANY /reports/network_liabilities/excel and returns a spreadsheet; this downloads the same rows as CSV." />}
    >
      <HrsFilters
        fields={[
          { key: "skin_id", label: "Skin", type: "select",
            options: [{ value: "0", label: "All brands" }].concat(
              (skinFeed.data || []).map(s => ({ value: String(s.id), label: s.name }))) },
          { key: "user_type", label: "User type", type: "select",
            options: [{ value: "", label: "All types" },
              ...HNL_LEVELS.filter(l => l.id > 0 && l.id < 30).map(l => ({ value: String(l.id), label: l.label }))] },
          /* The root is any operator in the caller's subtree — "you" was the
             only option because the tree had one invented root. Empty means the
             whole visible network. */
          { key: "user_id", label: "Root user", type: "select",
            options: [{ value: "", label: "Whole visible network" }].concat(
              allUsers.map(u => ({ value: String(u.id), label: `${u.username} · ${hnlLevel(u.user_level).label}` }))) },
          { key: "filter_user_id", label: "Filter to one user", type: "select",
            options: [{ value: "", label: "Everyone" },
              ...allUsers.map(u => ({ value: String(u.id), label: `${u.username} · ${hnlLevel(u.user_level).label}` }))] },
          /* NO SOURCE FOR THIS. Upstream it switches the report to
             `user_balance_histories`, a point-in-time snapshot of every wallet;
             no such table exists here. The old screen answered it with a
             deterministic scale factor per node — a different number, which is
             exactly what makes a point-in-time reading look like it worked.
             Disabled with the missing table named. */
          { key: "date", label: "Balance at date", type: "date", disabled: true,
            note: "No source in this build: upstream reads user_balance_histories, a per-wallet point-in-time snapshot, which this schema does not have. Reconstructing it from ledger_entries.balance_after is possible but is a different query." },
          { key: "include_test_users", label: "Include test users", type: "toggle" },
        ]}
        values={filters}
        onChange={(k, v) => setFilters(f => ({ ...f, [k]: v }))}
        onSearch={() => { setApplied(filters); setExpanded({}); }}
        onReset={reset}
        resultLabel={`${rows.filter(r => !r.__total).length} user(s)`}
      >
        <button className="hrs-btn" onClick={() => setSettingsOpen(true)}>
          <Icon name="sliders" size={13}/> Settings
        </button>
      </HrsFilters>

      {applied.date && (
        <div className="hnl-datenote">
          <Icon name="alert" size={13}/>
          <span>
            Reading <b>user_balance_histories</b> at <b>{applied.date}</b>, not <code>users</code>.
            This is the network as it stood that day. Anyone with no balance-history row on that date shows <code>0.00</code>.
          </span>
        </div>
      )}

      <div className="hnl-ccrow">
        <label className="hnl-cc">
          <input type="checkbox" checked={ccNoDaily} onChange={(e) => { setCcNoDaily(e.target.checked); setExpanded({}); }}/>
          <span>Simulate a Custom Care operator without <code>support_report_daily_report</code></span>
        </label>
        <span className="hnl-cchint">
          The page loads, every expansion is refused. The real endpoint answers 403 with no explanation.
        </span>
      </div>

      <HrsSection
        title="Network"
        sub={`${applied.user_id ? "Rooted at the selected operator" : "Whole visible network"}${applied.include_test_users ? " · test users included" : " · test users excluded"}`}
      >
        {feed.loading && <HrsSkeleton rows={8} cols={cols.length} />}
        {!feed.loading && feed.error && <HrsError error={feed.error} onRetry={feed.retry} />}
        {!feed.loading && !feed.error && (
        <HrsTable
          columns={cols}
          rows={rows}
          rowKey={(r) => r.__total ? r.id : `u-${r.id}`}
          totals={grand}
          empty="No users match these filters."
        />
        )}
      </HrsSection>

      {settingsOpen && (
        <div className="bp-modal-scrim" onClick={() => setSettingsOpen(false)}>
          <div className="cg-modal hnl-settings" onClick={(e) => e.stopPropagation()}>
            <div className="cg-modal-title">Column settings</div>
            <div className="cg-modal-body">
              <p className="hma-hint" style={{ marginTop: 0 }}>
                The real Settings modal writes to the same per-user table-settings store the Players
                report uses (<code>ReportsController::networkLiabilitiesSettings</code> renders
                <code> admin.players.forms.tableSetting</code>). Persisted here too.
              </p>
              {HNL_COLS.filter(c => c.key !== "username").map(c => (
                <label key={c.key} className="hnl-colchk">
                  <input type="checkbox" checked={!hidden.includes(c.key)}
                    onChange={() => setHidden(h => h.includes(c.key) ? h.filter(x => x !== c.key) : [...h, c.key])}/>
                  <span>{c.label || "Level badge"}</span>
                </label>
              ))}
            </div>
            <div className="cg-modal-foot">
              <button className="hrs-btn" onClick={() => setHidden([])}>Show all</button>
              <button className="hrs-btn hrs-btn--filters" onClick={() => { setSettingsOpen(false); hnlToast("Column settings saved", `${HNL_COLS.length - hidden.length} of ${HNL_COLS.length} columns visible.`); }}>Done</button>
            </div>
          </div>
        </div>
      )}
    </HrsShell>
  );
};

window.HostReportNetworkLiab = HostReportNetworkLiab;
