// On-chain metrics page blocks, formatters + stat blocks + SVG charts +
// the live posture lookup. Used by metrics.jsx only; the homepage teaser in
// Sections.jsx is self-contained so index.html doesn't need this file.
//
// Data contract (spec §8): the page renders from two published JSON files,
// data/metrics-latest.json and data/metrics-history.json. While the daily
// crawler isn't live, the fixture carries source: "MOCK …" and the page
// shows an explicit sample-data notice; the notice disappears on its own
// once the pipeline publishes real output without the marker.

// Metrics accent: same amber as the rest of the site.
const MX_ACCENT = SP_COL.accent;

const MX_FMT = {
  int: (v) => v == null ? '·' : Math.round(v).toLocaleString('en-US'),
  pct: (v) => v == null ? '·' : v.toFixed(2) + '%',
  pct1: (v) => v == null ? '·' : v.toFixed(1) + '%',
  xrp: (v) => v == null ? '·' : (v / 1e9).toFixed(2) + 'B XRP',
  compact: (v) => {
    if (v == null) return '·';
    if (v >= 1e6) return (v / 1e6).toFixed(2) + 'M';
    if (v >= 1e3) return (v / 1e3).toFixed(1) + 'K';
    return Math.round(v).toLocaleString('en-US');
  },
};

// Headline stat, big mono number, eyebrow label, optional footnote.
function StatBlock({ label, value, note, accent = false, big = false }) {
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 8, minWidth: 0 }}>
      <div style={{
        fontFamily: SP_MONO_W, fontSize: 11, letterSpacing: '0.3em',
        textTransform: 'uppercase', color: SP_COL.grayDark,
      }}>{label}</div>
      <div style={{
        fontFamily: SP_MONO_W, fontWeight: 600, lineHeight: 1.05,
        fontSize: big ? 'clamp(44px, 5vw, 64px)' : 32,
        color: accent ? MX_ACCENT : SP_COL.white,
        letterSpacing: '-0.01em',
      }}>{value}</div>
      {note ? (
        <div style={{
          fontFamily: SP_MONO_W, fontSize: 11, lineHeight: 1.5, color: SP_COL.grayDark,
        }}>{note}</div>
      ) : null}
    </div>
  );
}

// SVG time-series chart. chartStyle: 'line' | 'step' | 'bars'.
// Needs two non-null points to draw; below that it renders emptyNote instead,
// which real pipeline output hits early (1-row history, null rotation series).
function SeriesChart({ rows, field, color = MX_ACCENT, height = 160, chartStyle = 'line', yFmt = MX_FMT.compact,
  emptyNote = 'Not enough history yet. This chart draws as daily snapshots accumulate.' }) {
  const W = 760, H = height, PAD_L = 56, PAD_R = 8, PAD_T = 10, PAD_B = 22;
  rows = rows.filter(r => r[field] != null);
  if (rows.length < 2) {
    return (
      <div style={{
        minHeight: height * 0.7, display: 'flex', alignItems: 'center', justifyContent: 'center',
        border: `1px dashed ${SP_COL.grayDarker}`, padding: '18px 16px',
      }}>
        <span style={{
          fontFamily: SP_MONO_W, fontSize: 11, lineHeight: 1.6,
          color: SP_COL.grayDark, textAlign: 'center', maxWidth: 420,
        }}>{emptyNote}</span>
      </div>
    );
  }
  const values = rows.map(r => r[field]);
  const min = Math.min(...values), max = Math.max(...values);
  const span = (max - min) || 1;
  const lo = min - span * 0.08, hi = max + span * 0.08;
  const x = (i) => PAD_L + (i / (values.length - 1)) * (W - PAD_L - PAD_R);
  const y = (v) => PAD_T + (1 - (v - lo) / (hi - lo)) * (H - PAD_T - PAD_B);

  const gridYs = [0.0, 0.5, 1.0].map(t => lo + t * (hi - lo));
  const dates = rows.map(r => r.date);
  const xticks = [0, Math.floor(values.length / 2), values.length - 1];

  let shape = null;
  if (chartStyle === 'bars') {
    const BUCKETS = 56;
    const per = Math.floor(values.length / BUCKETS);
    const bars = [];
    for (let b = 0; b < BUCKETS; b++) {
      const slice = values.slice(b * per, (b + 1) * per);
      bars.push(slice.reduce((a, c) => a + c, 0) / slice.length);
    }
    const bw = (W - PAD_L - PAD_R) / BUCKETS;
    shape = bars.map((v, b) => (
      <rect key={b} x={PAD_L + b * bw + 1} y={y(v)} width={Math.max(1, bw - 2)}
        height={Math.max(1, H - PAD_B - y(v))} fill={color} opacity={0.85} />
    ));
  } else {
    let d = '';
    values.forEach((v, i) => {
      if (i === 0) { d += `M ${x(0).toFixed(1)} ${y(v).toFixed(1)}`; return; }
      if (chartStyle === 'step') d += ` H ${x(i).toFixed(1)} V ${y(v).toFixed(1)}`;
      else d += ` L ${x(i).toFixed(1)} ${y(v).toFixed(1)}`;
    });
    shape = <path d={d} fill="none" stroke={color} strokeWidth="1.5" />;
  }

  return (
    <svg viewBox={`0 0 ${W} ${H}`} style={{ width: '100%', height: 'auto', display: 'block' }}>
      {gridYs.map((v, i) => (
        <g key={i}>
          <line x1={PAD_L} x2={W - PAD_R} y1={y(v)} y2={y(v)}
            stroke={SP_COL.grayDarker} strokeWidth="1" strokeDasharray="2 4" />
          <text x={PAD_L - 8} y={y(v) + 3} textAnchor="end"
            fontFamily={SP_MONO_W} fontSize="10" fill={SP_COL.grayDark}>{yFmt(v)}</text>
        </g>
      ))}
      {shape}
      {xticks.map((i, k) => (
        <text key={k} x={x(i)} y={H - 6}
          textAnchor={k === 0 ? 'start' : k === 2 ? 'end' : 'middle'}
          fontFamily={SP_MONO_W} fontSize="10" fill={SP_COL.grayDark}>{dates[i]}</text>
      ))}
    </svg>
  );
}

// Signer quorum distribution. The live network is extremely skewed (one
// combination holds ~99% of all signer lists), so one linear scale renders
// every other row invisible. Report it in two levels instead: the dominant
// combination as a proportion of all signer lists, then the remaining
// combinations scaled among themselves so the tail is actually comparable.
// An OTHER row absorbs combinations beyond the published top entries.
function QuorumBars({ dist, total }) {
  if (!dist || !dist.length) return null;
  const sorted = [...dist].sort((a, b) => b.count - a.count);
  const sum = total || sorted.reduce((a, d) => a + d.count, 0);
  const label = (d) => d.quorum === 0 && d.entries === 0 ? 'OTHER' : `${d.quorum} OF ${d.entries}`;
  const share = (c) => {
    const s = (c / sum) * 100;
    return (s >= 1 ? s.toFixed(1) : s.toFixed(2)) + '%';
  };

  const head = sorted[0];
  const tail = sorted.slice(1);
  const other = Math.max(0, sum - sorted.reduce((a, d) => a + d.count, 0));
  const rows = other > 0 ? [...tail, { quorum: 0, entries: 0, count: other }] : tail;
  const tailMax = rows.length ? Math.max(...rows.map(d => d.count)) : 1;
  const headShare = (head.count / sum) * 100;
  const restCount = sum - head.count;

  return (
    <div style={{ display: 'flex', flexDirection: 'column' }}>
      <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 12 }}>
        <span style={{
          fontFamily: SP_MONO_W, fontSize: 11, letterSpacing: '0.25em',
          textTransform: 'uppercase', color: SP_COL.grayLight,
        }}>{label(head)} quorum</span>
        <span style={{ fontFamily: SP_MONO_W, fontSize: 26, fontWeight: 600, color: MX_ACCENT }}>{share(head.count)}</span>
      </div>
      <div style={{ height: 14, background: 'rgba(128,128,128,0.18)', margin: '10px 0 8px 0' }}>
        <div style={{ width: `${headShare}%`, height: '100%', background: MX_ACCENT }}></div>
      </div>
      <span style={{ fontFamily: SP_MONO_W, fontSize: 11, color: SP_COL.grayDark }}>
        {MX_FMT.int(head.count)} of {MX_FMT.int(sum)} signer lists
      </span>

      {rows.length ? (
        <>
          <span style={{
            fontFamily: SP_MONO_W, fontSize: 10, letterSpacing: '0.3em',
            textTransform: 'uppercase', color: SP_COL.grayLight,
            borderTop: `1px solid ${SP_COL.grayDarker}`, padding: '14px 0 12px 0', margin: '18px 0 0 0',
          }}>Remaining {MX_FMT.int(restCount)} lists · scaled within this group</span>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
            {rows.map((d, i) => (
              <div key={i} style={{ display: 'grid', gridTemplateColumns: '64px 1fr 130px', gap: 12, alignItems: 'center' }}>
                <span style={{ fontFamily: SP_MONO_W, fontSize: 11, letterSpacing: '0.15em', color: SP_COL.grayLight }}>{label(d)}</span>
                <div style={{ height: 12, display: 'flex', alignItems: 'center' }}>
                  <div style={{
                    width: `${(d.count / tailMax) * 100}%`, minWidth: 2, height: '100%',
                    background: SP_COL.grayMed,
                  }}></div>
                </div>
                <span style={{ fontFamily: SP_MONO_W, fontSize: 11, color: SP_COL.grayDark, textAlign: 'right', whiteSpace: 'nowrap' }}>
                  {MX_FMT.int(d.count)} · {share(d.count)}
                </span>
              </div>
            ))}
          </div>
        </>
      ) : null}
    </div>
  );
}

// Posture grade badge, shared with the live lookup.
function MxGradeBadge({ grade }) {
  const blackholed = grade === 'BLACKHOLED';
  const color = blackholed ? SP_COL.grayMed : grade === 'A' ? MX_ACCENT : SP_COL.white;
  return (
    <div style={{
      width: 92, height: 92, flex: '0 0 auto',
      border: `2px solid ${color}`, color,
      display: 'flex', alignItems: 'center', justifyContent: 'center',
      fontFamily: SP_MONO_W, fontWeight: 600,
      fontSize: blackholed ? 12 : 48, letterSpacing: blackholed ? '0.1em' : 0,
      textAlign: 'center', lineHeight: 1.3,
      whiteSpace: 'pre-line',
    }}>{blackholed ? 'BLACK\nHOLED' : grade}</div>
  );
}

// ── Page sections ───────────────────────────────────────────────────────────

function MetricsHero({ latest, isSample }) {
  return (
    <header style={{ display: 'flex', flexDirection: 'column', gap: 22 }} data-screen-label="01 Metrics Hero">
      <p style={{
        fontFamily: SP_MONO_W, fontSize: 11, letterSpacing: '0.4em',
        textTransform: 'uppercase', color: SP_COL.grayLight, margin: 0,
      }}>XRP Ledger · Key Hygiene · Network Telemetry</p>
      <h1 style={{
        fontFamily: SP_MONO_W, fontSize: 'clamp(34px, 5vw, 48px)', lineHeight: 1.15, fontWeight: 600,
        margin: 0, color: SP_COL.white, letterSpacing: '-0.005em', maxWidth: 880, textWrap: 'balance',
      }}>How the XRP Ledger holds its keys.</h1>
      {isSample ? (
        <div style={{
          border: `1px solid ${SP_COL.accent}`, borderRadius: 4, padding: '12px 16px',
          display: 'flex', flexDirection: 'column', gap: 6, maxWidth: 720,
        }}>
          <span style={{
            fontFamily: SP_MONO_W, fontSize: 10, letterSpacing: '0.35em',
            textTransform: 'uppercase', color: SP_COL.accent, fontWeight: 600,
          }}>Sample data</span>
          <span style={{ fontFamily: SP_MONO_W, fontSize: 12, lineHeight: 1.6, color: SP_COL.grayLight }}>
            The daily crawler is not live yet. The aggregate numbers and charts on this
            page are illustrative fixtures. The account lookup below is live and reads
            the validated ledger directly.
          </span>
        </div>
      ) : null}
      <div style={{
        display: 'flex', flexDirection: 'column', gap: 14, maxWidth: 720,
        fontFamily: SP_MONO_W, fontSize: 15, lineHeight: 1.6, color: SP_COL.grayLight,
      }}>
        <p style={{ margin: 0 }}>A full-ledger measurement of account security posture and identity adoption. No explorer tracks key hygiene. This page does.</p>
        <p style={{ margin: 0 }}>Counted from every AccountRoot, SignerList, DID, and Credential object on the validated ledger, one snapshot per day.</p>
      </div>
      {isSample ? null : (
        <p style={{
          fontFamily: SP_MONO_W, fontSize: 11, letterSpacing: '0.25em',
          textTransform: 'uppercase', color: SP_COL.grayDark, margin: 0,
        }}>As of ledger {MX_FMT.int(latest.as_of_ledger)} · {latest.as_of_date} · snapshot, not live</p>
      )}
    </header>
  );
}

function HeadlineStats({ m }) {
  return (
    <section data-screen-label="02 Headline Stats" style={{ display: 'flex', flexDirection: 'column', gap: 32 }}>
      <OutlineCard title="The headline number.">
        <div className="sp-mx-hero-grid" style={{ display: 'grid', gridTemplateColumns: '1fr 1.2fr', gap: 32, alignItems: 'center' }}>
          <StatBlock big accent
            label="Accounts in default configuration"
            value={MX_FMT.pct(m.default_config_pct)} />
          <div style={{ fontFamily: SP_MONO_W, fontSize: 13, lineHeight: 1.7, color: SP_COL.grayLight }}>
            <p style={{ margin: 0 }}>
              {MX_FMT.int(m.default_config_count)} of {MX_FMT.int(m.funded_accounts)} funded accounts
              have no regular key, no signer list, and the master key enabled, the configuration
              every account starts with, and most never leave.
            </p>
          </div>
        </div>
      </OutlineCard>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)', gap: 22 }}>
        <OutlineCard><StatBlock label="Funded accounts" value={MX_FMT.compact(m.funded_accounts)} note="AccountRoot entries on the validated ledger" /></OutlineCard>
        <OutlineCard><StatBlock label="Regular key set" value={MX_FMT.pct(m.regular_key_pct)} note={MX_FMT.int(m.regular_key_count) + ' accounts, excluding blackholed keys'} /></OutlineCard>
        <OutlineCard><StatBlock label="Signer list" value={MX_FMT.pct(m.signer_list_pct)} note={MX_FMT.int(m.signer_list_count) + ' accounts with multisign quorum'} /></OutlineCard>
        <OutlineCard><StatBlock label="Master disabled" value={MX_FMT.compact(m.master_disabled_count)} note="lsfDisableMaster flag set" /></OutlineCard>
        <OutlineCard><StatBlock label="Blackholed" value={MX_FMT.compact(m.blackholed_count)} note="No signing path remains, intentional for token issuers" /></OutlineCard>
      </div>
    </section>
  );
}

// Trend charts need history to mean anything. Below MIN_TREND_POINTS daily
// rows the whole section renders as one compact "collecting" card that shows
// the current value of each series instead of three empty placeholder boxes.
const MIN_TREND_POINTS = 7;

function TrendCollectingRow({ label, value, status }) {
  return (
    <div className="sp-mx-method-row" style={{
      display: 'grid', gridTemplateColumns: '1fr auto auto', gap: 18, alignItems: 'baseline',
      borderTop: `1px solid ${SP_COL.grayDarker}`, padding: '12px 0',
    }}>
      <span style={{
        fontFamily: SP_MONO_W, fontSize: 11, letterSpacing: '0.2em',
        textTransform: 'uppercase', color: SP_COL.grayLight,
      }}>{label}</span>
      <span style={{ fontFamily: SP_MONO_W, fontSize: 18, fontWeight: 600, color: SP_COL.white }}>{value}</span>
      <span style={{ fontFamily: SP_MONO_W, fontSize: 11, color: SP_COL.grayDark, textAlign: 'right', minWidth: 120 }}>{status}</span>
    </div>
  );
}

function TrendCharts({ rows }) {
  const usable = (f) => rows.filter(r => r[f] != null);
  const days = usable('default_config_pct').length;

  if (days < MIN_TREND_POINTS) {
    const latest = rows.length ? rows[rows.length - 1] : {};
    const since = rows.length ? rows[0].date : null;
    const dayLabel = `day ${days} of history`;
    return (
      <section data-screen-label="03 Trends">
        <OutlineCard title="Trend history · collecting.">
          <TrendCollectingRow label="Default configuration share"
            value={MX_FMT.pct(latest.default_config_pct)} status={dayLabel} />
          <TrendCollectingRow label="DID objects on ledger"
            value={MX_FMT.int(latest.did_total)} status={dayLabel} />
          <TrendCollectingRow label="Key rotations · trailing 30d"
            value="·" status="awaiting the stream listener" />
          <p style={{ fontFamily: SP_MONO_W, fontSize: 11, lineHeight: 1.6, color: SP_COL.grayDark, margin: '14px 0 0 0' }}>
            Daily snapshots began {since}. Trend charts draw here once {MIN_TREND_POINTS} days
            of history accumulate.
          </p>
        </OutlineCard>
      </section>
    );
  }

  return (
    <section data-screen-label="03 Trends" style={{ display: 'flex', flexDirection: 'column', gap: 32 }}>
      <OutlineCard title="Default configuration share · daily snapshots.">
        <SeriesChart rows={rows} field="default_config_pct"
          color={MX_ACCENT} yFmt={MX_FMT.pct} height={180} />
        <p style={{ fontFamily: SP_MONO_W, fontSize: 11, lineHeight: 1.6, color: SP_COL.grayDark, margin: '12px 0 0 0' }}>
          Share of funded accounts signing with the master key only. Movement is slow by
          nature, new accounts arrive in default configuration.
        </p>
      </OutlineCard>
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 22 }}>
        <OutlineCard title="DID objects · adoption.">
          <SeriesChart rows={rows} field="did_total"
            color={SP_COL.white} yFmt={MX_FMT.compact} height={140} />
          <p style={{ fontFamily: SP_MONO_W, fontSize: 11, lineHeight: 1.6, color: SP_COL.grayDark, margin: '12px 0 0 0' }}>
            XLS-40 DID objects on ledger. Counts are small and early; the chart tracks adoption over time.
          </p>
        </OutlineCard>
        <OutlineCard title="Key rotations · trailing 30d window.">
          <SeriesChart rows={rows} field="rotation_30d"
            color={SP_COL.white} yFmt={MX_FMT.compact} height={140}
            emptyNote="Rotation counts come from the validated-transaction stream listener, which is not live yet." />
          <p style={{ fontFamily: SP_MONO_W, fontSize: 11, lineHeight: 1.6, color: SP_COL.grayDark, margin: '12px 0 0 0' }}>
            SetRegularKey + SignerListSet transactions, validated, trailing 30 days.
          </p>
        </OutlineCard>
      </div>
    </section>
  );
}

function ValueAndDormancy({ m }) {
  return (
    <section data-screen-label="04 Value + Dormancy" style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 22 }}>
      <OutlineCard title="Value held in default configuration.">
        <div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
          <StatBlock label="All default-config accounts" value={MX_FMT.xrp(m.xrp_in_default_config)} />
          <StatBlock label="Accounts holding 10,000+ XRP" value={MX_FMT.xrp(m.xrp_in_default_config_10k)}
            note={m.xrp_in_default_config > 0
              ? ((m.xrp_in_default_config_10k / m.xrp_in_default_config) * 100).toFixed(1) +
                '% of default-config value sits in accounts holding 10,000+ XRP'
              : null} />
        </div>
      </OutlineCard>
      <OutlineCard title="Dormant accounts · no recent state change.">
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 16 }}>
          <StatBlock label="1 year" value={MX_FMT.compact(m.dormant_1y)} />
          <StatBlock label="3 years" value={MX_FMT.compact(m.dormant_3y)} />
          <StatBlock label="5 years" value={MX_FMT.compact(m.dormant_5y)} />
        </div>
        <p style={{ fontFamily: SP_MONO_W, fontSize: 11, lineHeight: 1.6, color: SP_COL.grayDark, margin: '20px 0 0 0' }}>
          Measured by last ledger state change. Any incoming payment resets the clock, so
          these figures undercount truly inactive accounts.
        </p>
      </OutlineCard>
    </section>
  );
}

function QuorumAndIdentity({ m }) {
  return (
    <section data-screen-label="05 Quorum + Identity" style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 22 }}>
      <OutlineCard title="Signer quorum distribution.">
        <QuorumBars dist={m.quorum_distribution} total={m.signer_list_count} />
        <p style={{ fontFamily: SP_MONO_W, fontSize: 11, lineHeight: 1.6, color: SP_COL.grayDark, margin: '16px 0 0 0' }}>
          Shares are of all signer lists on the validated ledger. The lower bars are
          scaled within their own group, not against the dominant combination.
        </p>
      </OutlineCard>
      <OutlineCard title="Identity adoption.">
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 24 }}>
          <StatBlock label="DID objects" value={MX_FMT.int(m.did_total)}
            note={m.did_new_30d == null ? 'XLS-40 DID ledger objects' : '+' + MX_FMT.int(m.did_new_30d) + ' in the last 30 days'} />
          <StatBlock label="Credentials" value={MX_FMT.int(m.credential_total)} note="XLS-70 credential objects" />
          <StatBlock label="Domain set" value={MX_FMT.int(m.domain_set_count)} note="Accounts publishing a domain field" />
          {m.master_disable_30d == null ? (
            <StatBlock label="Credentials accepted" value={MX_FMT.pct1(m.credential_accepted_pct)}
              note="Share accepted by the subject account" />
          ) : (
            <StatBlock label="Master disables / 30d" value={MX_FMT.int(m.master_disable_30d)}
              note="asfDisableMaster transactions" />
          )}
        </div>
      </OutlineCard>
    </section>
  );
}

function Methodology() {
  const lines = [
    ['SOURCE', 'Full-ledger snapshot via ledger_data, pinned to one validated ledger index per crawl.'],
    ['CADENCE', 'Daily. Published as static JSON, metrics-latest.json and metrics-history.json. No live node calls for the aggregate numbers on this page.'],
    ['BLACKHOLED', 'Master disabled, no signer list, and no usable regular key. Intentional for token issuers, a category, not a risk score.'],
    ['DORMANCY', 'Proxied by PreviousTxnLgrSeq, which moves on any state change including incoming dust. Treat as a lower bound on inactivity.'],
  ];
  return (
    <section data-screen-label="06 Methodology">
      <TerminalCard title="methodology · read this before quoting numbers">
        <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
          {lines.map(([k, v]) => (
            <div key={k} className="sp-mx-method-row" style={{ display: 'grid', gridTemplateColumns: '120px 1fr', gap: 16, alignItems: 'baseline' }}>
              <span style={{ fontFamily: SP_MONO_W, fontSize: 11, letterSpacing: '0.25em', color: MX_ACCENT }}>{k}</span>
              <span style={{ fontFamily: SP_MONO_W, fontSize: 13, lineHeight: 1.6, color: SP_COL.grayLight }}>{v}</span>
            </div>
          ))}
        </div>
      </TerminalCard>
    </section>
  );
}

// Provenance strip above the site footer, mirrors the published-file contract.
function MetricsProvenance({ latest, isSample }) {
  return (
    <section data-screen-label="08 Provenance" style={{
      borderTop: `1px solid ${SP_COL.grayDarker}`, paddingTop: 24,
      display: 'flex', justifyContent: 'space-between', flexWrap: 'wrap', gap: 12,
      fontFamily: SP_MONO_W, fontSize: 10, letterSpacing: '0.25em',
      textTransform: 'uppercase', color: SP_COL.grayDark,
    }}>
      <span>SciPHR · On-chain metrics · ledger {MX_FMT.int(latest.as_of_ledger)}{isSample ? ' (sample fixture)' : ''}</span>
      <span>Published: metrics-latest.json · metrics-history.json</span>
    </section>
  );
}

// ── Account posture lookup, LIVE ────────────────────────────────────────────
// Queries public XRPL JSON-RPC endpoints client-side (account_info +
// account_objects), grades the configuration, and renders factual findings.
// No keys involved; read-only public data.

const MX_RPC_ENDPOINTS = ['https://xrplcluster.com/', 'https://s2.ripple.com:51234/'];
const MX_LSF_DISABLE_MASTER = 0x00100000;
const MX_BLACKHOLE_KEYS = [
  'rrrrrrrrrrrrrrrrrrrrrhoLvTp',  // ACCOUNT_ZERO
  'rrrrrrrrrrrrrrrrrrrrBZbvji',   // ACCOUNT_ONE
  'rrrrrrrrrrrrrrrrrNAMEtxvNvQ',  // ripple-name reservation
  'rrrrrrrrrrrrrrrrrrrn5RM1rHd',  // NaN address
];

// Well-known mainnet accounts for one-tap demos. Results are whatever the
// ledger says today; labels identify the account, not a promised grade.
const MX_TRY_ADDRESSES = [
  { address: 'rsoLo2S1kiGeCcn6hCUXVrCpGMWLrRrLZz', tag: 'SOLO ISSUER' },
  { address: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B', tag: 'BITSTAMP ISSUER' },
  { address: 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh', tag: 'GENESIS-PHRASE ACCT' },
];

async function mxRpc(method, params) {
  let lastErr;
  for (const url of MX_RPC_ENDPOINTS) {
    try {
      const r = await fetch(url, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ method, params: [params] }),
      });
      if (!r.ok) throw new Error('HTTP ' + r.status);
      const j = await r.json();
      if (j.result && j.result.status === 'success') {
        j.result._host = new URL(url).hostname;
        return j.result;
      }
      if (j.result && j.result.error) { const e = new Error(j.result.error); e.rippled = j.result.error; throw e; }
      throw new Error('malformed response');
    } catch (e) {
      if (e.rippled) throw e; // a real rippled error (e.g. actNotFound), don't retry
      lastErr = e;
    }
  }
  throw lastErr || new Error('all endpoints failed');
}

function mxDecodeDomain(hex) {
  if (!hex) return null;
  try {
    let s = '';
    for (let i = 0; i < hex.length; i += 2) s += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
    return s;
  } catch (e) { return null; }
}

function mxBalanceBand(drops) {
  const xrp = Number(drops) / 1e6;
  if (xrp < 100) return '< 100 XRP';
  if (xrp < 1e3) return '100 – 1K XRP';
  if (xrp < 1e4) return '1K – 10K XRP';
  if (xrp < 1e5) return '10K – 100K XRP';
  if (xrp < 1e6) return '100K – 1M XRP';
  return '1M+ XRP';
}

// Pure grading logic, mirrors the spec's posture categories.
function mxGrade({ masterDisabled, regularKey, signerList, signerQuorum, signerCount }) {
  const rkBlackholed = regularKey && MX_BLACKHOLE_KEYS.indexOf(regularKey) !== -1;
  const usableRk = regularKey && !rkBlackholed;
  if (masterDisabled && !signerList && !usableRk) {
    return {
      grade: 'BLACKHOLED',
      findings: [
        rkBlackholed
          ? 'Master key disabled and regular key set to a known blackhole address.'
          : 'Master key disabled with no regular key and no signer list.',
        'No remaining signing path exists. The account is irrecoverable by design.',
        'This is a deliberate pattern used by token issuers to freeze supply rules.',
      ],
    };
  }
  if (signerList && masterDisabled) {
    return {
      grade: 'A',
      findings: [
        `Signer list present with a ${signerQuorum}-of-${signerCount} quorum. No single key can move funds.`,
        'Master key is disabled. The original seed is no longer a spending path.',
        'Recovery is governed by the signer set, not by any one device.',
      ],
    };
  }
  if (signerList || usableRk) {
    const f = [];
    if (signerList) f.push(`Signer list present with a ${signerQuorum}-of-${signerCount} quorum.`);
    if (usableRk) f.push('A regular key is set. Day-to-day signing does not expose the master key.');
    f.push('The master key remains enabled and can still sign. Disabling it removes that path.');
    if (!signerList) f.push('No signer list. Recovery depends on the keys that exist today.');
    return { grade: 'B', findings: f };
  }
  return {
    grade: 'C',
    findings: [
      'This account signs with its master key only, the configuration every account starts with.',
      'One secret controls the account. If it is lost or leaked, there is no second signing path.',
      'No rotation has ever occurred on this account.',
    ],
  };
}

async function mxLookup(address) {
  // signer_lists:true folds the SignerList into account_info; unlike a
  // paginated account_objects scan it can't miss the list on accounts that
  // own thousands of objects (e.g. token issuers).
  const info = await mxRpc('account_info', { account: address, ledger_index: 'validated', signer_lists: true });
  const ad = info.account_data;
  const masterDisabled = (ad.Flags & MX_LSF_DISABLE_MASTER) !== 0;
  const regularKey = ad.RegularKey || null;
  const sl = ((ad.signer_lists || info.signer_lists || []))[0] || null;
  const signerQuorum = sl ? sl.SignerQuorum : 0;
  const signerCount = sl ? (sl.SignerEntries || []).length : 0;

  // DID (XLS-40) via direct ledger_entry, deterministic, no pagination.
  // entryNotFound means no DID; other errors (older servers) leave it unset.
  let hasDid = false;
  try {
    await mxRpc('ledger_entry', { did: address, ledger_index: 'validated' });
    hasDid = true;
  } catch (e) { /* entryNotFound or unsupported, leave as not set */ }

  const domain = mxDecodeDomain(ad.Domain);
  const { grade, findings } = mxGrade({
    masterDisabled, regularKey, signerList: !!sl, signerQuorum, signerCount,
  });

  return {
    address,
    ledgerIndex: info.ledger_index,
    host: info._host,
    grade,
    findings,
    balance_band: mxBalanceBand(ad.Balance),
    config: {
      master_key: masterDisabled ? 'DISABLED' : 'ENABLED',
      regular_key: regularKey
        ? (MX_BLACKHOLE_KEYS.indexOf(regularKey) !== -1 ? regularKey.slice(0, 20) + '… (BLACKHOLE)' : 'SET')
        : 'NOT SET',
      signer_list: sl ? `${signerQuorum}-OF-${signerCount} QUORUM` : 'NONE',
      domain: domain ? 'SET' : 'NOT SET',
      did: hasDid ? 'SET' : 'NOT SET',
    },
  };
}

function PostureLookup() {
  const [input, setInput] = React.useState('');
  const [phase, setPhase] = React.useState('idle'); // idle | running | done | error
  const [result, setResult] = React.useState(null);
  const [error, setError] = React.useState(null);
  const live = React.useRef(true);
  React.useEffect(() => () => { live.current = false; }, []);

  const run = async (addr) => {
    const q = (addr || input).trim();
    if (!q || phase === 'running') return;
    setInput(q);
    if (!/^r[1-9A-HJ-NP-Za-km-z]{24,34}$/.test(q)) {
      setError({ kind: 'format', msg: 'Not a valid XRPL classic address. Expected r… (base58, 25–35 chars).' });
      setPhase('error');
      return;
    }
    setPhase('running'); setResult(null); setError(null);
    try {
      const res = await mxLookup(q);
      if (!live.current) return;
      setResult(res); setPhase('done');
    } catch (e) {
      if (!live.current) return;
      const rippled = e.rippled;
      setError({
        kind: rippled || 'network',
        msg: rippled === 'actNotFound'
          ? 'Account not found on the validated ledger. It may be unfunded or deleted.'
          : rippled
            ? 'Ledger returned: ' + rippled
            : 'Could not reach a public XRPL endpoint. Check your connection and retry.',
      });
      setPhase('error');
    }
  };

  const inputStyle = {
    flex: 1, minWidth: 0, background: SP_COL.black, color: SP_COL.white,
    border: `1px solid ${SP_COL.grayDark}`, borderRadius: 4,
    padding: '12px 14px', fontFamily: SP_MONO_W, fontSize: 13, outline: 'none',
  };

  return (
    <section id="posture" data-screen-label="07 Posture Lookup" style={{ display: 'flex', flexDirection: 'column', gap: 22 }}>
      <OutlineCard title="Check an account · live.">
        <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
          <p style={{ fontFamily: SP_MONO_W, fontSize: 13, lineHeight: 1.6, color: SP_COL.grayLight, margin: 0 }}>
            Enter any XRPL address for a configuration-level posture read, straight from the
            validated ledger: grade, key setup, and what it means. Read-only public data, no
            keys involved.
          </p>
          <form onSubmit={(e) => { e.preventDefault(); run(); }} style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
            <input value={input} onChange={(e) => setInput(e.target.value)}
              placeholder="r..." spellCheck={false} style={inputStyle} aria-label="XRPL address" />
            <CommandButton as="button" type="submit" label={phase === 'running' ? 'Reading…' : 'Run lookup'}
              variant="solid" disabled={phase === 'running'} />
          </form>
          <div style={{ display: 'flex', gap: 10, flexWrap: 'wrap', alignItems: 'center' }}>
            <span style={{ fontFamily: SP_MONO_W, fontSize: 10, letterSpacing: '0.25em', textTransform: 'uppercase', color: SP_COL.grayDark }}>Try:</span>
            {MX_TRY_ADDRESSES.map(e => (
              <button key={e.address} onClick={() => run(e.address)} style={{
                background: 'transparent', color: SP_COL.grayLight, cursor: 'pointer',
                border: `1px solid ${SP_COL.grayDarker}`, borderRadius: 4,
                padding: '6px 10px', fontFamily: SP_MONO_W, fontSize: 10,
                letterSpacing: '0.15em', textTransform: 'uppercase',
              }}>{e.tag}</button>
            ))}
          </div>
        </div>
      </OutlineCard>

      {phase === 'running' ? (
        <TerminalCard title="posture · querying validated ledger">
          <div style={{ display: 'flex', flexDirection: 'column', gap: 8, fontFamily: SP_MONO_W, fontSize: 13, lineHeight: 1.6, color: SP_COL.grayLight }}>
            <p style={{ margin: 0 }}>&gt; account_info {input.slice(0, 18)}… ledger_index:validated signer_lists:true</p>
            <p style={{ margin: 0 }}>&gt; ledger_entry did</p>
            <p style={{ margin: 0 }}>&gt; <Caret /></p>
          </div>
        </TerminalCard>
      ) : null}

      {phase === 'error' && error ? (
        <TerminalCard title="posture · no result">
          <div style={{ display: 'flex', flexDirection: 'column', gap: 8, fontFamily: SP_MONO_W, fontSize: 13, lineHeight: 1.6 }}>
            <p style={{ margin: 0, color: SP_COL.error }}>&gt; {error.msg}</p>
            <p style={{ margin: 0, color: SP_COL.grayDark }}>&gt; nothing was written. Lookups are read-only.</p>
          </div>
        </TerminalCard>
      ) : null}

      {phase === 'done' && result ? (
        <TerminalCard
          title={'posture · ' + result.address.slice(0, 24) + (result.address.length > 24 ? '…' : '')}
          footer={
            <span>Live · ledger {result.ledgerIndex ? result.ledgerIndex.toLocaleString('en-US') : 'validated'} · public JSON-RPC ({result.host || 'xrplcluster.com'}) · read-only</span>
          }>
          <div style={{ display: 'flex', gap: 28, alignItems: 'flex-start', flexWrap: 'wrap' }}>
            <MxGradeBadge grade={result.grade} />
            <div style={{ flex: 1, minWidth: 280, display: 'flex', flexDirection: 'column', gap: 16 }}>
              <div className="sp-mx-config-grid" style={{ display: 'grid', gridTemplateColumns: 'repeat(3, auto)', gap: '10px 28px', justifyContent: 'start' }}>
                {Object.entries(result.config).map(([k, v]) => (
                  <div key={k} style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
                    <span style={{ fontFamily: SP_MONO_W, fontSize: 10, letterSpacing: '0.2em', textTransform: 'uppercase', color: SP_COL.grayDark }}>{k.replace(/_/g, ' ')}</span>
                    <span style={{ fontFamily: SP_MONO_W, fontSize: 12, color: v === 'DISABLED' || v === 'NONE' || v === 'NOT SET' || v.indexOf('BLACKHOLE') !== -1 ? SP_COL.grayMed : MX_ACCENT }}>{v}</span>
                  </div>
                ))}
                <div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
                  <span style={{ fontFamily: SP_MONO_W, fontSize: 10, letterSpacing: '0.2em', textTransform: 'uppercase', color: SP_COL.grayDark }}>balance band</span>
                  <span style={{ fontFamily: SP_MONO_W, fontSize: 12, color: SP_COL.white }}>{result.balance_band}</span>
                </div>
              </div>
              <hr style={{ border: 0, borderTop: `1px solid rgba(128,128,128,0.4)`, margin: 0 }} />
              <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
                {result.findings.map((f, i) => (
                  <p key={i} style={{ fontFamily: SP_MONO_W, fontSize: 13, lineHeight: 1.6, color: SP_COL.grayLight, margin: 0 }}>&gt; {f}</p>
                ))}
                {result.grade === 'BLACKHOLED' ? (
                  <p style={{ fontFamily: SP_MONO_W, fontSize: 13, lineHeight: 1.6, color: SP_COL.grayMed, margin: 0 }}>&gt; Not graded, irrecoverable by design.</p>
                ) : null}
              </div>
            </div>
          </div>
        </TerminalCard>
      ) : null}
    </section>
  );
}

Object.assign(window, {
  MX_ACCENT, MX_FMT, StatBlock, SeriesChart, QuorumBars, MxGradeBadge,
  MetricsHero, HeadlineStats, TrendCharts, ValueAndDormancy, QuorumAndIdentity,
  Methodology, MetricsProvenance, PostureLookup,
});
