// ─────────────────────────────────────────────────────────────────────────
// reporting.jsx — the Leads and Ads sections Bobby and Mike asked for on the
// September 14 call. New surface only: nothing in here touches the CA Rollup,
// because Bobby was explicit that it "shouldn't be changed" [3:52:37].
//
//   Leads — "did the lead come in from Facebook? Did the lead come in from
//   Google? Did the lead come in from the website? Did the lead come in from
//   the phone?" — Bobby [3:51:14]
//
//   Ads — "Ad management should be its own tab... active campaign performance,
//   leads generated, lead costs, CTR. Just at a glance, be like, okay, they're
//   looking good, they need some work." — Mike [3:56:10]
//
//   Sections — "maybe each source should have its own section, and each
//   section should have subsections. So for the meta advertising, the
//   subsections would be by campaign and ad set." — Bobby [3:56:36]
//
// The numbers come from the tables the GoHighLevel and Facebook syncs write
// into. Neither sync exists yet, so these screens are mostly empty — and they
// say why they are empty instead of drawing a row of zeros that reads like a
// measurement. A zero you can trust is worth more than a zero you can't.
// ─────────────────────────────────────────────────────────────────────────

// ── Period window ─────────────────────────────────────────────────────────
// Every screen here is "over a window", and Mike asked to be able to change it.
const RPT_PERIODS = [
  { v: '7d',      label: 'Last 7 days' },
  { v: '30d',     label: 'Last 30 days' },
  { v: 'month',   label: 'This month' },
  { v: 'quarter', label: 'This quarter' },
  { v: 'year',    label: 'This year' },
  { v: 'all',     label: 'All time' },
];

const rptIso = (d) => d.toISOString().slice(0, 10);

function rptWindow(period) {
  const today = new Date();
  const end = rptIso(today);
  const back = (days) => {
    const d = new Date(today);
    d.setDate(d.getDate() - days);
    return rptIso(d);
  };
  switch (period) {
    case '7d':      return { from: back(7),  to: end, label: 'Last 7 days' };
    case '30d':     return { from: back(30), to: end, label: 'Last 30 days' };
    case 'month':   return { from: rptIso(new Date(today.getFullYear(), today.getMonth(), 1)), to: end, label: 'This month' };
    case 'quarter': return { from: rptIso(new Date(today.getFullYear(), Math.floor(today.getMonth() / 3) * 3, 1)), to: end, label: 'This quarter' };
    case 'year':    return { from: rptIso(new Date(today.getFullYear(), 0, 1)), to: end, label: 'This year' };
    default:        return { from: null, to: null, label: 'All time' };
  }
}

// ── Small shared pieces ───────────────────────────────────────────────────

// The page around every reporting section: room at the sides, and at the bottom for the phone's floating
// tab bar, which otherwise covers the last rows.
const RPT_PAGE = { padding: '12px 16px 112px', minWidth: 0 };

// Filters in an even grid: two to a row on a phone, side by side on a wider screen.
function RptControls({ theme, children }) {
  return (
    <div style={{
      display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(150px, 1fr))',
      gap: 8, marginBottom: 12, maxWidth: 720,
    }}>
      {children}
    </div>
  );
}

// Headline numbers: two to a row on a phone, stretched across the row when there is room.
function RptStats({ children }) {
  return (
    <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))', gap: 8, marginBottom: 16 }}>
      {children}
    </div>
  );
}

// The section's name and the window it covers, the same on every reporting screen.
function RptHeader({ theme, label, children }) {
  return (
    <>
      <SectionLabel theme={theme}>{label}</SectionLabel>
      <div style={{ fontSize: 12, color: theme.inkMuted, marginTop: 4, marginBottom: 12 }}>{children}</div>
    </>
  );
}

function rptSelectStyle(theme) {
  return {
    appearance: 'none', padding: '9px 28px 9px 12px', width: '100%', minWidth: 0,
    background: theme.surface, color: theme.ink,
    border: `1px solid ${theme.rule}`, borderRadius: 8,
    fontSize: 13, fontFamily: 'inherit', outline: 'none', cursor: 'pointer',
  };
}

function RptSelect({ theme, value, onChange, options, ariaLabel }) {
  return (
    <select
      value={value}
      onChange={(e) => onChange(e.target.value)}
      aria-label={ariaLabel}
      style={rptSelectStyle(theme)}
    >
      {options.map(o => <option key={o.v} value={o.v}>{o.label}</option>)}
    </select>
  );
}

// A stat that knows the difference between "none" and "not measured yet".
// Twelve buckets across whatever window is selected, so the sparkline has the
// same shape whether the period is a week or a quarter. Items carry a date and
// optionally a value to sum; without one they are counted.
function rptSeries(items, dateOf, win, valueOf, buckets = 12) {
  if (!items || !items.length || !win || !win.from || !win.to) return null;
  const start = Date.parse(`${win.from}T00:00:00Z`);
  const end = Date.parse(`${win.to}T23:59:59Z`);
  if (!Number.isFinite(start) || !Number.isFinite(end) || end <= start) return null;

  const out = new Array(buckets).fill(0);
  const width = (end - start) / buckets;
  let seen = 0;
  for (const item of items) {
    const raw = dateOf(item);
    if (!raw) continue;
    const at = Date.parse(raw.length <= 10 ? `${raw}T12:00:00Z` : raw);
    if (!Number.isFinite(at) || at < start || at > end) continue;
    const slot = Math.min(buckets - 1, Math.floor((at - start) / width));
    out[slot] += valueOf ? (valueOf(item) || 0) : 1;
    seen += 1;
  }
  // A flat line of zeros says nothing; leave the tile as a plain number.
  return seen ? out : null;
}

// A twelve-point sparkline: the shape of the window behind the number, with the
// latest point marked. One hue — this is one series, so it needs no legend and
// no palette. Decorative on its own; it earns its place by turning a number
// into "and it is going up".
function RptSpark({ theme, points, width = 108, height = 26 }) {
  const values = (points || []).filter(v => Number.isFinite(v));
  if (values.length < 3) return null;

  const low = Math.min(...values);
  const high = Math.max(...values);
  const span = high - low || 1;
  const step = values.length > 1 ? width / (values.length - 1) : width;
  // Inset by the stroke so the line is not clipped at the top and bottom.
  const pad = 2;
  const y = v => pad + (height - pad * 2) * (1 - (v - low) / span);
  const d = values.map((v, i) => `${i ? 'L' : 'M'}${(i * step).toFixed(1)} ${y(v).toFixed(1)}`).join(' ');
  const lastX = (values.length - 1) * step;
  const lastY = y(values[values.length - 1]);

  return (
    <svg
      width={width} height={height} viewBox={`0 0 ${width} ${height}`}
      // The number beside it is the accessible value; this is the shape of it.
      aria-hidden="true" focusable="false"
      style={{ display: 'block', marginTop: 8, overflow: 'visible' }}
    >
      <path d={d} fill="none" stroke={theme.inkMuted} strokeWidth="2"
            strokeLinecap="round" strokeLinejoin="round" opacity="0.65" />
      <circle cx={lastX} cy={lastY} r="3" fill={theme.accent}
              stroke={theme.surface} strokeWidth="2" />
    </svg>
  );
}

// 1,284 · 12.9K · 1.2M — a stat tile reads at a glance or it is just a table cell.
function rptCompact(n) {
  if (!Number.isFinite(n)) return n;
  const size = Math.abs(n);
  if (size >= 1e6) return `${(n / 1e6).toFixed(size >= 1e7 ? 0 : 1)}M`;
  if (size >= 10000) return `${(n / 1000).toFixed(size >= 1e5 ? 0 : 1)}K`;
  return n.toLocaleString();
}

function RptStat({ theme, label, value, sub, muted, trend, delta, deltaLabel, upIsGood = true }) {
  const shown = typeof value === 'number' ? rptCompact(value) : value;
  const moved = Number.isFinite(delta) && delta !== 0;
  // Direction crossed with whether up is the good direction — a rising cost per
  // lead is not good news, and the colour should not say it is.
  const good = moved ? (delta > 0) === upIsGood : null;

  return (
    <div style={{
      minWidth: 0,
      background: theme.surface, border: `1px solid ${theme.rule}`,
      borderRadius: theme.radius || 10, padding: '12px 14px',
    }}>
      <div style={{ fontSize: 10, fontWeight: 700, letterSpacing: 0.6, textTransform: 'uppercase', color: theme.inkMuted }}>
        {label}
      </div>
      <div style={{
        // Proportional figures: tabular-nums gives every digit the width of a
        // zero, which makes a number like 121 look loose at this size. Tabular
        // belongs in table columns, where digits must line up.
        fontSize: 22, fontWeight: 700, marginTop: 4,
        color: muted ? theme.inkMuted : theme.ink,
      }}>{shown}</div>
      {moved ? (
        <div style={{
          fontSize: 11, marginTop: 3, fontWeight: 600,
          color: good ? (STATUS && STATUS.green) || '#43A047' : '#C6483C',
        }}>
          {delta > 0 ? '▲' : '▼'} {rptCompact(Math.abs(delta))}
          {deltaLabel ? <span style={{ color: theme.inkMuted, fontWeight: 400 }}> {deltaLabel}</span> : null}
        </div>
      ) : null}
      {sub ? <div style={{ fontSize: 11, color: theme.inkMuted, marginTop: 2 }}>{sub}</div> : null}
      {trend ? <RptSpark theme={theme} points={trend} /> : null}
    </div>
  );
}

// Why a screen is empty matters more than the fact that it is. This says which
// sync has to exist before numbers appear, so nobody files it as a bug.
function RptNotConnected({ theme, title, what, needs }) {
  return (
    <div style={{
      border: `1px dashed ${theme.rule}`, borderRadius: theme.radius || 10,
      padding: 20, textAlign: 'left', background: 'transparent',
    }}>
      <div style={{ fontSize: 14, fontWeight: 700, color: theme.ink, marginBottom: 6 }}>{title}</div>
      <div style={{ fontSize: 13, color: theme.inkSoft, lineHeight: 1.5 }}>{what}</div>
      {needs ? (
        <div style={{ fontSize: 12, color: theme.inkMuted, marginTop: 10, lineHeight: 1.6 }}>
          <strong style={{ color: theme.inkSoft, fontWeight: 600 }}>Waiting on:</strong> {needs}
        </div>
      ) : null}
    </div>
  );
}

// Last sync, in words. Kurt and Mike confirm the data — they need to know
// whether a missing number is a real nothing or a failed job.
function RptSyncNote({ theme, runs, source, label }) {
  const run = (runs || []).find(r => r.source === source);
  if (!run) {
    return (
      <div style={{ fontSize: 11, color: theme.inkMuted, marginBottom: 10 }}>
        {label} has never run.
      </div>
    );
  }
  const when = run.startedAt ? new Date(run.startedAt).toLocaleString() : 'unknown time';
  const tone = run.status === 'failed' ? '#C6483C' : run.status === 'ok' ? theme.inkMuted : theme.gold;
  return (
    <div style={{ fontSize: 11, color: tone, marginBottom: 10 }}>
      {label} · {run.status === 'ok' ? 'last ran' : run.status} {when}
      {run.status === 'ok' && run.rowsWritten != null ? ` · ${run.rowsWritten} rows` : ''}
      {run.error ? ` · ${run.error}` : ''}
    </div>
  );
}

// Every reporting screen draws through RptTable, so a phone reading only has to
// exist once. Scrolling a nine-column table sideways on a phone is not reading
// it — the row label is off-screen by the time you reach the number.
function useNarrow(breakpoint = 720) {
  const [vw, setVw] = React.useState(typeof window !== 'undefined' ? window.innerWidth : 1200);
  React.useEffect(() => {
    const onResize = () => setVw(window.innerWidth);
    window.addEventListener('resize', onResize);
    return () => window.removeEventListener('resize', onResize);
  }, []);
  return vw < breakpoint;
}

function RptCardRow({ theme, columns, title, get, strong }) {
  const text = columns.filter(c => c.align !== 'right');
  const nums = columns.filter(c => c.align === 'right');
  const sub = text.map(c => ({ c, v: get(c) })).filter(x => x.v != null && x.v !== '' && x.v !== '—');
  return (
    <div style={{
      border: `1px solid ${theme.rule}`, borderRadius: theme.radius || 10, minWidth: 0,
      background: strong ? (theme.bgElev || theme.surface) : theme.surface, padding: '12px 14px',
    }}>
      <div style={{ fontSize: 14, fontWeight: 700, color: theme.ink, overflowWrap: 'anywhere' }}>{title}</div>
      {sub.length ? (
        <div style={{ fontSize: 12, color: theme.inkMuted, marginTop: 3, overflowWrap: 'anywhere' }}>
          {sub.map((x, i) => <React.Fragment key={x.c.key}>{i ? ' · ' : ''}{x.v}</React.Fragment>)}
        </div>
      ) : null}
      {nums.length ? (
        <div style={{
          // Three across, or two by two when there are four, so a row never ends with one stray number.
          display: 'grid', gridTemplateColumns: `repeat(${nums.length <= 3 ? nums.length : (nums.length === 4 ? 2 : 3)}, minmax(0, 1fr))`,
          gap: '10px 12px', marginTop: 10,
        }}>
          {nums.map(c => {
            const value = get(c);
            return (
              <div key={c.key} style={{ minWidth: 0 }}>
                <div style={{
                  fontSize: 10, fontWeight: 700, letterSpacing: 0.5, textTransform: 'uppercase',
                  color: theme.inkMuted, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
                }}>{c.label}</div>
                <div style={{
                  fontSize: 14, marginTop: 2, fontWeight: strong ? 700 : 600, fontVariantNumeric: 'tabular-nums',
                  color: theme.ink, overflowWrap: 'anywhere',
                }}>{value == null || value === '' ? '—' : value}</div>
              </div>
            );
          })}
        </div>
      ) : null}
    </div>
  );
}

// Width of the element itself, not the window: inside the desktop sidebar layout, or the phone-sized
// frame on a tablet, the space a table gets is far narrower than the window.
function useRptWidth(ref) {
  const [w, setW] = React.useState(null);
  React.useEffect(() => {
    const el = ref.current;
    if (!el || typeof ResizeObserver === 'undefined') return undefined;
    const ro = new ResizeObserver(entries => setW(entries[0].contentRect.width));
    ro.observe(el);
    return () => ro.disconnect();
  }, []);
  return w;
}

function RptTable({ theme, columns, rows, footer, empty }) {
  const ref = React.useRef(null);
  const width = useRptWidth(ref);
  const windowNarrow = useNarrow();
  const narrow = width == null ? windowNarrow : width < 640;
  return <div ref={ref} style={{ minWidth: 0 }}>{rptTableBody({ theme, columns, rows, footer, empty, narrow })}</div>;
}

function rptTableBody({ theme, columns, rows, footer, empty, narrow }) {
  if (!rows.length) {
    return <div style={{ fontSize: 13, color: theme.inkMuted, padding: '16px 4px' }}>{empty}</div>;
  }
  if (narrow) {
    // The first column is what the row is about — client, campaign, source — so
    // it becomes the card's heading and the rest become label/value pairs.
    const [head, ...rest] = columns;
    return (
      <div style={{ display: 'grid', gap: 8 }}>
        {rows.map((r, i) => (
          <RptCardRow
            key={r._key || i} theme={theme} columns={rest}
            title={head.render ? head.render(r) : r[head.key]}
            get={c => (c.render ? c.render(r) : r[c.key])}
          />
        ))}
        {footer ? (
          <RptCardRow
            theme={theme} columns={rest} strong
            title={footer[head.key] != null ? footer[head.key] : 'Total'}
            get={c => footer[c.key]}
          />
        ) : null}
      </div>
    );
  }
  const cell = { padding: '8px 10px', borderBottom: `1px solid ${theme.rule}`, whiteSpace: 'nowrap' };
  return (
    <div style={{ overflowX: 'auto', border: `1px solid ${theme.rule}`, borderRadius: theme.radius || 10 }}>
      <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13, color: theme.ink }}>
        <thead>
          <tr>
            {columns.map(c => (
              <th key={c.key} style={{
                ...cell, textAlign: c.align || 'left', fontSize: 10, fontWeight: 700,
                letterSpacing: 0.5, textTransform: 'uppercase', color: theme.inkMuted,
                background: theme.bgElev || 'transparent', position: 'sticky', top: 0,
              }}>{c.label}</th>
            ))}
          </tr>
        </thead>
        <tbody>
          {rows.map((r, i) => (
            <tr key={r._key || i}>
              {columns.map(c => (
                <td key={c.key} style={{
                  ...cell, textAlign: c.align || 'left',
                  fontVariantNumeric: c.align === 'right' ? 'tabular-nums' : 'normal',
                  color: c.muted ? theme.inkMuted : theme.ink,
                }}>{c.render ? c.render(r) : r[c.key]}</td>
              ))}
            </tr>
          ))}
        </tbody>
        {footer ? (
          <tfoot>
            <tr>
              {columns.map(c => (
                <td key={c.key} style={{
                  ...cell, textAlign: c.align || 'left', fontWeight: 700, borderBottom: 'none',
                  fontVariantNumeric: c.align === 'right' ? 'tabular-nums' : 'normal',
                  background: theme.bgElev || 'transparent',
                }}>{footer[c.key] != null ? footer[c.key] : ''}</td>
              ))}
            </tr>
          </tfoot>
        ) : null}
      </table>
    </div>
  );
}

// ── Leads ─────────────────────────────────────────────────────────────────

const LEAD_SOURCES = [
  { v: 'facebook', label: 'Facebook' },
  { v: 'google',   label: 'Google' },
  { v: 'website',  label: 'Website' },
  { v: 'phone',    label: 'Phone' },
  { v: 'referral', label: 'Referral' },
  { v: 'walk_in',  label: 'Walk-in' },
  { v: 'other',    label: 'Other' },
];
const leadSourceLabel = (s) => (LEAD_SOURCES.find(x => x.v === s) || { label: s || '—' }).label;

function LeadsSection({ state, theme, navigate }) {
  const [period, setPeriod]   = React.useState('30d');
  const [clientId, setClient] = React.useState('');
  const [source, setSource]   = React.useState('');
  const [rows, setRows]       = React.useState(null);   // null = still loading
  const [runs, setRuns]       = React.useState([]);
  const [error, setError]     = React.useState(null);

  const win = rptWindow(period);

  React.useEffect(() => {
    let cancelled = false;
    setRows(null); setError(null);
    Promise.all([
      CABT_api.fetchLeads({ from: win.from, to: win.to, clientId: clientId || undefined, source: source || undefined }),
      CABT_api.fetchSyncRuns({ limit: 25 }),
    ])
      .then(([leads, syncRuns]) => { if (!cancelled) { setRows(leads); setRuns(syncRuns); } })
      .catch(e => { if (!cancelled) { setRows([]); setError(e.message || String(e)); } });
    return () => { cancelled = true; };
  }, [period, clientId, source]);

  const clientOptions = [{ v: '', label: 'All clients' }].concat(
    (state.clients || []).slice()
      .sort((a, b) => (a.name || '').localeCompare(b.name || ''))
      .map(c => ({ v: c.id, label: c.name || c.id }))
  );

  const leads = rows || [];
  const count  = (f) => leads.filter(f).length;
  const booked = count(l => l.bookedAt);
  const showed = count(l => l.showedAt);
  const signed = count(l => l.signedAt);
  const pct = (n) => (leads.length ? `${Math.round((n / leads.length) * 100)}%` : '—');

  // By source — the breakdown that did not exist before, and the reason this
  // section was asked for.
  const bySource = LEAD_SOURCES.map(s => {
    const ls = leads.filter(l => l.source === s.v);
    return {
      _key: s.v,
      source: s.label,
      leads: ls.length,
      booked: ls.filter(l => l.bookedAt).length,
      showed: ls.filter(l => l.showedAt).length,
      signed: ls.filter(l => l.signedAt).length,
      share: leads.length ? `${Math.round((ls.length / leads.length) * 100)}%` : '—',
    };
  }).filter(r => r.leads > 0);

  const clientName = (l) => l.client?.name || l.clientId || '—';
  const stage = (l) => l.signedAt ? 'Signed' : l.showedAt ? 'Showed' : l.bookedAt ? 'Booked' : l.lostAt ? 'Lost' : 'New';

  return (
    <div style={RPT_PAGE}>
      <RptHeader theme={theme} label="Leads">
        {win.label}
        {rows === null ? ' · loading…' : ` · ${leads.length} lead${leads.length === 1 ? '' : 's'}`}
      </RptHeader>

      <RptSyncNote theme={theme} runs={runs} source="ghl" label="GoHighLevel sync" />

      <RptControls theme={theme}>
        <RptSelect theme={theme} value={period} onChange={setPeriod} options={RPT_PERIODS} ariaLabel="Period" />
        <RptSelect theme={theme} value={clientId} onChange={setClient} options={clientOptions} ariaLabel="Client" />
        <RptSelect
          theme={theme} value={source} onChange={setSource} ariaLabel="Source"
          options={[{ v: '', label: 'All sources' }].concat(LEAD_SOURCES)}
        />
      </RptControls>

      {error ? (
        <div style={{ fontSize: 13, color: '#C6483C', marginBottom: 12 }}>Could not load leads: {error}</div>
      ) : null}

      <RptStats>
        <RptStat theme={theme} label="Leads"  value={rows === null ? '—' : leads.length} muted={!leads.length}
                 trend={rptSeries(leads, l => l.createdAt, win)} />
        <RptStat theme={theme} label="Booked" value={rows === null ? '—' : booked} sub={pct(booked)} muted={!booked}
                 trend={rptSeries(leads.filter(l => l.bookedAt), l => l.bookedAt, win)} />
        <RptStat theme={theme} label="Showed" value={rows === null ? '—' : showed} sub={pct(showed)} muted={!showed}
                 trend={rptSeries(leads.filter(l => l.showedAt), l => l.showedAt, win)} />
        <RptStat theme={theme} label="Signed" value={rows === null ? '—' : signed} sub={pct(signed)} muted={!signed}
                 trend={rptSeries(leads.filter(l => l.signedAt), l => l.signedAt, win)} />
      </RptStats>

      {rows !== null && !leads.length ? (
        <RptNotConnected
          theme={theme}
          title="No leads recorded yet"
          what="Every lead will carry where it came from — Facebook, Google, the website or the phone — which is the split the old scoreboard never captured. Nothing appears here until the GoHighLevel sync writes its first batch."
          needs="GoHighLevel API access, and a decision on whether we pull at agency level across every sub-account or one sub-account at a time."
        />
      ) : null}

      {bySource.length ? (
        <div style={{ marginBottom: 20 }}>
          <SectionLabel theme={theme}>By source</SectionLabel>
          <RptTable
            theme={theme}
            rows={bySource}
            empty="No leads in this window."
            columns={[
              { key: 'source', label: 'Source' },
              { key: 'leads',  label: 'Leads',  align: 'right' },
              { key: 'share',  label: 'Share',  align: 'right', muted: true },
              { key: 'booked', label: 'Booked', align: 'right' },
              { key: 'showed', label: 'Showed', align: 'right' },
              { key: 'signed', label: 'Signed', align: 'right' },
            ]}
            footer={{
              source: 'Total', leads: leads.length, share: '100%',
              booked, showed, signed,
            }}
          />
        </div>
      ) : null}

      {leads.length ? (
        <div>
          <SectionLabel theme={theme}>All leads</SectionLabel>
          <RptTable
            theme={theme}
            rows={leads.slice(0, 200).map(l => ({ ...l, _key: l.id }))}
            empty="No leads in this window."
            columns={[
              { key: 'created', label: 'Date',   render: l => CABT_fmtDate((l.createdAt || '').slice(0, 10)) },
              { key: 'client',  label: 'Client', render: clientName },
              { key: 'name',    label: 'Name',   render: l => [l.firstName, l.lastName].filter(Boolean).join(' ') || '—' },
              { key: 'source',  label: 'Source', render: l => leadSourceLabel(l.source) },
              { key: 'stage',   label: 'Stage',  render: stage },
              { key: 'origin',  label: 'Origin', muted: true, render: l => (l.confirmedAt ? 'confirmed' : l.origin) },
              { key: 'value',   label: 'Value',  align: 'right', render: l => (l.value != null ? CABT_fmtMoney(l.value) : '—') },
            ]}
          />
          {leads.length > 200 ? (
            <div style={{ fontSize: 11, color: theme.inkMuted, marginTop: 8 }}>
              Showing the 200 most recent of {leads.length}. Narrow the window or pick a client to see the rest.
            </div>
          ) : null}
        </div>
      ) : null}
    </div>
  );
}

// ── Ads ───────────────────────────────────────────────────────────────────

const AD_PLATFORMS = [
  { v: 'meta',   label: 'Meta advertising',   levels: ['campaign', 'adset'] },
  { v: 'google', label: 'Google advertising', levels: ['campaign'] },
];

// The sources Bobby listed that have no numbers behind them yet. Named rather
// than hidden, so the gap between the brief and the build is visible.
// Website (Analytics, Search Console, Business Profile) and Social have had their own sections since
// 2026-09-18, so only what is still unconnected is listed here.
const AD_PENDING_SOURCES = [
  { title: 'Stripe',                     what: 'Revenue beside the spend that produced it.', needs: 'Stripe API key — the scoreboard already reads Stripe customer ids, so the join exists.' },
];

function AdsSection({ state, theme, navigate }) {
  const [platform, setPlatform] = React.useState('meta');
  const [level, setLevel]       = React.useState('campaign');
  const [period, setPeriod]     = React.useState('30d');
  const [clientId, setClient]   = React.useState('');
  const [data, setData]         = React.useState(null);
  const [runs, setRuns]         = React.useState([]);
  const [error, setError]       = React.useState(null);

  const win = rptWindow(period);
  const platformDef = AD_PLATFORMS.find(p => p.v === platform) || AD_PLATFORMS[0];

  // Google has no ad sets, so the sub-tab cannot survive a platform switch.
  React.useEffect(() => {
    if (!platformDef.levels.includes(level)) setLevel('campaign');
  }, [platform]);

  React.useEffect(() => {
    let cancelled = false;
    setData(null); setError(null);
    Promise.all([
      CABT_api.fetchAdMetrics({ from: win.from, to: win.to, clientId: clientId || undefined, level }),
      CABT_api.fetchSyncRuns({ limit: 25 }),
    ])
      .then(([d, syncRuns]) => { if (!cancelled) { setData(d); setRuns(syncRuns); } })
      .catch(e => { if (!cancelled) { setData({ rows: [], accounts: [], campaigns: [], adSets: [] }); setError(e.message || String(e)); } });
    return () => { cancelled = true; };
  }, [period, clientId, level]);

  const clientOptions = [{ v: '', label: 'All clients' }].concat(
    (state.clients || []).slice()
      .sort((a, b) => (a.name || '').localeCompare(b.name || ''))
      .map(c => ({ v: c.id, label: c.name || c.id }))
  );

  const d = data || { rows: [], accounts: [], campaigns: [], adSets: [] };

  // Which accounts belong to this platform, and therefore which campaigns and
  // ad sets the daily rows can point at.
  const platformAccounts = d.accounts.filter(a => a.platform === platform);
  const accountIds = new Set(platformAccounts.map(a => a.id));
  const campaigns = d.campaigns.filter(c => accountIds.has(c.adAccountId));
  const campaignIds = new Set(campaigns.map(c => c.id));
  const adSets = d.adSets.filter(s => campaignIds.has(s.adCampaignId));

  const nameFor = (refId) => {
    if (level === 'campaign') return (campaigns.find(c => c.id === refId) || {}).name || refId;
    if (level === 'adset')    return (adSets.find(s => s.id === refId) || {}).name || refId;
    return (platformAccounts.find(a => a.id === refId) || {}).name || refId;
  };
  const belongs = (refId) => (level === 'adset'
    ? adSets.some(s => s.id === refId)
    : campaigns.some(c => c.id === refId));

  // Days are summed into one row per campaign or ad set. CTR and cost per lead
  // are recomputed from the sums, never averaged from the daily values —
  // averaging a ratio gives a number that is wrong in a way nobody can see.
  const grouped = {};
  d.rows.filter(r => belongs(r.refId)).forEach(r => {
    const g = grouped[r.refId] || (grouped[r.refId] = {
      _key: r.refId, refId: r.refId, spend: 0, impressions: 0, clicks: 0, leads: 0,
    });
    g.spend       += Number(r.spend || 0);
    g.impressions += Number(r.impressions || 0);
    g.clicks      += Number(r.clicks || 0);
    g.leads       += Number(r.leads || 0);
  });

  const rows = Object.values(grouped).map(g => ({
    ...g,
    name: nameFor(g.refId),
    ctr: g.impressions > 0 ? (g.clicks / g.impressions) * 100 : null,
    cpl: g.leads > 0 ? g.spend / g.leads : null,
  })).sort((a, b) => b.spend - a.spend);

  const total = rows.reduce((t, r) => ({
    spend: t.spend + r.spend, impressions: t.impressions + r.impressions,
    clicks: t.clicks + r.clicks, leads: t.leads + r.leads,
  }), { spend: 0, impressions: 0, clicks: 0, leads: 0 });
  const totalCtr = total.impressions > 0 ? (total.clicks / total.impressions) * 100 : null;
  const totalCpl = total.leads > 0 ? total.spend / total.leads : null;

  // "Just at a glance, be like, okay, they're looking good, they need some
  // work" — Mike. The comparison is against this window's own average, so it
  // stays meaningful whatever the client spends.
  const health = (r) => {
    if (totalCpl == null || r.cpl == null) return null;
    if (r.cpl > totalCpl * 1.5) return { label: 'Needs work', color: '#C6483C' };
    if (r.cpl < totalCpl * 0.75) return { label: 'Looking good', color: '#2E7D53' };
    return { label: 'Steady', color: theme.inkMuted };
  };

  const num = (n) => Number(n || 0).toLocaleString();

  return (
    <div style={RPT_PAGE}>
      <RptHeader theme={theme} label="Ad management">
        {win.label}
        {data === null ? ' · loading…' : ` · ${rows.length} ${level === 'adset' ? 'ad set' : 'campaign'}${rows.length === 1 ? '' : 's'}`}
      </RptHeader>

      <RptSyncNote
        theme={theme} runs={runs}
        source={platform === 'meta' ? 'meta' : 'google_ads'}
        label={`${platformDef.label} sync`}
      />

      <Tabs
        theme={theme} value={platform} onChange={setPlatform}
        tabs={AD_PLATFORMS.map(p => ({ value: p.v, label: p.label }))}
      />

      <div style={{ height: 12 }} />

      <RptControls theme={theme}>
        {platformDef.levels.length > 1 ? (
          <RptSelect
            theme={theme} value={level} onChange={setLevel} ariaLabel="Level"
            options={[{ v: 'campaign', label: 'By campaign' }, { v: 'adset', label: 'By ad set' }]}
          />
        ) : null}
        <RptSelect theme={theme} value={period} onChange={setPeriod} options={RPT_PERIODS} ariaLabel="Period" />
        <RptSelect theme={theme} value={clientId} onChange={setClient} options={clientOptions} ariaLabel="Client" />
      </RptControls>

      {error ? (
        <div style={{ fontSize: 13, color: '#C6483C', marginBottom: 12 }}>Could not load ad metrics: {error}</div>
      ) : null}

      <RptStats>
        <RptStat theme={theme} label="Spend" value={data === null ? '—' : CABT_fmtMoney(total.spend)} muted={!total.spend}
                 trend={rptSeries(d.rows.filter(r => belongs(r.refId)), r => r.day, win, r => Number(r.spend || 0))} />
        <RptStat theme={theme} label="Leads" value={data === null ? '—' : num(total.leads)} muted={!total.leads}
                 trend={rptSeries(d.rows.filter(r => belongs(r.refId)), r => r.day, win, r => Number(r.leads || 0))} />
        <RptStat theme={theme} label="Cost / lead" value={totalCpl == null ? '—' : CABT_fmtMoney(totalCpl)} muted={totalCpl == null} />
        <RptStat theme={theme} label="CTR" value={totalCtr == null ? '—' : `${totalCtr.toFixed(2)}%`} sub={totalCtr == null ? null : `${num(total.clicks)} clicks`} muted={totalCtr == null} />
      </RptStats>

      {data !== null && !rows.length ? (
        <RptNotConnected
          theme={theme}
          title={`No ${platformDef.label.toLowerCase()} data yet`}
          what={platform === 'meta'
            ? 'Campaign and ad set performance — spend, impressions, clicks, CTR, leads and cost per lead — summed over whatever window you pick. Numbers appear once the Facebook sync runs.'
            : 'Campaign performance from Google Ads, in the same shape as Meta so the two can be read side by side.'}
          needs={platform === 'meta'
            ? 'Which Facebook ad accounts we pull, and who grants access to them.'
            : 'Google Ads account access, and whether Google Analytics is combined into this section or kept separate.'}
        />
      ) : null}

      {rows.length ? (
        <RptTable
          theme={theme}
          rows={rows}
          empty="Nothing in this window."
          columns={[
            { key: 'name',        label: level === 'adset' ? 'Ad set' : 'Campaign' },
            { key: 'health',      label: 'At a glance', render: r => {
              const h = health(r);
              return h ? <span style={{ color: h.color, fontWeight: 600 }}>{h.label}</span> : '—';
            } },
            { key: 'spend',       label: 'Spend',       align: 'right', render: r => CABT_fmtMoney(r.spend) },
            { key: 'impressions', label: 'Impressions', align: 'right', render: r => num(r.impressions) },
            { key: 'clicks',      label: 'Clicks',      align: 'right', render: r => num(r.clicks) },
            { key: 'ctr',         label: 'CTR',         align: 'right', render: r => (r.ctr == null ? '—' : `${r.ctr.toFixed(2)}%`) },
            { key: 'leads',       label: 'Leads',       align: 'right', render: r => num(r.leads) },
            { key: 'cpl',         label: 'Cost / lead', align: 'right', render: r => (r.cpl == null ? '—' : CABT_fmtMoney(r.cpl)) },
          ]}
          footer={{
            name: 'Total', health: '',
            spend: CABT_fmtMoney(total.spend), impressions: num(total.impressions),
            clicks: num(total.clicks), ctr: totalCtr == null ? '—' : `${totalCtr.toFixed(2)}%`,
            leads: num(total.leads), cpl: totalCpl == null ? '—' : CABT_fmtMoney(totalCpl),
          }}
        />
      ) : null}

      <div style={{ marginTop: 28 }}>
        <SectionLabel theme={theme}>Other sources</SectionLabel>
        <div style={{ display: 'grid', gap: 10 }}>
          {AD_PENDING_SOURCES.map(s => (
            <RptNotConnected key={s.title} theme={theme} title={s.title} what={s.what} needs={s.needs} />
          ))}
        </div>
      </div>
    </div>
  );
}


// == Website ==============================================================
// Analytics, Search Console and the Google listing read together.
//
//   "There should be something for Semrush or Google Analytics, or in that case
//   maybe certain things are combined, so we don't have the same information
//   twice." - Bobby [3:56:36]
//
// Combined is the point: a visit from Google is one visit whether Analytics or
// Search Console counted it. What stays apart is what the visitor did - a tap on
// "call" or "directions" from the listing is a person heading for the door, and
// folding that into pageviews would bury it.

function WebSection({ state, theme, navigate }) {
  const [period, setPeriod]   = React.useState('30d');
  const [clientId, setClient] = React.useState('');
  const [data, setData]       = React.useState(null);
  const [error, setError]     = React.useState(null);

  const win = rptWindow(period);

  React.useEffect(() => {
    let cancelled = false;
    setData(null); setError(null);
    CABT_api.fetchWebMetrics({ from: win.from, to: win.to, clientId: clientId || undefined })
      .then(function (d) { if (!cancelled) setData(d); })
      .catch(function (e) { if (!cancelled) { setData({ rows: [], sources: [] }); setError(e.message || String(e)); } });
    return function () { cancelled = true; };
  }, [period, clientId]);

  const clientOptions = [{ v: '', label: 'All clients' }].concat(
    (state.clients || []).slice()
      .sort(function (a, b) { return (a.name || '').localeCompare(b.name || ''); })
      .map(function (c) { return { v: c.id, label: c.name || c.id }; })
  );

  const d = data || { rows: [], sources: [] };
  const nameOf = function (id) {
    const found = (state.clients || []).find(function (c) { return c.id === id; });
    return (found && found.name) || id;
  };

  const total = d.rows.reduce(function (t, r) {
    return {
      sessions: t.sessions + (r.sessions || 0),
      users: t.users + (r.users || 0),
      searchClicks: t.searchClicks + (r.searchClicks || 0),
      searchImpressions: t.searchImpressions + (r.searchImpressions || 0),
      mapViews: t.mapViews + (r.mapViews || 0),
      calls: t.calls + (r.listingCalls || 0),
      directions: t.directions + (r.listingDirections || 0),
    };
  }, { sessions: 0, users: 0, searchClicks: 0, searchImpressions: 0, mapViews: 0, calls: 0, directions: 0 });

  // Where everyone came from, not only the ones who filled a form in.
  const bySource = {};
  d.sources.forEach(function (r) {
    const key = r.source + ' / ' + r.medium;
    const at = bySource[key] || (bySource[key] = { _key: key, source: r.source, medium: r.medium, sessions: 0, conversions: 0 });
    at.sessions += r.sessions || 0;
    at.conversions += r.conversions || 0;
  });
  const sourceRows = Object.values(bySource).sort(function (a, b) { return b.sessions - a.sessions; });
  const sourceTotal = sourceRows.reduce(function (t, r) { return t + r.sessions; }, 0);

  const byClient = {};
  d.rows.forEach(function (r) {
    const at = byClient[r.clientId] || (byClient[r.clientId] = {
      _key: r.clientId, clientId: r.clientId, sessions: 0, users: 0,
      searchClicks: 0, mapViews: 0, calls: 0, directions: 0,
    });
    at.sessions += r.sessions || 0;
    at.users += r.users || 0;
    at.searchClicks += r.searchClicks || 0;
    at.mapViews += r.mapViews || 0;
    at.calls += r.listingCalls || 0;
    at.directions += r.listingDirections || 0;
  });
  const clientRows = Object.values(byClient).sort(function (a, b) { return b.sessions - a.sessions; });

  const share = function (n) { return sourceTotal ? Math.round((n / sourceTotal) * 100) + '%' : '-'; };

  return (
    <div style={RPT_PAGE}>
      <RptHeader theme={theme} label="Website">{win.label}</RptHeader>
      <RptControls theme={theme}>
        <RptSelect theme={theme} value={period} onChange={setPeriod} options={RPT_PERIODS} ariaLabel="Period" />
        <RptSelect theme={theme} value={clientId} onChange={setClient} options={clientOptions} ariaLabel="Client" />
      </RptControls>

      {error ? <div style={{ fontSize: 12, color: '#C6483C', marginBottom: 10 }}>{error}</div> : null}

      <RptStats>
        <RptStat theme={theme} label="Sessions" value={total.sessions.toLocaleString()}
                 trend={rptSeries(d.rows, r => r.day, win, r => Number(r.sessions || 0))} />
        <RptStat theme={theme} label="People" value={total.users.toLocaleString()}
                 trend={rptSeries(d.rows, r => r.day, win, r => Number(r.users || 0))} />
        <RptStat theme={theme} label="From search" value={total.searchClicks.toLocaleString()}
                 sub={total.searchImpressions ? total.searchImpressions.toLocaleString() + ' impressions' : null} />
        <RptStat theme={theme} label="Map views" value={total.mapViews.toLocaleString()}
                 trend={rptSeries(d.rows, r => r.day, win, r => Number(r.mapViews || 0))} />
        <RptStat theme={theme} label="Calls" value={total.calls.toLocaleString()} sub="from the listing" />
        <RptStat theme={theme} label="Directions" value={total.directions.toLocaleString()} sub="from the listing" />
      </RptStats>

      <h3 style={{ fontSize: 13, fontWeight: 700, color: theme.ink, margin: '18px 0 8px' }}>
        Where they came from
      </h3>
      <RptTable
        theme={theme}
        columns={[
          { key: 'source', label: 'Source' },
          { key: 'medium', label: 'Medium', muted: true },
          { key: 'sessions', label: 'Sessions', align: 'right', render: function (r) { return r.sessions.toLocaleString(); } },
          { key: 'share', label: 'Share', align: 'right', render: function (r) { return share(r.sessions); } },
          { key: 'conversions', label: 'Conversions', align: 'right', render: function (r) { return (r.conversions || 0).toLocaleString(); } },
        ]}
        rows={sourceRows}
        empty={data ? 'Nothing in this window.' : 'Loading...'}
      />

      <h3 style={{ fontSize: 13, fontWeight: 700, color: theme.ink, margin: '22px 0 8px' }}>
        By client
      </h3>
      <RptTable
        theme={theme}
        columns={[
          { key: 'client', label: 'Client', render: function (r) { return nameOf(r.clientId); } },
          { key: 'sessions', label: 'Sessions', align: 'right', render: function (r) { return r.sessions.toLocaleString(); } },
          { key: 'users', label: 'People', align: 'right', render: function (r) { return r.users.toLocaleString(); } },
          { key: 'searchClicks', label: 'Search', align: 'right', render: function (r) { return r.searchClicks.toLocaleString(); } },
          { key: 'mapViews', label: 'Map views', align: 'right', render: function (r) { return r.mapViews.toLocaleString(); } },
          { key: 'calls', label: 'Calls', align: 'right', render: function (r) { return r.calls.toLocaleString(); } },
          { key: 'directions', label: 'Directions', align: 'right', render: function (r) { return r.directions.toLocaleString(); } },
        ]}
        rows={clientRows}
        empty={data ? 'Nothing in this window.' : 'Loading...'}
      />

      <p style={{ fontSize: 11, color: theme.inkMuted, marginTop: 12, lineHeight: 1.6 }}>
        Sessions and search are the same visit counted by two tools, so they are read side by
        side rather than added. Calls and directions come from the Google listing, where
        someone acts without ever reaching the website.
      </p>
    </div>
  );
}

// == Social ===============================================================
// Per platform, because Bobby asked for "social media" beside the paid numbers
// and a follower on Instagram is not a follower on Facebook.

function SocialSection({ state, theme, navigate }) {
  const [period, setPeriod]   = React.useState('30d');
  const [clientId, setClient] = React.useState('');
  const [data, setData]       = React.useState(null);
  const [error, setError]     = React.useState(null);

  const win = rptWindow(period);

  React.useEffect(() => {
    let cancelled = false;
    setData(null); setError(null);
    CABT_api.fetchSocialMetrics({ from: win.from, to: win.to, clientId: clientId || undefined })
      .then(function (d) { if (!cancelled) setData(d); })
      .catch(function (e) { if (!cancelled) { setData({ rows: [] }); setError(e.message || String(e)); } });
    return function () { cancelled = true; };
  }, [period, clientId]);

  const clientOptions = [{ v: '', label: 'All clients' }].concat(
    (state.clients || []).slice()
      .sort(function (a, b) { return (a.name || '').localeCompare(b.name || ''); })
      .map(function (c) { return { v: c.id, label: c.name || c.id }; })
  );

  const rows = (data || { rows: [] }).rows;
  const nameOf = function (id) {
    const found = (state.clients || []).find(function (c) { return c.id === id; });
    return (found && found.name) || id;
  };

  // Followers is a level, not a total. The newest day in the window is the
  // number and the oldest is what it grew from; summing daily follower counts
  // would report a gym of two hundred as a gym of six thousand.
  const byPair = {};
  rows.forEach(function (r) {
    const key = r.clientId + '::' + r.platform;
    const at = byPair[key] || (byPair[key] = {
      _key: key, clientId: r.clientId, platform: r.platform,
      first: null, last: null, reach: 0, impressions: 0, engaged: 0,
    });
    at.reach += r.reach || 0;
    at.impressions += r.impressions || 0;
    at.engaged += r.engaged || 0;
    if (!at.first || r.day < at.first.day) at.first = r;
    if (!at.last || r.day > at.last.day) at.last = r;
  });

  const pairRows = Object.values(byPair).map(function (p) {
    const now = p.last ? (p.last.followers || 0) : 0;
    const then = p.first ? (p.first.followers || 0) : 0;
    return Object.assign({}, p, { followers: now, growth: now - then });
  }).sort(function (a, b) { return b.followers - a.followers; });

  const platforms = {};
  pairRows.forEach(function (r) {
    const at = platforms[r.platform] || (platforms[r.platform] = { followers: 0, reach: 0, engaged: 0, impressions: 0, growth: 0 });
    at.growth += r.growth;
    at.followers += r.followers;
    at.reach += r.reach;
    at.engaged += r.engaged;
    at.impressions += r.impressions;
  });
  const fb = platforms.facebook || { followers: 0, impressions: 0 };
  const ig = platforms.instagram || { followers: 0, reach: 0 };

  return (
    <div style={RPT_PAGE}>
      <RptHeader theme={theme} label="Social">{win.label}</RptHeader>
      <RptControls theme={theme}>
        <RptSelect theme={theme} value={period} onChange={setPeriod} options={RPT_PERIODS} ariaLabel="Period" />
        <RptSelect theme={theme} value={clientId} onChange={setClient} options={clientOptions} ariaLabel="Client" />
      </RptControls>

      {error ? <div style={{ fontSize: 12, color: '#C6483C', marginBottom: 10 }}>{error}</div> : null}

      <RptStats>
        <RptStat theme={theme} label="Facebook followers" value={fb.followers.toLocaleString()}
                 delta={fb.growth} deltaLabel={`over ${win.label.toLowerCase()}`}
                 trend={rptSeries(rows.filter(r => r.platform === 'facebook'), r => r.day, win, r => Number(r.followers || 0))} />
        <RptStat theme={theme} label="Facebook reached" value={fb.impressions.toLocaleString()}
                 trend={rptSeries(rows.filter(r => r.platform === 'facebook'), r => r.day, win, r => Number(r.impressions || 0))} />
        <RptStat theme={theme} label="Instagram followers" value={ig.followers.toLocaleString()}
                 delta={ig.growth} deltaLabel={`over ${win.label.toLowerCase()}`}
                 trend={rptSeries(rows.filter(r => r.platform === 'instagram'), r => r.day, win, r => Number(r.followers || 0))} />
        <RptStat theme={theme} label="Instagram reach" value={ig.reach.toLocaleString()}
                 trend={rptSeries(rows.filter(r => r.platform === 'instagram'), r => r.day, win, r => Number(r.reach || 0))} />
      </RptStats>

      <RptTable
        theme={theme}
        columns={[
          { key: 'client', label: 'Client', render: function (r) { return nameOf(r.clientId); } },
          { key: 'platform', label: 'Platform', render: function (r) { return r.platform === 'facebook' ? 'Facebook' : 'Instagram'; } },
          { key: 'followers', label: 'Followers', align: 'right', render: function (r) { return r.followers.toLocaleString(); } },
          { key: 'growth', label: 'Change', align: 'right', render: function (r) { return r.growth > 0 ? '+' + r.growth : String(r.growth); } },
          { key: 'reached', label: 'Reached', align: 'right', render: function (r) { return (r.platform === 'facebook' ? r.impressions : r.reach).toLocaleString(); } },
          { key: 'engaged', label: 'Engaged', align: 'right', render: function (r) { return r.engaged.toLocaleString(); } },
        ]}
        rows={pairRows}
        empty={data ? 'Nothing in this window.' : 'Loading...'}
      />

      <p style={{ fontSize: 11, color: theme.inkMuted, marginTop: 12, lineHeight: 1.6 }}>
        Followers is the count on the last day of the window, and Change is the difference from
        the first - a follower count is a level, not something to add up day by day.
      </p>
    </div>
  );
}

Object.assign(window, {
  LeadsSection, AdsSection, WebSection, SocialSection,
  RPT_PERIODS, rptWindow, LEAD_SOURCES, AD_PLATFORMS,
  RptStat, RptTable, RptNotConnected, RptSyncNote, RptSelect,
});
