// screens/platform.jsx — the super admin's Overview.
//
// WHY THIS SCREEN EXISTS
// A super admin used to land on Dashboard, which is scoped to User.CompanyId()
// (DashboardController.cs:32). So the platform operator was shown ONE tenant's SBOM
// findings under the heading "Threat landscape" — their own company's — with nothing
// saying so. At 1,524 tracked that reads like a platform total. It is not.
//
// WHAT THIS SCREEN IS
// The estate, not the vehicles in it. A super admin's job is that every tenant is
// onboarded, connected and being alerted; the CVEs belong to the tenant's own analysts,
// who have the ledger for them. So the questions here are operational:
//   - is any tenant stalled (no admin, nobody signed in, nothing configured)?
//   - is any integration silently broken?
//   - is the product's one promise - "we tell you when a new CVE hits your SBOM" -
//     actually being delivered, and to whom?
//
// WHAT IT DELIBERATELY DOES NOT SHOW
// Per-tenant SBOM/component/finding counts. There is no cross-tenant endpoint: /api/
// dashboard, /api/ledger and even /api/browse ("rollups over the caller's matches
// only", BrowseController.cs:23) are all tenant-scoped. Adding GET /api/admin/estate
// would fill the one gap - see the note the Estate card renders rather than faking a
// number. The alert delivery log is the nearest honest substitute and IS cross-tenant.

// How long an account may sit unverified before it stops being "new" and starts being
// a stalled onboarding. Three days is a working week's grace either side of a weekend.
const PLAT_STALE_DAYS = 3;

function platDays(iso) {
  if (!iso) return null;
  const t = Date.parse(iso);
  if (isNaN(t)) return null;
  return Math.floor((Date.now() - t) / 86400000);
}

function platAgo(iso) {
  const d = platDays(iso);
  if (d === null) return "—";
  if (d === 0) return "today";
  if (d === 1) return "yesterday";
  if (d < 30) return d + "d ago";
  if (d < 365) return Math.round(d / 30) + "mo ago";
  return Math.round(d / 365) + "y ago";
}

// One row of the attention queue. Rank is operational impact, not severity of a CVE:
// 0 blocks the tenant from using the product at all, 1 breaks something silently,
// 2 is a nudge. The list is short on purpose - it is a to-do, not a report.
function platIssues(companies, users, jiraById) {
  const out = [];
  const byCompany = {};
  users.forEach((u) => {
    (byCompany[u.companyId] = byCompany[u.companyId] || []).push(u);
  });

  companies.forEach((c) => {
    const mine = byCompany[c.companyId] || [];
    const admins = mine.filter((u) => isCompanyAdminRole(u.type));
    const unverified = mine.filter((u) => !u.isVerified);
    const stale = unverified.filter((u) => (platDays(u.createdAt) || 0) >= PLAT_STALE_DAYS);

    if (!mine.length) {
      out.push({ rank: 0, company: c, kind: "no accounts",
        detail: "Onboarded, but nobody can sign in yet." });
    } else if (!admins.length) {
      // Not fatal - a super admin can run the tenant - but it means every account
      // change for this customer comes back to us instead of to them.
      out.push({ rank: 1, company: c, kind: "no company admin",
        detail: mine.length + " account" + (mine.length === 1 ? "" : "s") + ", none of them an administrator." });
    }

    if (stale.length) {
      out.push({ rank: 1, company: c, kind: "sign-in never completed",
        detail: stale.length + " account" + (stale.length === 1 ? "" : "s") +
          " created " + platAgo(stale[0].createdAt) + " and still not activated." });
    } else if (unverified.length) {
      out.push({ rank: 2, company: c, kind: "awaiting first sign-in",
        detail: unverified.length + " new account" + (unverified.length === 1 ? "" : "s") + " yet to enter the activation token." });
    }

    const j = jiraById ? jiraById[c.companyId] : undefined;
    if (j) {
      if (j.keyReady === false) {
        // Server-side, not tenant-side: no token can be stored for ANYONE until the
        // app service has Jira__TokenKey. It shows per tenant because that is where
        // the config lives, but one fix clears every row of this kind.
        out.push({ rank: 0, company: c, kind: "Jira key missing on the server",
          detail: j.keyError || "Set Jira__TokenKey on the app service." });
      } else if (j.connected && j.hasToken && !j.tokenReadable) {
        out.push({ rank: 0, company: c, kind: "Jira token unreadable",
          detail: "The stored token will not decrypt - the encryption key was rotated. Issues cannot be filed." });
      } else if (j.connected && j.enabled === false) {
        out.push({ rank: 2, company: c, kind: "Jira switched off",
          detail: "Configured for " + (j.baseUrl || "their site") + ", but nothing is being filed." });
      }
    }
  });

  return out.sort((a, b) =>
    a.rank - b.rank || String(a.company.companyName).localeCompare(String(b.company.companyName)));
}

function PlatformDashboard({ onNav }) {
  // Four calls, one await. Deliveries is allowed to fail on its own: alerting may be
  // switched off entirely, and that must not blank the tenancy half of the page.
  const api = useApi((signal) => Promise.all([
    window.VCApi.adminCompanies(signal),
    window.VCApi.adminUsers("", signal),
    window.VCApi.adminTaxonomy(signal),
    window.VCApi.adminAlertDeliveries({ limit: 100 }, signal).catch(() => []),
  ]).then(([companies, users, tax, deliveries]) => ({
    companies: companies || [], users: users || [],
    tax: tax || {}, deliveries: Array.isArray(deliveries) ? deliveries : [],
  })));

  // Jira config is per tenant, so this is one request per company and cannot be part of
  // the load above without holding the whole page behind fourteen round trips. It fills
  // in progressively instead, and the cards that use it say so while it lands.
  const [jira, setJira] = useState(null);
  useEffect(() => {
    const companies = api.data && api.data.companies;
    if (!companies || !companies.length) { setJira(null); return; }
    let live = true;
    const byId = {};
    let done = 0;
    setJira({ byId: {}, done: 0, total: companies.length });
    companies.forEach((c) => {
      window.VCApi.adminJiraConfig(c.companyId).then(
        (cfg) => { byId[c.companyId] = cfg || null; },
        () => { byId[c.companyId] = null; },
      ).then(() => {
        if (!live) return;
        done += 1;
        setJira({ byId: Object.assign({}, byId), done: done, total: companies.length });
      });
    });
    return () => { live = false; };
  }, [api.data]);

  const [running, setRunning] = useState(false);
  const [runMsg, setRunMsg] = useState(null);

  function runAlerts() {
    setRunning(true); setRunMsg(null);
    window.VCApi.adminRunAlerts().then(
      (r) => {
        setRunning(false);
        setRunMsg({ ok: "Alert pass finished: " + (r.emailsSent || 0) + " e-mail" +
          ((r.emailsSent || 0) === 1 ? "" : "s") + " to " + (r.companiesAlerted || 0) + " of " +
          (r.companiesConsidered || 0) + " tenants, covering " + (r.vulnerabilitiesReported || 0) + " vulnerabilities." });
        api.refetch();
      },
      (e) => { setRunning(false); setRunMsg({ err: (e && e.message) || "The alert pass did not run." }); },
    );
  }

  const d = api.data;
  const stats = useMemo(() => {
    if (!d) return null;
    const companies = d.companies, users = d.users;
    const byType = {};
    companies.forEach((c) => {
      const k = companyTypeLabel(c.companyType);
      byType[k] = (byType[k] || 0) + 1;
    });
    const roles = { "Super admin": 0, "Company admin": 0, "Analyst": 0 };
    users.forEach((u) => { roles[roleLabel(u.type)] = (roles[roleLabel(u.type)] || 0) + 1; });

    const deliveries = d.deliveries;
    const failed = deliveries.filter((x) => String(x.outcome).toLowerCase() !== "sent" &&
                                            String(x.outcome).toLowerCase() !== "ok");
    const lastRun = deliveries.length ? (deliveries[0].createdAt || deliveries[0].syncRunUtc) : null;

    return {
      tenants: companies.length,
      accounts: users.length,
      unverified: users.filter((u) => !u.isVerified).length,
      byType: Object.keys(byType).sort((a, b) => byType[b] - byType[a] || a.localeCompare(b))
        .map((k) => [k, byType[k]]),
      roles: roles,
      // Deleting the last super admin is refused server-side, which means a single
      // super admin is also a single point of recovery for the whole platform.
      soleSuperAdmin: roles["Super admin"] === 1,
      tenantsWithNoAdmin: companies.filter((c) =>
        !users.some((u) => u.companyId === c.companyId && isCompanyAdminRole(u.type))).length,
      deliveries: deliveries,
      failed: failed,
      lastRun: lastRun,
      alertedTenants: new Set(deliveries.map((x) => x.companyId)).size,
      cvesNotified: deliveries.reduce((n, x) => n + (x.vulnCount || 0), 0),
      tax: {
        oems: (d.tax.oems || []).length,
        vendors: (d.tax.vendors || []).length,
        statuses: (d.tax.statuses || []).length,
        bands: (d.tax.severityBands || []).length,
      },
    };
  }, [d]);

  const issues = useMemo(
    () => (d ? platIssues(d.companies, d.users, jira && jira.byId) : []),
    [d, jira]);

  const jiraRoll = useMemo(() => {
    if (!d || !jira) return null;
    let connected = 0, off = 0, never = 0, broken = 0;
    d.companies.forEach((c) => {
      const j = jira.byId[c.companyId];
      if (j === undefined) return;                       // not back yet
      if (!j || !j.connected) { never += 1; return; }
      if (j.keyReady === false || (j.hasToken && !j.tokenReadable)) { broken += 1; return; }
      if (j.enabled === false) { off += 1; return; }
      connected += 1;
    });
    return { connected, off, never, broken, done: jira.done, total: jira.total };
  }, [d, jira]);

  if (api.loading && !d) return <PageLoading label="Reading the estate…" />;
  if (api.error && !d) return <PageError message={api.error} onRetry={api.refetch} />;
  if (!stats) return null;

  const rankCls = ["is-crit", "is-warn", "is-info"];

  return (
    <div className="page">
      <div className="page-head">
        <div>
          <span className="eyebrow">Platform</span>
          <h1 className="page-title">Tenant operations</h1>
          <p className="page-desc">
            Every tenant on this deployment, and whether it is actually working. Vulnerabilities
            belong to each tenant's own analysts — switch to <b>My tenant</b> for your company's
            own SBOM findings.
          </p>
        </div>
        <div className="page-head-aside">
          <button className="btn btn-ghost" onClick={runAlerts} disabled={running}
            title="Run one new-vulnerability alert pass now. The per-tenant watermark still applies, so nobody is re-sent anything.">
            {running ? <span className="btn-spin" /> : <Icon name="bell" size={15} />}
            {running ? "Running…" : "Run alerts now"}
          </button>
        </div>
      </div>

      <div className="admin-span2"><AdminNote err={runMsg && runMsg.err} ok={runMsg && runMsg.ok} /></div>

      {/* The same strip the tenant Overview uses, so the two read as siblings. */}
      <div className="plat-strip">
        <div className="plat-stat"><b>{stats.tenants}</b><span>tenants</span></div>
        <div className="plat-stat"><b>{stats.accounts}</b><span>accounts</span></div>
        <div className="plat-stat">
          <b className={stats.unverified ? "is-warn" : ""}>{stats.unverified}</b>
          <span>awaiting first sign-in</span>
        </div>
        <div className="plat-stat">
          <b className="plat-stat-when">{stats.lastRun ? platAgo(stats.lastRun) : "never"}</b>
          <span>last alert delivered</span>
        </div>
      </div>

      {/* ---- the queue. This is the screen's reason to exist: everything else is
              context for it, so it comes first and takes the width. ---- */}
      <section className="card admin-card plat-queue">
        <div className="card-head card-head-row">
          <h3><Icon name="alert" size={16} /> Needs attention</h3>
          <span className="card-sub">{issues.length ? issues.length + " open" : "nothing outstanding"}</span>
        </div>
        {issues.length === 0 ? (
          <div className="empty">
            <Icon name="check" size={24} />
            <p>Every tenant is set up</p>
            <span className="empty-sub">
              All {stats.tenants} have an administrator, every account has signed in, and no
              integration is reporting a fault.
            </span>
          </div>
        ) : (
          <div className="plat-issues">
            {issues.map((it, i) => (
              <button key={it.company.companyId + "|" + it.kind + "|" + i}
                className={"plat-issue " + rankCls[it.rank]}
                onClick={() => onNav && onNav("admin")}
                title="Open Administration">
                <span className="plat-issue-dot" />
                <span className="plat-issue-co">{it.company.companyName || ("#" + it.company.companyId)}</span>
                <span className="plat-issue-kind">{it.kind}</span>
                <span className="plat-issue-detail">{it.detail}</span>
                <Icon name="chevR" size={14} className="plat-issue-go" />
              </button>
            ))}
          </div>
        )}
        {jira && jira.done < jira.total && (
          <p className="plat-pending">
            <span className="btn-spin" /> Checking integrations — {jira.done} of {jira.total} tenants read.
          </p>
        )}
      </section>

      <div className="plat-grid">
        {/* ---- tenants ---- */}
        <section className="card admin-card">
          <div className="card-head card-head-row">
            <h3><Icon name="layers" size={16} /> Tenants</h3>
            <span className="card-sub">{stats.tenants}</span>
          </div>
          <div className="plat-bars">
            {stats.byType.map(([label, n]) => (
              <div className="plat-bar" key={label}>
                <span className="plat-bar-lab">{label}</span>
                <span className="plat-bar-track">
                  <span className="plat-bar-fill" style={{ width: (n / stats.tenants * 100) + "%" }} />
                </span>
                <span className="plat-bar-num mono">{n}</span>
              </div>
            ))}
          </div>
          {stats.tenantsWithNoAdmin > 0 && (
            <p className="tax-note">
              <Icon name="alert" size={13} />
              {stats.tenantsWithNoAdmin} of {stats.tenants} have no administrator of their own,
              so every account change for them comes back to you.
            </p>
          )}
        </section>

        {/* ---- accounts ---- */}
        <section className="card admin-card">
          <div className="card-head card-head-row">
            <h3><Icon name="user" size={16} /> Accounts</h3>
            <span className="card-sub">{stats.accounts}</span>
          </div>
          <div className="plat-bars">
            {["Super admin", "Company admin", "Analyst"].map((r) => (
              <div className="plat-bar" key={r}>
                <span className="plat-bar-lab">{r}</span>
                <span className="plat-bar-track">
                  <span className="plat-bar-fill"
                    style={{ width: (stats.accounts ? stats.roles[r] / stats.accounts * 100 : 0) + "%" }} />
                </span>
                <span className="plat-bar-num mono">{stats.roles[r]}</span>
              </div>
            ))}
          </div>
          {stats.soleSuperAdmin && (
            <p className="tax-note">
              <Icon name="shield" size={13} />
              One super admin account. The server refuses to delete the last one, so this is
              also the only account that can recover the platform — consider a second.
            </p>
          )}
        </section>

        {/* ---- integrations ---- */}
        <section className="card admin-card">
          <div className="card-head card-head-row">
            <h3><Icon name="link" size={16} /> Jira</h3>
            <span className="card-sub">
              {jiraRoll ? jiraRoll.done + " of " + jiraRoll.total + " read" : "reading…"}
            </span>
          </div>
          {!jiraRoll ? <div className="plat-quiet">Checking each tenant…</div> : (
            <div className="plat-chips">
              <span className="plat-chip is-ok"><b>{jiraRoll.connected}</b> connected</span>
              <span className="plat-chip is-off"><b>{jiraRoll.off}</b> switched off</span>
              <span className="plat-chip"><b>{jiraRoll.never}</b> never configured</span>
              {jiraRoll.broken > 0 &&
                <span className="plat-chip is-bad"><b>{jiraRoll.broken}</b> faulted</span>}
            </div>
          )}
          <p className="plat-quiet">
            Outbound only — VulnerabilityCore files and updates issues; Jira never calls back.
          </p>
        </section>

        {/* ---- alerting ---- */}
        <section className="card admin-card">
          <div className="card-head card-head-row">
            <h3><Icon name="bell" size={16} /> New-CVE alerting</h3>
            <span className="card-sub">last {stats.deliveries.length} deliveries</span>
          </div>
          {stats.deliveries.length === 0 ? (
            <div className="empty">
              <Icon name="bell" size={24} />
              <p>Nothing delivered yet</p>
              <span className="empty-sub">
                Either no tenant has had a new match since alerting was switched on, or it is off.
              </span>
            </div>
          ) : (
            <React.Fragment>
              <div className="plat-chips">
                <span className="plat-chip is-ok"><b>{stats.deliveries.length - stats.failed.length}</b> delivered</span>
                {stats.failed.length > 0 &&
                  <span className="plat-chip is-bad"><b>{stats.failed.length}</b> failed</span>}
                <span className="plat-chip"><b>{stats.alertedTenants}</b> tenants reached</span>
                <span className="plat-chip"><b>{stats.cvesNotified}</b> CVEs notified</span>
              </div>
              {/* The one cross-tenant vulnerability signal that exists today: which
                  customers are getting new findings, and how many. */}
              <div className="admintbl-wrap admintbl-scroll">
                <table className="admintbl">
                  <thead>
                    <tr><th>Tenant</th><th>Recipient</th><th className="admintbl-num">CVEs</th>
                      <th>Result</th><th className="admintbl-num">When</th></tr>
                  </thead>
                  <tbody>
                    {stats.deliveries.slice(0, 40).map((x) => {
                      const ok = ["sent", "ok"].indexOf(String(x.outcome).toLowerCase()) >= 0;
                      return (
                        <tr key={x.logId}>
                          <td><strong>{x.companyName || ("#" + x.companyId)}</strong></td>
                          <td className="admintbl-sub">{x.recipient || "—"}</td>
                          <td className="admintbl-num mono">{x.vulnCount}</td>
                          <td><span className={"jira-log-outcome jira-log-" + (ok ? "ok" : "failed")}>{x.outcome}</span></td>
                          <td className="admintbl-num jira-log-when">{platAgo(x.createdAt || x.syncRunUtc)}</td>
                        </tr>
                      );
                    })}
                  </tbody>
                </table>
              </div>
            </React.Fragment>
          )}
        </section>

        {/* ---- taxonomy ---- */}
        <section className="card admin-card">
          <div className="card-head card-head-row">
            <h3><Icon name="sparkle" size={16} /> Classification</h3>
            <span className="card-sub">applies to every tenant</span>
          </div>
          <div className="plat-counts">
            {[["OEM marques", stats.tax.oems], ["Vendor rules", stats.tax.vendors],
              ["Triage statuses", stats.tax.statuses], ["Severity bands", stats.tax.bands]].map(([l, n]) => (
              <div className="plat-count" key={l}><b className="mono">{n}</b><span>{l}</span></div>
            ))}
          </div>
          <p className="plat-quiet">
            One vocabulary for the whole deployment — an edit re-attributes every tenant's
            catalog on its next load.
          </p>
        </section>

        {/* ---- the honest gap ---- */}
        <section className="card admin-card plat-gap">
          <div className="card-head card-head-row">
            <h3><Icon name="package" size={16} /> The estate</h3>
            <span className="card-sub">not available yet</span>
          </div>
          <p className="plat-quiet">
            SBOMs, components and open findings <b>per tenant</b> are not on this page because no
            endpoint exposes them: <code>/api/dashboard</code>, <code>/api/ledger</code> and
            <code> /api/browse</code> are all scoped to the caller's own company. The delivery log
            above is the nearest cross-tenant signal there is.
          </p>
          <p className="plat-quiet">
            One new read — per-tenant component and finding counts — would complete this card
            rather than have it estimate something it cannot see.
          </p>
        </section>
      </div>
    </div>
  );
}

window.PlatformDashboard = PlatformDashboard;
