// Project Space → "Traffic" tab (migrations 0070/0071). Visitor numbers for
// the tenant's website(s), collected on the web box from the server's own
// access logs and posted nightly — no tracking script on their site, no
// third-party analytics, nothing runs in their visitors' browsers.
//
// Rendered as THE REPORT (owner's ask 2026-09-06: "exactly like the PDF
// reports"): the same paper-white document the manual traffic report
// prints — same tiles, same line chart, same section heads, same prose
// rules, same device bars, same day table, same limitations list. It is a
// document, so it keeps its own ink/paper palette in every app theme.
//
// Honest by construction: every number is a server-side ESTIMATE and the
// document says so in the same words the PDF does.

// Report palette — fixed, not theme tokens (it's paper).
const RPT = { BLUE: '#2a78d6', ORANGE: '#eb6834', INK: '#0b0b0b', INK2: '#52514e', INK3: '#7a7873',
              GRID: '#e6e4df', SURFACE: '#ffffff', BAND: '#f0efec' };
const RPT_FONT = '-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif';
const RS = {
    wrap:  { background: RPT.SURFACE, color: RPT.INK, fontFamily: RPT_FONT, fontSize: 14, lineHeight: 1.55,
             maxWidth: 760, padding: '28px 24px 40px', borderRadius: 8, border: '1px solid var(--border)' },
    h1:    { fontSize: 21, margin: '0 0 2px', letterSpacing: '-.01em', fontWeight: 700, color: RPT.INK },
    h2:    { fontSize: 13, margin: '30px 0 4px', letterSpacing: '.14em', textTransform: 'uppercase', color: RPT.INK2, fontWeight: 600 },
    sub:   { color: RPT.INK2, fontSize: 13, margin: 0 },
    rule:  { height: 1, background: RPT.GRID, margin: '16px 0 0' },
    stats: { display: 'flex', gap: 10, margin: '18px 0 4px', flexWrap: 'wrap' },
    stat:  { flex: 1, minWidth: 120, border: `1px solid ${RPT.GRID}`, borderRadius: 8, padding: '10px 12px' },
    statB: { display: 'block', fontSize: 23, letterSpacing: '-.02em', lineHeight: 1.15, fontWeight: 700 },
    statS: { fontSize: 10.5, color: RPT.INK2, letterSpacing: '.04em', textTransform: 'uppercase' },
    legend:{ display: 'flex', gap: 18, fontSize: 12, color: RPT.INK2, margin: '2px 0 6px', flexWrap: 'wrap' },
    key:   { width: 9, height: 9, borderRadius: 2, display: 'inline-block', marginRight: 6 },
    p:     { margin: '8px 0' },
    note:  { fontSize: 12, color: RPT.INK2 },
    table: { borderCollapse: 'collapse', width: '100%', fontSize: 12, marginTop: 6 },
    th:    { textAlign: 'right', padding: '3.5px 6px', borderBottom: `1px solid ${RPT.GRID}`, color: RPT.INK2, fontWeight: 600, fontSize: 11, letterSpacing: '.06em', textTransform: 'uppercase' },
    td:    { textAlign: 'right', padding: '3.5px 6px', borderBottom: `1px solid ${RPT.GRID}` },
};
const tickStyle = { fontSize: 10, fill: RPT.INK3, fontFamily: 'inherit' };
const valStyle  = { fontSize: 11, fill: RPT.INK, fontWeight: 600, fontFamily: 'inherit' };

const fmtMonth = (ym) => { const [y, m] = ym.split('-').map(Number); return new Date(y, m - 1, 1).toLocaleDateString(undefined, { month: 'long', year: 'numeric' }); };
const thisMonth = () => new Date().toISOString().slice(0, 7);
const shiftMonth = (ym, delta) => { const [y, m] = ym.split('-').map(Number); const d = new Date(y, m - 1 + delta, 1); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`; };
const pretty = (iso) => new Date(iso + 'T12:00:00').toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
const fmtN = (n) => (n || 0).toLocaleString();
const isoAdd = (iso, n) => { const d = new Date(iso + 'T12:00:00'); d.setDate(d.getDate() + n); return d.toISOString().slice(0, 10); };

// The report's time series: visitors per day with the social subset beneath
// it, one shared axis (two units would mean two charts — never a second
// y-scale). Direct port of the PDF's chart, including the peak marker.
const TimeChart = ({ days, mark }) => {
    const W = 660, H = 268, PADL = 32, PADB = 30, PADT = 32;
    const n = days.length;
    let top = Math.max(...days.map(d => d.visits), 10); top = Math.ceil(top / 10) * 10;
    const step = top <= 50 ? 10 : top <= 120 ? 20 : 50;
    const ph = H - PADB - PADT, pw = W - PADL - 10;
    const x = (i) => PADL + (pw * i / Math.max(n - 1, 1));
    const y = (v) => PADT + ph - (v / top) * ph;
    const path = (key) => days.map((d, i) => `${i === 0 ? 'M' : 'L'}${x(i).toFixed(1)},${y(d[key] || 0).toFixed(1)}`).join(' ');
    const hasSocial = days.some(d => d.social_visits > 0);
    const grid = []; for (let gv = 0; gv <= top; gv += step) grid.push(gv);
    const mi = Math.max(0, days.findIndex(d => d.date === mark));
    const every = Math.max(1, Math.round(n / 9));
    const labels = []; let lastX = -99;
    days.forEach((d, i) => {
        if ((i % every === 0 || i === n - 1) && (x(i) - lastX > 56 || i === n - 1)) {
            if (i === n - 1 && x(i) - lastX <= 56) labels.pop();
            lastX = x(i);
            labels.push({ i, text: pretty(d.date), anchor: i === n - 1 ? 'end' : i === 0 ? 'start' : 'middle' });
        }
    });
    return (
        <svg viewBox={`0 0 ${W} ${H}`} width="100%" role="img" aria-label="Unique visitors per day over time" style={{ display: 'block' }}>
            {grid.map(gv => (
                <g key={gv}>
                    <line x1={PADL} x2={W - 6} y1={y(gv)} y2={y(gv)} stroke={RPT.GRID} strokeWidth="1" />
                    <text x={PADL - 7} y={y(gv) + 3.5} textAnchor="end" style={tickStyle}>{gv}</text>
                </g>
            ))}
            {hasSocial && <path d={path('social_visits')} fill="none" stroke={RPT.ORANGE} strokeWidth="2" strokeLinejoin="round" strokeLinecap="round" />}
            <path d={path('visits')} fill="none" stroke={RPT.BLUE} strokeWidth="2" strokeLinejoin="round" strokeLinecap="round" />
            {n > 0 && (
                <g>
                    <circle cx={x(mi)} cy={y(days[mi].visits)} r="4.5" fill={RPT.BLUE} stroke={RPT.SURFACE} strokeWidth="2" />
                    <text x={x(mi)} y={y(days[mi].visits) - 10} textAnchor="middle" style={valStyle}>{days[mi].visits}</text>
                </g>
            )}
            {labels.map(l => <text key={l.i} x={x(l.i)} y={H - 12} textAnchor={l.anchor} style={tickStyle}>{l.text}</text>)}
            <line x1={PADL} x2={W - 6} y1={y(0)} y2={y(0)} stroke={RPT.INK3} strokeWidth="1" />
        </svg>
    );
};

// Horizontal bars — one series, so no legend; the title names it.
const DeviceBars = ({ devices }) => {
    const tot = devices.reduce((a, d) => a + d.n, 0) || 1;
    const rh = 26;
    return (
        <svg viewBox={`0 0 660 ${10 + devices.length * rh}`} width="100%" role="img" aria-label="Device share" style={{ display: 'block' }}>
            {devices.map((d, i) => {
                const pct = d.n / tot * 100, yy = 4 + i * rh, w = Math.max(pct * 4.6, 2);
                return (
                    <g key={d.name}>
                        <text x="0" y={yy + 13} style={{ fontSize: 11, fill: RPT.INK2, fontFamily: 'inherit' }}>{d.name}</text>
                        <rect x="78" y={yy + 3} width={w} height="13" fill={RPT.BLUE} rx="4" />
                        <text x={78 + w + 7} y={yy + 13} style={valStyle}>{pct.toFixed(0)}%</text>
                    </g>
                );
            })}
        </svg>
    );
};

const TrafficTab = ({ tenant }) => {
    const [month, setMonth] = React.useState(thisMonth());
    const [data, setData]   = React.useState(null);     // last good payload
    const [error, setError] = React.useState(null);
    const [siteId, setSiteId] = React.useState(null);   // picked site when there are several

    React.useEffect(() => {
        let live = true;
        api.getTraffic(month)
            .then(d => { if (!live) return; setData(d); setError(null); })
            .catch(err => { if (live) setError(err.message); });
        return () => { live = false; };
    }, [month]);

    if (error && !data) return <LoadErrorBanner what="traffic" hasData={false} onRetry={() => setMonth(m => m)} />;
    if (!data) return <p style={{ color: 'var(--text-3)', fontSize: '0.875rem' }}>Loading…</p>;
    if (!data.can_view) return <p style={{ color: 'var(--text-3)', fontSize: '0.875rem' }}>This page hasn't been shared with you.</p>;

    const NOTE_SOURCE = <li><b>Source.</b> The web server writes one line for every file it sends — the time, the visitor's address, the page, and the site that referred them. That log is the report. Nothing was added to the site and nothing is tracked across the wider web: no third-party analytics service, no cookie banner.</li>;
    const NOTE_EST = <li><b>"Visitors" is an estimate.</b> It counts distinct network addresses per day. A household on one connection reads as one visitor; a phone moving between wifi and cellular reads as two. It is a reliable way to see a change in demand, not an exact headcount — and some of the traffic is our own monitoring.</li>;

    // Empty state: no site registered at all, or none has reported yet.
    if (data.sites.length === 0 || data.months.length === 0) {
        return (
            <div style={{ maxWidth: 640 }}>
                <p style={{ fontSize: '0.875rem', color: 'var(--text-2)' }}>
                    {data.sites.length === 0
                        ? 'No website is connected to traffic reporting yet. Once your service provider wires your site in, daily visitor numbers land here on their own.'
                        : 'Your site is connected — the first day\'s numbers arrive after tonight\'s collection run.'}
                </p>
                <ul style={{ ...RS.note, color: 'var(--text-3)', paddingLeft: '1.2rem' }}>{NOTE_SOURCE}{NOTE_EST}</ul>
            </div>
        );
    }

    const sites = data.sites;
    const site  = sites.find(s => s.id === siteId) || sites[0];
    const earliest = data.months[0], latest = data.months[data.months.length - 1];
    const canBack = month > earliest, canFwd = month < latest || month < thisMonth();

    // Continuous day list from first to last reported day (a missing day
    // inside the window reads as 0, like the report's merged timeline).
    let days = [];
    if (site.days.length) {
        const byDate = Object.fromEntries(site.days.map(d => [d.date, d]));
        for (let d = site.days[0].date; d <= site.days[site.days.length - 1].date; d = isoAdd(d, 1)) {
            days.push(byDate[d] || { date: d, visits: 0, page_views: 0, social_visits: 0 });
        }
    }
    const reported = site.days.length;
    const sortedV = site.days.map(d => d.visits).sort((a, b) => a - b);
    const typical = sortedV.length ? sortedV[Math.floor(sortedV.length / 2)] : 0;   // median, the report's "typical day"
    const peak = site.days.reduce((b, d) => (!b || d.visits > b.visits ? d : b), null);
    const socTotal = site.social_visits >= 10 ? site.social_visits : 0;              // below 10 the subset isn't worth a series
    const range = days.length ? `${pretty(days[0].date)}–${pretty(days[days.length - 1].date)}, ${days[0].date.slice(0, 4)}` : fmtMonth(month);
    const today = new Date().toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' });
    const mult = typical && peak ? peak.visits / typical : 0;

    const Nav = (
        <div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', flexWrap: 'wrap', marginBottom: '0.75rem' }}>
            <div className="seg-control">
                <button type="button" className="seg-btn" disabled={!canBack} onClick={() => setMonth(m => shiftMonth(m, -1))} title="Previous month"><i className="fas fa-chevron-left"></i></button>
                <span className="seg-btn on" style={{ cursor: 'default', minWidth: 150, justifyContent: 'center' }}>{fmtMonth(month)}</span>
                <button type="button" className="seg-btn" disabled={!canFwd} onClick={() => setMonth(m => shiftMonth(m, +1))} title="Next month"><i className="fas fa-chevron-right"></i></button>
            </div>
            {sites.length > 1 && (
                <div style={{ display: 'flex', gap: '0.4rem', flexWrap: 'wrap' }}>
                    {sites.map(s => (
                        <button key={s.id} type="button" className={`filter-chip ${s.id === site.id ? 'active' : ''}`}
                                onClick={() => setSiteId(s.id)} title={s.hostname}>{s.label}</button>
                    ))}
                </div>
            )}
        </div>
    );

    if (reported === 0) {
        return (
            <div>
                {Nav}
                <p style={{ fontSize: '0.875rem', color: 'var(--text-3)' }}>No numbers for {fmtMonth(month)}.</p>
            </div>
        );
    }

    return (
        <div>
            {error && <LoadErrorBanner what="traffic" hasData onRetry={() => setMonth(m => m)} />}
            {Nav}
            <div style={RS.wrap}>
                <h1 style={RS.h1}>{tenant?.name || site.label} — website traffic</h1>
                <p style={RS.sub}>{site.hostname} · {range} · prepared {today}</p>
                <div style={RS.rule}></div>

                <div style={RS.stats}>
                    <div style={RS.stat}><b style={RS.statB}>{fmtN(site.visits)}</b><span style={RS.statS}>Visits, {reported} day{reported === 1 ? '' : 's'}</span></div>
                    <div style={RS.stat}><b style={RS.statB}>{fmtN(typical)}</b><span style={RS.statS}>Typical day</span></div>
                    <div style={RS.stat}><b style={RS.statB}>{fmtN(peak.visits)}</b><span style={RS.statS}>Busiest ({pretty(peak.date)})</span></div>
                    {socTotal
                        ? <div style={RS.stat}><b style={RS.statB}>{fmtN(socTotal)}</b><span style={RS.statS}>From social</span></div>
                        : <div style={RS.stat}><b style={RS.statB}>{fmtN(site.page_views)}</b><span style={RS.statS}>Page views</span></div>}
                </div>

                <h2 style={RS.h2}>Visitors per day</h2>
                <div style={RS.legend}>
                    <span><i style={{ ...RS.key, background: RPT.BLUE }}></i>Visitors</span>
                    {socTotal > 0 && <span><i style={{ ...RS.key, background: RPT.ORANGE }}></i>From social</span>}
                </div>
                <TimeChart days={socTotal ? days : days.map(d => ({ ...d, social_visits: 0 }))} mark={peak.date} />

                <h2 style={RS.h2}>Reading the line</h2>
                {socTotal > 0 && peak.social_visits > 0 ? (
                    <p style={RS.p}>
                        {pretty(peak.date)} drew {peak.visits} visitors against a typical day of about {typical}{mult ? ` — roughly ${mult.toFixed(1)}× normal` : ''}. {peak.social_visits} arrived
                        straight from a social link. That is the shape a post doing well makes from the server's side: a sharp first day, a smaller
                        second, then a slide back to baseline. Across the window {socTotal} visits trace to social. The traffic is real and
                        measurable, and it is also perishable — its worth is whatever it converts into while it lasts.
                    </p>
                ) : (
                    <p style={RS.p}>
                        A typical day is about {typical} visitors and the busiest day in this window was {pretty(peak.date)} at {peak.visits}.
                        {socTotal ? ` ${socTotal} visits across the window trace to social.` : ' There is no social spike in this data: what traffic the site gets arrives steadily rather than in bursts.'}
                    </p>
                )}

                {reported >= 5 && site.devices.length > 0 && (
                    <>
                        <h2 style={RS.h2}>How visitors arrive</h2>
                        <DeviceBars devices={site.devices} />
                        <p style={{ ...RS.p, ...RS.note }}>A phone-heavy audience is the signature of social traffic: people tap a link inside an app, not at a desk. It is also the practical case for designing anything new phone-first.</p>
                    </>
                )}

                {site.top_pages.length > 0 && (
                    <>
                        <h2 style={RS.h2}>Most-visited pages</h2>
                        <table style={RS.table}>
                            <thead><tr><th style={{ ...RS.th, textAlign: 'left' }}>Page</th><th style={RS.th}>Views</th></tr></thead>
                            <tbody>{site.top_pages.map(p => <tr key={p.path}><td style={{ ...RS.td, textAlign: 'left', fontFamily: 'ui-monospace, Menlo, monospace', fontSize: 11 }}>{p.path}</td><td style={RS.td}>{fmtN(p.views)}</td></tr>)}</tbody>
                        </table>
                    </>
                )}

                <h2 style={RS.h2}>The numbers</h2>
                <table style={RS.table}>
                    <thead><tr><th style={{ ...RS.th, textAlign: 'left' }}>Day</th><th style={RS.th}>Visitors</th>{socTotal > 0 && <th style={RS.th}>From social</th>}</tr></thead>
                    <tbody>
                        {days.map(d => (
                            <tr key={d.date}>
                                <td style={{ ...RS.td, textAlign: 'left' }}>{pretty(d.date)}</td>
                                <td style={RS.td}>{d.visits || '—'}</td>
                                {socTotal > 0 && <td style={RS.td}>{d.social_visits || '—'}</td>}
                            </tr>
                        ))}
                    </tbody>
                </table>

                <h2 style={RS.h2}>Where this comes from, and what it can't tell you</h2>
                <ul style={{ ...RS.note, paddingLeft: '1.2rem' }}>
                    {NOTE_SOURCE}
                    {NOTE_EST}
                    {socTotal > 0 && <li><b>Attribution is by referrer.</b> Social apps tag outbound taps, which is how those visits are identified. Anyone who typed the address in, or came via an app that strips the tag, lands in the general count — so the social figure is a floor, not a ceiling.</li>}
                    <li><b>Updated nightly.</b> Each day's numbers arrive the following morning; the month fills in as it goes.</li>
                </ul>
            </div>
        </div>
    );
};
