// Chart Growth Hub — Student Trading Journal (premium).
//
// Table: journal_entries (+ lot_size, stop_loss, take_profit, strategy_tag,
// session_tag, emotion, followed_plan, signal_id). RLS: a student reads/writes
// only their own rows; staff have a SELECT-only policy so a coach can review
// but can never edit or delete a student's record of their own trades.
//
// The "Private note" lives in a separate table (journal_private) with no staff
// policy at all. RLS cannot be scoped to one column, so the only way to keep a
// note out of a staff `select *` is to keep it out of the row entirely.
//
// Logging is open to every student. VIP gates the pro analytics block and the
// branded PDF export — that gating is presentational only, enforced here.
//
// Exposes window.JournalPanel({client, session, isVIP, onNeedVip, member,
//                              prefill, onPrefillUsed}).
(function(){
const { useState, useEffect, useMemo, useRef } = React;

/* ── constants ───────────────────────────────────────────────────────────── */

const STRATS = [
  {v:'A',      label:'Strategy A — EMA Trend'},
  {v:'B',      label:'Strategy B — ADX Momentum'},
  {v:'C',      label:'Strategy C — StochRSI Reversal'},
  {v:'D',      label:'Strategy D — SMI Supply & Demand'},
  {v:'manual', label:'Manual / my own setup'},
];
const STRAT_SHORT = {A:'A',B:'B',C:'C',D:'D',manual:'Manual'};

// Session windows in PH time (UTC+8) — the same three the landing page and the
// Session Timings tab publish. Used to auto-suggest a session tag.
const SESSIONS = [
  {v:'asian',  label:'Asian (7:00 AM PH)',    from:7*60,    to:11*60},
  {v:'london', label:'London (4:00 PM PH)',   from:16*60,   to:20*60},
  {v:'ny',     label:'New York (9:30 PM PH)', from:21*60+30,to:25*60+30},
  {v:'other',  label:'Outside a main session',from:-1,      to:-1},
];
const SESSION_SHORT = {asian:'Asian',london:'London',ny:'New York',other:'Other'};

const EMOTIONS = [
  {v:'calm',      label:'Calm & patient'},
  {v:'confident', label:'Confident'},
  {v:'fomo',      label:'FOMO — chased it'},
  {v:'fear',      label:'Fearful / hesitant'},
  {v:'greed',     label:'Greedy — oversized'},
  {v:'revenge',   label:'Revenge trading'},
  {v:'tired',     label:'Tired / unfocused'},
];
const EMOTION_SHORT = {calm:'Calm',confident:'Confident',fomo:'FOMO',fear:'Fear',greed:'Greed',revenge:'Revenge',tired:'Tired'};
// Which moods count as a red flag in the discipline read-out.
const BAD_MOOD = {fomo:1, greed:1, revenge:1, tired:1};

const RANGES = [
  {v:'all', label:'All time', days:0},
  {v:'7',   label:'Last 7 days',  days:7},
  {v:'30',  label:'Last 30 days', days:30},
  {v:'90',  label:'Last 90 days', days:90},
];

/* ── helpers ─────────────────────────────────────────────────────────────── */

const num = v => (v===null || v===undefined || v==='') ? null : Number(v);
const fmtDate = d => d ? new Date(d+'T00:00:00').toLocaleDateString(undefined,{month:'short',day:'numeric',year:'numeric'}) : '—';
const money = n => (n===null||n===undefined||n==='') ? '—'
  : (Number(n)>=0?'+':'−') + '$' + Math.abs(Number(n)).toFixed(2);
const money0 = n => (Number(n)>=0?'+':'−') + '$' + Math.abs(Number(n)).toFixed(2);

// Signed R multiple from price levels. Needs entry, exit and a stop, because R
// is defined against the risk actually taken, not against the P&L.
function rMultiple(r){
  const e = num(r.entry_price), x = num(r.exit_price), sl = num(r.stop_loss);
  if(e===null || x===null || sl===null) return null;
  const risk = Math.abs(e - sl);
  if(!(risk > 0)) return null;                     // guards 0 and NaN
  const dir = r.direction === 'short' ? -1 : 1;
  return ((x - e) * dir) / risk;
}

// Today in PH time, as YYYY-MM-DD. Same trick used across the site: shift the
// epoch by +8h and then read the UTC fields, so the answer never depends on the
// device timezone. PH has no DST.
function phToday(){
  const d = new Date(Date.now() + 8*3600000);
  return d.toISOString().slice(0,10);
}
function phMinutesNow(){
  const d = new Date(Date.now() + 8*3600000);
  return d.getUTCHours()*60 + d.getUTCMinutes();
}
function guessSession(){
  const m = phMinutesNow();
  for(const s of SESSIONS){
    if(s.from < 0) continue;
    // NY spills past midnight, so compare against the wrapped minute too.
    if((m >= s.from && m < s.to) || (m + 1440 >= s.from && m + 1440 < s.to)) return s.v;
  }
  return 'other';
}
function daysAgoISO(n){
  const d = new Date(Date.now() + 8*3600000 - n*86400000);
  return d.toISOString().slice(0,10);
}

function blankForm(){
  return { trade_date: phToday(), pair:'XAU/USD', direction:'long', lot_size:'',
           entry_price:'', exit_price:'', stop_loss:'', take_profit:'', pnl:'',
           strategy_tag:'', session_tag: guessSession(), emotion:'',
           followed_plan:null, notes:'', private_note:'', signal_id:null };
}

/* ── one-time stylesheet ─────────────────────────────────────────────────── */
// Injected once rather than inlined per element, because the responsive
// table/card swap and the print layout both need real media queries, which
// inline styles cannot express.
(function injectCSS(){
  if(document.getElementById('cg-journal-css')) return;
  const s = document.createElement('style');
  s.id = 'cg-journal-css';
  s.textContent = `
  .jr-actions{display:flex;gap:9px;align-items:center;flex-wrap:wrap}
  .jr-btn-sm{cursor:pointer;padding:8px 13px;border-radius:9px;border:1px solid var(--border);
    background:var(--surface);color:var(--ink-dim);font-size:.78rem;font-weight:600;
    display:inline-flex;align-items:center;gap:7px;transition:border-color .18s,color .18s}
  .jr-btn-sm:hover{border-color:var(--border-gold);color:var(--gold-2)}
  .jr-btn-sm svg{width:14px;height:14px;fill:none;stroke:currentColor;stroke-width:1.8;
    stroke-linecap:round;stroke-linejoin:round}
  .jr-menu{position:absolute;right:0;top:calc(100% + 6px);z-index:40;min-width:230px;
    background:var(--navy-950);border:1px solid var(--border-gold);border-radius:11px;
    box-shadow:0 18px 44px rgba(0,0,0,.55);overflow:hidden}
  .jr-menu button{display:block;width:100%;text-align:left;padding:11px 14px;background:none;
    border:0;color:var(--ink-dim);font-size:.8rem;cursor:pointer;font-family:inherit}
  .jr-menu button:hover:not(:disabled){background:rgba(212,175,55,.09);color:var(--gold-2)}
  .jr-menu button:disabled{opacity:.45;cursor:not-allowed}
  .jr-menu .jr-menu-sub{display:block;font-size:.7rem;color:var(--ink-faint);margin-top:2px}

  .jr-filters{display:flex;gap:9px;flex-wrap:wrap;align-items:center;margin-bottom:18px}
  .jr-filters select{background:var(--surface);border:1px solid var(--border);color:var(--ink-dim);
    border-radius:9px;padding:8px 11px;font-size:.78rem;font-family:inherit;cursor:pointer}
  .jr-filters select:focus{outline:none;border-color:var(--border-gold)}

  .jr-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:18px}
  .jr-stat{background:linear-gradient(165deg,var(--surface-2),var(--surface));
    border:1px solid var(--border);border-radius:var(--radius);padding:15px 17px}
  .jr-stat .v{font-family:var(--font-mono);font-size:1.34rem;font-weight:600;color:var(--gold-2);line-height:1.15}
  .jr-stat .l{font-size:.75rem;color:var(--ink-dim);margin-top:3px}
  @media(max-width:820px){ .jr-stats{grid-template-columns:repeat(2,1fr)} }

  .jr-pro{border:1px solid var(--border-gold);border-radius:var(--radius);
    background:linear-gradient(165deg,rgba(212,175,55,.05),transparent 60%),var(--navy-950);
    padding:20px 22px;margin-bottom:20px}
  .jr-metrics{display:grid;grid-template-columns:repeat(5,1fr);gap:10px;margin-top:18px}
  .jr-metric{padding:11px 13px;border:1px solid var(--border);border-radius:10px;background:var(--surface)}
  .jr-metric .v{font-family:var(--font-mono);font-size:.98rem;font-weight:600}
  .jr-metric .l{font-size:.68rem;color:var(--ink-faint);margin-top:2px;text-transform:uppercase;letter-spacing:.04em}
  @media(max-width:820px){ .jr-metrics{grid-template-columns:repeat(2,1fr)} }

  .jr-split{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:18px}
  @media(max-width:820px){ .jr-split{grid-template-columns:1fr} }
  .jr-bd{border:1px solid var(--border);border-radius:10px;overflow:hidden}
  .jr-bd h4{margin:0;padding:10px 13px;font-size:.76rem;text-transform:uppercase;letter-spacing:.06em;
    color:var(--ink-faint);border-bottom:1px solid var(--border);background:var(--surface)}
  .jr-bd table{width:100%;border-collapse:collapse}
  .jr-bd td{padding:8px 13px;font-size:.79rem;border-bottom:1px solid var(--border)}
  .jr-bd tr:last-child td{border-bottom:0}
  .jr-bd td.n{font-family:var(--font-mono);text-align:right;white-space:nowrap}

  .jr-table{width:100%;border-collapse:separate;border-spacing:0;
    border:1px solid var(--border);border-radius:var(--radius);overflow:hidden}
  .jr-table th{font-size:.68rem;text-transform:uppercase;letter-spacing:.06em;color:var(--ink-faint);
    text-align:left;padding:10px 12px;background:var(--surface);border-bottom:1px solid var(--border);
    cursor:pointer;white-space:nowrap;user-select:none}
  .jr-table th:hover{color:var(--gold-2)}
  .jr-table td{padding:11px 12px;font-size:.82rem;border-bottom:1px solid var(--border);vertical-align:top}
  .jr-table tr:last-child td{border-bottom:0}
  .jr-table tbody tr:hover{background:rgba(212,175,55,.035)}
  .jr-table td.n{font-family:var(--font-mono);white-space:nowrap}
  .jr-pill{font-family:var(--font-mono);font-size:.64rem;text-transform:uppercase;letter-spacing:.03em;
    padding:2px 7px;border-radius:999px;border:1px solid var(--border);white-space:nowrap;display:inline-block}

  .jr-cards{display:grid;gap:10px}
  .jr-card{padding:14px 16px;border:1px solid var(--border);border-radius:var(--radius-sm);
    background:linear-gradient(165deg,var(--surface-2),var(--surface))}

  /* Desktop shows the dense table; phones get the card list. */
  .jr-desktop{display:block}
  .jr-mobile{display:none}
  @media(max-width:760px){ .jr-desktop{display:none} .jr-mobile{display:block} }

  /* view switch */
  .jr-seg{display:inline-flex;border:1px solid var(--border);border-radius:9px;overflow:hidden}
  .jr-seg button{background:none;border:0;padding:8px 14px;font-size:.78rem;font-weight:600;
    color:var(--ink-faint);cursor:pointer;font-family:inherit;display:inline-flex;align-items:center;gap:6px}
  .jr-seg button+button{border-left:1px solid var(--border)}
  .jr-seg button.on{background:rgba(212,175,55,.12);color:var(--gold-2)}
  .jr-seg svg{width:14px;height:14px;fill:none;stroke:currentColor;stroke-width:1.8;
    stroke-linecap:round;stroke-linejoin:round}

  /* month calendar */
  .jr-cal{border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;margin-bottom:16px}
  .jr-cal-head{display:flex;align-items:center;gap:8px;padding:12px 14px;background:var(--surface);
    border-bottom:1px solid var(--border);flex-wrap:wrap}
  .jr-cal-title{font-weight:700;font-size:.95rem;min-width:150px}
  .jr-cal-sum{margin-left:auto;font-family:var(--font-mono);font-size:.82rem;font-weight:600}
  .jr-cal-dow{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));background:var(--surface)}
  .jr-cal-dow>div{padding:7px 6px;text-align:center;font-size:.64rem;text-transform:uppercase;
    letter-spacing:.07em;color:var(--ink-faint)}
  .jr-cal-grid{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:5px;padding:8px;background:var(--navy-950)}
  .jr-cal-pad{min-height:74px}
  .jr-cal-day{position:relative;min-width:0;min-height:74px;border:1px solid var(--border);border-radius:9px;
    padding:6px 7px;display:flex;flex-direction:column;align-items:flex-start;gap:2px;
    cursor:pointer;font-family:inherit;text-align:left;transition:border-color .15s,transform .12s}
  .jr-cal-day:hover{border-color:var(--border-gold);transform:translateY(-1px)}
  .jr-cal-day.sel{box-shadow:0 0 0 1px var(--gold-2) inset}
  .jr-cal-day:focus-visible{outline:2px solid var(--gold-2);outline-offset:1px}
  .jr-cal-num{font-family:var(--font-mono);font-size:.72rem;color:var(--ink-faint)}
  .jr-cal-pnl{font-family:var(--font-mono);font-size:.78rem;font-weight:700;margin-top:auto;
    white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}
  .jr-cal-pnl-s{display:none;font-family:var(--font-mono);font-size:.7rem;font-weight:700;
    margin-top:auto;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}
  .jr-cal-n{font-size:.62rem;color:var(--ink-faint);white-space:nowrap}
  .jr-cal-flag{position:absolute;top:6px;right:6px;width:6px;height:6px;border-radius:50%;
    background:var(--red);box-shadow:0 0 0 2px var(--navy-950)}
  @media(max-width:640px){
    .jr-cal-grid{gap:3px;padding:5px}
    .jr-cal-day,.jr-cal-pad{min-height:58px}
    .jr-cal-day{padding:4px 5px;border-radius:7px}
    .jr-cal-pnl{display:none}
    .jr-cal-pnl-s{display:block}
    .jr-cal-n{display:none}
    .jr-cal-sum{width:100%;margin-left:0;order:9}
  }

  .jr-day{border:1px solid var(--border-gold);border-radius:var(--radius);padding:16px 18px;margin-bottom:16px;
    background:linear-gradient(165deg,rgba(212,175,55,.05),transparent 60%),var(--navy-950)}
  .jr-day-head{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:12px}

  .jr-lock{display:flex;gap:16px;align-items:center;justify-content:space-between;flex-wrap:wrap;
    padding:20px 22px;border:1px dashed var(--border-gold);border-radius:var(--radius);
    background:var(--surface);margin-bottom:20px}
  `;
  document.head.appendChild(s);
})();

/* ── analytics ───────────────────────────────────────────────────────────── */

// One pass over the filtered rows. Everything downstream (cards, curve,
// breakdowns, PDF) reads from this so the numbers can never disagree.
function analyse(rows){
  const withPnl = rows.filter(r => num(r.pnl) !== null);
  const wins  = withPnl.filter(r => Number(r.pnl) > 0);
  const losses= withPnl.filter(r => Number(r.pnl) < 0);
  const gross = wins.reduce((s,r)=>s+Number(r.pnl),0);
  const bad   = Math.abs(losses.reduce((s,r)=>s+Number(r.pnl),0));
  const total = withPnl.reduce((s,r)=>s+Number(r.pnl),0);

  const rs = rows.map(rMultiple).filter(v => v !== null);
  const avgR = rs.length ? rs.reduce((a,b)=>a+b,0)/rs.length : null;

  const planned = rows.filter(r => r.followed_plan !== null && r.followed_plan !== undefined);
  const kept = planned.filter(r => r.followed_plan === true).length;

  // Chronological order for the streak and the equity curve; the list itself is
  // newest-first, so this must sort its own copy rather than reuse it.
  const chrono = withPnl.slice().sort((a,b)=>
    a.trade_date === b.trade_date
      ? String(a.created_at||'').localeCompare(String(b.created_at||''))
      : String(a.trade_date).localeCompare(String(b.trade_date)));

  let streak = 0, streakWin = null;
  for(let i = chrono.length - 1; i >= 0; i--){
    const w = Number(chrono[i].pnl) > 0;
    if(streakWin === null){ streakWin = w; streak = 1; }
    else if(w === streakWin) streak++;
    else break;
  }

  let cum = 0;
  const curve = chrono.map(r => { cum += Number(r.pnl); return { d:r.trade_date, v:cum }; });

  return {
    count: rows.length,
    scored: withPnl.length,
    winRate: withPnl.length ? Math.round(wins.length/withPnl.length*100) : null,
    total,
    // No losses at all means the ratio is undefined, not infinite — show a dash
    // rather than an "∞" that looks like a bug.
    profitFactor: bad > 0 ? gross/bad : null,
    avgWin:  wins.length   ? gross/wins.length : null,
    avgLoss: losses.length ? bad/losses.length : null,
    best:  withPnl.length ? Math.max(...withPnl.map(r=>Number(r.pnl))) : null,
    worst: withPnl.length ? Math.min(...withPnl.map(r=>Number(r.pnl))) : null,
    avgR,
    rCount: rs.length,
    discipline: planned.length ? Math.round(kept/planned.length*100) : null,
    disciplineOf: planned.length,
    streak, streakWin,
    curve,
  };
}

// win-rate / P&L split by an arbitrary key, used for both breakdown tables.
function bucket(rows, key, labels){
  const out = {};
  rows.forEach(r => {
    const k = r[key] || '—';
    if(!out[k]) out[k] = { k, n:0, wins:0, scored:0, pnl:0 };
    const b = out[k]; b.n++;
    const p = num(r.pnl);
    if(p !== null){ b.scored++; b.pnl += p; if(p > 0) b.wins++; }
  });
  return Object.values(out)
    .map(b => ({...b, label: labels[b.k] || b.k, wr: b.scored ? Math.round(b.wins/b.scored*100) : null}))
    .sort((a,b) => b.pnl - a.pnl);
}

/* ── equity curve ────────────────────────────────────────────────────────── */

function EquityCurve({curve, height}){
  const H = height || 168, W = 720, PAD = 6;
  if(!curve.length) return <div style={{height:H,display:'grid',placeItems:'center',
    color:'var(--ink-faint)',fontSize:'.83rem'}}>Log a trade with a P&amp;L to see your equity curve.</div>;

  const vals = curve.map(p=>p.v);
  let lo = Math.min(0, ...vals), hi = Math.max(0, ...vals);
  if(hi === lo){ hi = lo + 1; }                        // flat line still needs a range
  const span = hi - lo;
  const x = i => PAD + (curve.length === 1 ? (W-2*PAD)/2 : i*(W-2*PAD)/(curve.length-1));
  const y = v => PAD + (hi - v)*(H-2*PAD)/span;
  const pts = curve.map((p,i)=> x(i)+','+y(p.v)).join(' ');
  const up = vals[vals.length-1] >= 0;
  const col = up ? 'var(--green)' : 'var(--red)';
  const zero = y(0);

  return <svg viewBox={'0 0 '+W+' '+H} preserveAspectRatio="none"
      style={{width:'100%',height:H,display:'block',overflow:'visible'}}>
    <defs>
      <linearGradient id="jrFill" x1="0" y1="0" x2="0" y2="1">
        <stop offset="0%"   stopColor={up?'#4ade80':'#f87171'} stopOpacity=".28"/>
        <stop offset="100%" stopColor={up?'#4ade80':'#f87171'} stopOpacity="0"/>
      </linearGradient>
    </defs>
    <line x1="0" x2={W} y1={zero} y2={zero} stroke="var(--border)" strokeWidth="1" strokeDasharray="4 4"/>
    <polygon points={x(0)+','+zero+' '+pts+' '+x(curve.length-1)+','+zero} fill="url(#jrFill)"/>
    <polyline points={pts} fill="none" stroke={col} strokeWidth="2"
      vectorEffect="non-scaling-stroke" strokeLinejoin="round" strokeLinecap="round"/>
    <circle cx={x(curve.length-1)} cy={y(vals[vals.length-1])} r="3.5" fill={col}
      vectorEffect="non-scaling-stroke"/>
  </svg>;
}

/* ── month calendar ──────────────────────────────────────────────────────── */

// trade_date is a plain 'YYYY-MM-DD' string, never a timestamp. Every date
// calculation below is done on numbers or on UTC, never with a local-time
// Date — `new Date('2026-08-01')` is midnight UTC, which is the previous day
// for anyone west of Greenwich and would shift the whole grid by one cell.
const MONTHS = ['January','February','March','April','May','June',
                'July','August','September','October','November','December'];
const DOW = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];

// Phone calendar cells are ~44px wide. Drop the currency symbol and the cents,
// and fold anything past 999 into a k suffix, so the number still fits.
function shortMoney(n){
  const v = Number(n), a = Math.abs(v), sign = v < 0 ? '\u2212' : '+';
  if(a >= 10000) return sign + Math.round(a/1000) + 'k';
  if(a >= 1000)  return sign + (a/1000).toFixed(1).replace(/\.0$/,'') + 'k';
  return sign + Math.round(a);
}

const pad2 = n => (n < 10 ? '0' : '') + n;
function ymd(y, m, d){ return y + '-' + pad2(m+1) + '-' + pad2(d); }         // m is 0-based
function daysInMonth(y, m){ return new Date(Date.UTC(y, m+1, 0)).getUTCDate(); }
function firstDow(y, m){ return new Date(Date.UTC(y, m, 1)).getUTCDay(); }   // 0 = Sunday
function addMonth(ym, delta){
  let y = ym.y, m = ym.m + delta;
  y += Math.floor(m/12); m = ((m % 12) + 12) % 12;
  return { y, m };
}
function phMonth(){
  const d = new Date(Date.now() + 8*3600000);
  return { y: d.getUTCFullYear(), m: d.getUTCMonth() };
}
function monthOf(iso){ return { y: Number(iso.slice(0,4)), m: Number(iso.slice(5,7)) - 1 }; }
function inMonth(iso, ym){
  return Number(iso.slice(0,4)) === ym.y && Number(iso.slice(5,7)) - 1 === ym.m;
}

function MonthCalendar({rows, ym, onPrev, onNext, onToday, selected, onSelect}){
  // Roll the month's trades up per day once, rather than filtering the full
  // list inside every one of the ~35 cells.
  const byDay = useMemo(()=>{
    const m = {};
    rows.forEach(r => {
      const k = r.trade_date;
      if(!m[k]) m[k] = { n:0, pnl:0, scored:0, offPlan:0 };
      const b = m[k]; b.n++;
      const p = num(r.pnl);
      if(p !== null){ b.scored++; b.pnl += p; }
      if(r.followed_plan === false) b.offPlan++;
    });
    return m;
  },[rows]);

  const dim = daysInMonth(ym.y, ym.m);
  const lead = firstDow(ym.y, ym.m);
  const today = phToday();

  // Colour intensity is relative to the biggest day of this month, so a quiet
  // month is still readable instead of washing out to a single flat tone.
  const peak = Math.max(1, ...Object.keys(byDay)
    .filter(k => inMonth(k, ym))
    .map(k => Math.abs(byDay[k].pnl)));

  const cells = [];
  for(let i = 0; i < lead; i++) cells.push(<div key={'x'+i} className="jr-cal-pad"/>);
  for(let d = 1; d <= dim; d++){
    const key = ymd(ym.y, ym.m, d);
    const b = byDay[key];
    const isToday = key === today;
    const isSel = key === selected;
    let bg = 'transparent', bd = 'var(--border)';
    if(b && b.scored){
      const t = Math.min(1, Math.abs(b.pnl)/peak);
      const a = (0.10 + 0.30*t).toFixed(3);
      bg = b.pnl > 0 ? 'rgba(74,222,128,'+a+')' : b.pnl < 0 ? 'rgba(248,113,113,'+a+')' : 'transparent';
      bd = b.pnl > 0 ? 'rgba(74,222,128,.4)' : b.pnl < 0 ? 'rgba(248,113,113,.4)' : 'var(--border)';
    }
    cells.push(
      <button key={key} type="button" className={'jr-cal-day'+(isSel?' sel':'')}
        onClick={()=>onSelect(isSel ? null : key)}
        aria-pressed={isSel}
        aria-label={fmtDate(key) + (b ? ' — ' + b.n + ' trade' + (b.n===1?'':'s') + ', ' + money0(b.pnl) : ' — no trades')}
        style={{background:bg, borderColor:isSel?'var(--gold-2)':bd}}>
        <span className="jr-cal-num" style={isToday?{color:'var(--gold-2)',fontWeight:700}:null}>{d}</span>
        {b && (()=>{ const c = b.scored ? (b.pnl>0?'var(--green)':b.pnl<0?'var(--red)':'var(--ink-faint)') : 'var(--ink-faint)';
          return <React.Fragment>
            <span className="jr-cal-pnl" style={{color:c}}>{b.scored ? money0(b.pnl) : '—'}</span>
            <span className="jr-cal-pnl-s" style={{color:c}}>{b.scored ? shortMoney(b.pnl) : '—'}</span>
            <span className="jr-cal-n">{b.n} {b.n===1?'trade':'trades'}</span>
          </React.Fragment>; })()}
        {b && b.offPlan > 0 && <span className="jr-cal-flag" title={b.offPlan+' off-plan'}/>}
      </button>
    );
  }

  const monthRows = rows.filter(r => inMonth(r.trade_date, ym));
  const monthPnl = monthRows.reduce((s,r)=> s + (num(r.pnl)||0), 0);

  return <div className="jr-cal">
    <div className="jr-cal-head">
      <button type="button" className="jr-btn-sm" onClick={onPrev} aria-label="Previous month">‹</button>
      <div className="jr-cal-title">{MONTHS[ym.m]} {ym.y}</div>
      <button type="button" className="jr-btn-sm" onClick={onNext} aria-label="Next month">›</button>
      <button type="button" className="jr-btn-sm" onClick={onToday}>This month</button>
      <div className="jr-cal-sum">
        <span style={{color:monthPnl>0?'var(--green)':monthPnl<0?'var(--red)':'var(--ink-faint)'}}>{money0(monthPnl)}</span>
        <span style={{color:'var(--ink-faint)'}}> · {monthRows.length} {monthRows.length===1?'trade':'trades'}</span>
      </div>
    </div>
    <div className="jr-cal-dow">{DOW.map(d=><div key={d}>{d}</div>)}</div>
    <div className="jr-cal-grid">{cells}</div>
  </div>;
}

/* ── export ──────────────────────────────────────────────────────────────── */

function csvCell(v){
  const s = (v===null||v===undefined) ? '' : String(v);
  // Leading =,+,-,@ are treated as a formula by Excel; prefix to neutralise.
  const safe = /^[=+\-@]/.test(s) ? "'" + s : s;
  return '"' + safe.replace(/"/g,'""') + '"';
}
function downloadCSV(rows, name){
  const head = ['Date','Pair','Direction','Lots','Entry','Stop','Target','Exit','P&L','R',
                'Strategy','Session','Mood','Followed plan','Notes'];
  const body = rows.map(r => [
    r.trade_date, r.pair, r.direction, r.lot_size, r.entry_price, r.stop_loss,
    r.take_profit, r.exit_price, r.pnl,
    rMultiple(r)===null ? '' : rMultiple(r).toFixed(2),
    STRAT_SHORT[r.strategy_tag]||'', SESSION_SHORT[r.session_tag]||'',
    EMOTION_SHORT[r.emotion]||'',
    r.followed_plan===true?'Yes':r.followed_plan===false?'No':'',
    r.notes||'',
  ].map(csvCell).join(','));
  // BOM so Excel reads the UTF-8 currency and arrow glyphs correctly.
  const blob = new Blob(['﻿' + [head.map(csvCell).join(','), ...body].join('\r\n')],
    {type:'text/csv;charset=utf-8'});
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob); a.download = name + '.csv';
  document.body.appendChild(a); a.click();
  setTimeout(()=>{ URL.revokeObjectURL(a.href); a.remove(); }, 0);
}

const esc = s => String(s===null||s===undefined?'':s)
  .replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');

// Branded report. Rendered into a hidden same-origin iframe and printed from
// there — window.open() gets killed by popup blockers even on a click, and an
// iframe keeps the dashboard itself out of the print output.
function buildReportHTML(rows, st, meta){
  const curveSVG = (function(){
    const c = st.curve; if(!c.length) return '';
    const H=150,W=700,P=5;
    const vals=c.map(p=>p.v); let lo=Math.min(0,...vals), hi=Math.max(0,...vals);
    if(hi===lo) hi=lo+1; const span=hi-lo;
    const x=i=>P+(c.length===1?(W-2*P)/2:i*(W-2*P)/(c.length-1));
    const y=v=>P+(hi-v)*(H-2*P)/span;
    const pts=c.map((p,i)=>x(i)+','+y(p.v)).join(' ');
    const up=vals[vals.length-1]>=0, col=up?'#15803d':'#b91c1c';
    return '<svg viewBox="0 0 '+W+' '+H+'" style="width:100%;height:150px">'
      + '<line x1="0" x2="'+W+'" y1="'+y(0)+'" y2="'+y(0)+'" stroke="#cbd5e1" stroke-dasharray="4 4"/>'
      + '<polyline points="'+pts+'" fill="none" stroke="'+col+'" stroke-width="2"/></svg>';
  })();

  const statCell = (l,v) => '<div class="s"><div class="sv">'+esc(v)+'</div><div class="sl">'+esc(l)+'</div></div>';
  const trs = rows.map(r => {
    const R = rMultiple(r);
    const p = num(r.pnl);
    return '<tr>'
      + '<td>'+esc(fmtDate(r.trade_date))+'</td>'
      + '<td>'+esc(r.pair)+'</td>'
      + '<td>'+esc(r.direction==='short'?'Short':'Long')+'</td>'
      + '<td class="n">'+esc(r.lot_size??'')+'</td>'
      + '<td class="n">'+esc(r.entry_price??'')+'</td>'
      + '<td class="n">'+esc(r.stop_loss??'')+'</td>'
      + '<td class="n">'+esc(r.exit_price??'')+'</td>'
      + '<td class="n" style="color:'+(p>0?'#15803d':p<0?'#b91c1c':'#334155')+'">'+esc(p===null?'':money0(p))+'</td>'
      + '<td class="n">'+esc(R===null?'':(R>=0?'+':'')+R.toFixed(2)+'R')+'</td>'
      + '<td>'+esc(STRAT_SHORT[r.strategy_tag]||'')+'</td>'
      + '<td>'+esc(SESSION_SHORT[r.session_tag]||'')+'</td>'
      + '<td class="note">'+esc(r.notes||'')+'</td>'
      + '</tr>';
  }).join('');

  const bdRows = (arr) => arr.map(b =>
    '<tr><td>'+esc(b.label)+'</td><td class="n">'+b.n+'</td><td class="n">'
    + (b.wr===null?'—':b.wr+'%')+'</td><td class="n" style="color:'
    + (b.pnl>0?'#15803d':b.pnl<0?'#b91c1c':'#334155')+'">'+esc(money0(b.pnl))+'</td></tr>').join('');

  return '<!doctype html><html><head><meta charset="utf-8"><title>'
    + esc(meta.filename) + '</title><style>'
    + '@page{size:A4 landscape;margin:12mm}'
    + '*{box-sizing:border-box}'
    + 'body{font:12px/1.45 -apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;color:#0f172a;margin:0}'
    + '.hd{display:flex;align-items:center;gap:14px;border-bottom:2px solid #d4af37;padding-bottom:12px;margin-bottom:16px}'
    + '.hd img{width:44px;height:44px;object-fit:contain}'
    + '.hd h1{margin:0;font-size:19px;letter-spacing:.2px}'
    + '.hd .sub{color:#64748b;font-size:11px;margin-top:2px}'
    + '.hd .who{margin-left:auto;text-align:right;font-size:11px;color:#334155}'
    + '.hd .who b{display:block;font-size:13px;color:#0f172a}'
    + '.stats{display:flex;gap:10px;margin-bottom:14px}'
    + '.s{flex:1;border:1px solid #e2e8f0;border-radius:7px;padding:9px 11px;background:#f8fafc}'
    + '.sv{font-size:16px;font-weight:700;font-variant-numeric:tabular-nums}'
    + '.sl{font-size:9.5px;color:#64748b;text-transform:uppercase;letter-spacing:.05em;margin-top:2px}'
    + '.box{border:1px solid #e2e8f0;border-radius:7px;padding:10px 12px;margin-bottom:14px}'
    + '.box h2{margin:0 0 6px;font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:#64748b}'
    + '.cols{display:flex;gap:12px;margin-bottom:14px}.cols>div{flex:1}'
    + 'table{width:100%;border-collapse:collapse}'
    + 'th{font-size:9.5px;text-transform:uppercase;letter-spacing:.05em;color:#64748b;text-align:left;'
    +   'border-bottom:1px solid #cbd5e1;padding:6px 7px}'
    + 'td{padding:6px 7px;border-bottom:1px solid #f1f5f9;font-size:10.5px;vertical-align:top}'
    + 'td.n{text-align:right;font-variant-numeric:tabular-nums;white-space:nowrap}'
    + 'td.note{color:#475569;max-width:220px}'
    + 'thead{display:table-header-group}tr{break-inside:avoid}'
    + '.ft{margin-top:16px;padding-top:9px;border-top:1px solid #e2e8f0;font-size:9.5px;color:#94a3b8;'
    +   'display:flex;justify-content:space-between}'
    + '</style></head><body>'
    + '<div class="hd"><img src="' + esc(meta.logo) + '" alt="">'
    +   '<div><h1>Trading Journal Statement</h1><div class="sub">Chart Growth Hub · '
    +   esc(meta.rangeLabel) + '</div></div>'
    +   '<div class="who"><b>' + esc(meta.name) + '</b>' + esc(meta.memberId) + '</div></div>'
    + '<div class="stats">'
    +   statCell('Trades', st.count)
    +   statCell('Win rate', st.winRate===null?'—':st.winRate+'%')
    +   statCell('Net P&L', money0(st.total))
    +   statCell('Profit factor', st.profitFactor===null?'—':st.profitFactor.toFixed(2))
    +   statCell('Avg R', st.avgR===null?'—':(st.avgR>=0?'+':'')+st.avgR.toFixed(2)+'R')
    +   statCell('Discipline', st.discipline===null?'—':st.discipline+'%')
    + '</div>'
    + (curveSVG ? '<div class="box"><h2>Equity curve</h2>'+curveSVG+'</div>' : '')
    + '<div class="cols">'
    +   '<div class="box"><h2>By strategy</h2><table><thead><tr><th>Strategy</th><th class="n">Trades</th>'
    +     '<th class="n">Win rate</th><th class="n">P&L</th></tr></thead><tbody>'
    +     (bdRows(meta.byStrat) || '<tr><td colspan="4">No tagged trades.</td></tr>') + '</tbody></table></div>'
    +   '<div class="box"><h2>By session</h2><table><thead><tr><th>Session</th><th class="n">Trades</th>'
    +     '<th class="n">Win rate</th><th class="n">P&L</th></tr></thead><tbody>'
    +     (bdRows(meta.bySess) || '<tr><td colspan="4">No tagged trades.</td></tr>') + '</tbody></table></div>'
    + '</div>'
    + '<table><thead><tr><th>Date</th><th>Pair</th><th>Dir</th><th class="n">Lots</th>'
    +   '<th class="n">Entry</th><th class="n">Stop</th><th class="n">Exit</th><th class="n">P&L</th>'
    +   '<th class="n">R</th><th>Strat</th><th>Session</th><th>Notes</th></tr></thead><tbody>'
    +   (trs || '<tr><td colspan="12">No trades in this range.</td></tr>') + '</tbody></table>'
    + '<div class="ft"><span>Generated ' + esc(meta.generated)
    +   '</span><span>chartgrowthhub.com · private to ' + esc(meta.name) + '</span></div>'
    + '</body></html>';
}

function printReport(html){
  const old = document.getElementById('cg-journal-print');
  if(old) old.remove();
  const f = document.createElement('iframe');
  f.id = 'cg-journal-print';
  f.setAttribute('aria-hidden','true');
  // Off-screen rather than display:none — a display:none iframe does not lay
  // out in some browsers and prints blank.
  f.style.cssText = 'position:fixed;left:-10000px;top:0;width:1024px;height:768px;border:0';
  document.body.appendChild(f);
  const d = f.contentDocument;
  d.open(); d.write(html); d.close();
  const go = () => {
    try { f.contentWindow.focus(); f.contentWindow.print(); }
    catch(e){ /* printing blocked — the iframe is harmless, just leave it */ }
  };
  // Give the logo a beat to load so it is not missing from the first page.
  if(d.readyState === 'complete') setTimeout(go, 350);
  else f.onload = () => setTimeout(go, 350);
}

/* ── small pieces ────────────────────────────────────────────────────────── */

function Stat({label, value, tone}){
  return <div className="jr-stat">
    <div className="v" style={tone?{color:tone}:null}>{value}</div>
    <div className="l">{label}</div>
  </div>;
}
function Metric({label, value, tone}){
  return <div className="jr-metric">
    <div className="v" style={{color:tone||'var(--ink)'}}>{value}</div>
    <div className="l">{label}</div>
  </div>;
}
function Breakdown({title, rows, empty}){
  return <div className="jr-bd">
    <h4>{title}</h4>
    {rows.length === 0
      ? <div style={{padding:'14px 13px',color:'var(--ink-faint)',fontSize:'.79rem'}}>{empty}</div>
      : <table><tbody>{rows.map(b=>(
          <tr key={b.k}>
            <td>{b.label}</td>
            <td className="n" style={{color:'var(--ink-faint)'}}>{b.n}</td>
            <td className="n">{b.wr===null?'—':b.wr+'%'}</td>
            <td className="n" style={{color:b.pnl>0?'var(--green)':b.pnl<0?'var(--red)':'var(--ink-faint)'}}>{money0(b.pnl)}</td>
          </tr>))}</tbody></table>}
  </div>;
}

/* ── trade list ──────────────────────────────────────────────────────────── */
// Shared by the list view and the calendar's day panel, so a trade renders
// identically wherever it is reached from. `showSort` is off inside the day
// panel — a single day has nothing worth re-sorting.
function TradeList({rows, sort, onSort, onEdit, onDel, showSort}){
  const toggleSort = k => { if(showSort && onSort) onSort(k); };
  return <React.Fragment>
    {/* desktop table */}
    <div className="jr-desktop">
      <table className="jr-table">
        <thead><tr>
          {[['trade_date','Date'],['pair','Pair'],['lot_size','Lots'],['entry_price','Entry'],
            ['exit_price','Exit'],['pnl','P&L'],['r','R'],['strategy_tag','Strategy'],['session_tag','Session']]
            .map(([k,l])=>(
            <th key={k} onClick={()=>toggleSort(k)} style={showSort?null:{cursor:'default'}}>
              {l}{showSort && sort.k===k ? (sort.dir===1?' ↑':' ↓') : ''}</th>))}
          <th style={{cursor:'default'}}>Notes</th>
          <th style={{cursor:'default',textAlign:'right'}}></th>
        </tr></thead>
        <tbody>
          {rows.map(r=>{ const R = rMultiple(r), p = num(r.pnl);
            return <tr key={r.id}>
              <td className="n" style={{color:'var(--ink-faint)'}}>{fmtDate(r.trade_date)}</td>
              <td>
                <div style={{fontWeight:600}}>{r.pair}</div>
                <span className="jr-pill" style={{marginTop:3,color:r.direction==='short'?'var(--red)':'var(--green)'}}>
                  {r.direction==='short'?'Short':'Long'}</span>
              </td>
              <td className="n" style={{color:'var(--ink-faint)'}}>{r.lot_size??'—'}</td>
              <td className="n" style={{color:'var(--ink-faint)'}}>{r.entry_price??'—'}</td>
              <td className="n" style={{color:'var(--ink-faint)'}}>{r.exit_price??'—'}</td>
              <td className="n" style={{fontWeight:600,
                color:p>0?'var(--green)':p<0?'var(--red)':'var(--ink-faint)'}}>{money(r.pnl)}</td>
              <td className="n" style={{color:R===null?'var(--ink-faint)':R>=0?'var(--green)':'var(--red)'}}>
                {R===null?'—':(R>=0?'+':'')+R.toFixed(2)+'R'}</td>
              <td>{r.strategy_tag ? <span className="jr-pill" style={{color:'var(--gold-2)',borderColor:'var(--border-gold)'}}>{STRAT_SHORT[r.strategy_tag]}</span> : <span style={{color:'var(--ink-faint)'}}>—</span>}</td>
              <td style={{color:'var(--ink-faint)',fontSize:'.76rem'}}>{SESSION_SHORT[r.session_tag]||'—'}</td>
              <td style={{color:'var(--ink-dim)',maxWidth:230}}>
                {r.notes||''}
                {(r.emotion || r.followed_plan!==null) && <div style={{marginTop:4,display:'flex',gap:5,flexWrap:'wrap'}}>
                  {r.emotion && <span className="jr-pill" style={{color:BAD_MOOD[r.emotion]?'var(--red)':'var(--ink-faint)'}}>{EMOTION_SHORT[r.emotion]}</span>}
                  {r.followed_plan===true  && <span className="jr-pill" style={{color:'var(--green)'}}>plan kept</span>}
                  {r.followed_plan===false && <span className="jr-pill" style={{color:'var(--red)'}}>off plan</span>}
                </div>}
              </td>
              <td style={{textAlign:'right',whiteSpace:'nowrap'}}>
                <button onClick={()=>onEdit(r)} style={{cursor:'pointer',padding:'5px 9px',borderRadius:7,
                  border:'1px solid var(--border)',background:'var(--surface)',color:'var(--ink-dim)',fontSize:'.72rem'}}>Edit</button>
                <button onClick={()=>onDel(r)} style={{cursor:'pointer',padding:'5px 9px',borderRadius:7,marginLeft:5,
                  border:'1px solid rgba(248,113,113,.3)',background:'rgba(248,113,113,.1)',color:'var(--red)',fontSize:'.72rem'}}>Del</button>
              </td>
            </tr>; })}
        </tbody>
      </table>
    </div>

    {/* mobile cards */}
    <div className="jr-mobile"><div className="jr-cards">
      {rows.map(r=>{ const R = rMultiple(r), p = num(r.pnl);
        return <div key={r.id} className="jr-card">
          <div style={{display:'flex',justifyContent:'space-between',gap:12,alignItems:'flex-start'}}>
            <div style={{minWidth:0}}>
              <div style={{display:'flex',gap:7,alignItems:'center',flexWrap:'wrap',marginBottom:4}}>
                <span style={{fontWeight:600}}>{r.pair}</span>
                <span className="jr-pill" style={{color:r.direction==='short'?'var(--red)':'var(--green)'}}>
                  {r.direction==='short'?'Short':'Long'}</span>
                {r.strategy_tag && <span className="jr-pill" style={{color:'var(--gold-2)',borderColor:'var(--border-gold)'}}>{STRAT_SHORT[r.strategy_tag]}</span>}
              </div>
              <div style={{fontFamily:'var(--font-mono)',fontSize:'.72rem',color:'var(--ink-faint)'}}>
                {fmtDate(r.trade_date)}{r.session_tag?' · '+SESSION_SHORT[r.session_tag]:''}
              </div>
            </div>
            <div style={{textAlign:'right',flexShrink:0}}>
              <div style={{fontFamily:'var(--font-mono)',fontWeight:600,
                color:p>0?'var(--green)':p<0?'var(--red)':'var(--ink-faint)'}}>{money(r.pnl)}</div>
              {R!==null && <div style={{fontFamily:'var(--font-mono)',fontSize:'.72rem',
                color:R>=0?'var(--green)':'var(--red)'}}>{(R>=0?'+':'')+R.toFixed(2)}R</div>}
            </div>
          </div>
          {(r.entry_price!==null||r.exit_price!==null||r.stop_loss!==null) &&
            <div style={{fontFamily:'var(--font-mono)',fontSize:'.71rem',color:'var(--ink-faint)',marginTop:6}}>
              {r.entry_price!==null?'entry '+r.entry_price:''}
              {r.stop_loss!==null?'  sl '+r.stop_loss:''}
              {r.exit_price!==null?'  exit '+r.exit_price:''}
              {r.lot_size!==null?'  '+r.lot_size+' lots':''}
            </div>}
          {r.notes && <div style={{color:'var(--ink-dim)',fontSize:'.84rem',marginTop:7}}>{r.notes}</div>}
          {(r.emotion || r.followed_plan!==null) && <div style={{marginTop:7,display:'flex',gap:5,flexWrap:'wrap'}}>
            {r.emotion && <span className="jr-pill" style={{color:BAD_MOOD[r.emotion]?'var(--red)':'var(--ink-faint)'}}>{EMOTION_SHORT[r.emotion]}</span>}
            {r.followed_plan===true  && <span className="jr-pill" style={{color:'var(--green)'}}>plan kept</span>}
            {r.followed_plan===false && <span className="jr-pill" style={{color:'var(--red)'}}>off plan</span>}
          </div>}
          <div style={{display:'flex',gap:6,marginTop:11}}>
            <button onClick={()=>onEdit(r)} style={{cursor:'pointer',padding:'6px 11px',borderRadius:7,
              border:'1px solid var(--border)',background:'var(--surface)',color:'var(--ink-dim)',fontSize:'.74rem'}}>Edit</button>
            <button onClick={()=>onDel(r)} style={{cursor:'pointer',padding:'6px 11px',borderRadius:7,
              border:'1px solid rgba(248,113,113,.3)',background:'rgba(248,113,113,.1)',color:'var(--red)',fontSize:'.74rem'}}>Del</button>
          </div>
        </div>; })}
    </div></div>
  </React.Fragment>;
}

/* ── main panel ──────────────────────────────────────────────────────────── */

function JournalPanel({client, session, isVIP, onNeedVip, member, prefill, onPrefillUsed}){
  const [rows,setRows]   = useState(null);
  const [err,setErr]     = useState('');
  const [form,setForm]   = useState(null);
  const [busy,setBusy]   = useState(false);
  const [menu,setMenu]   = useState(false);
  const [range,setRange] = useState('all');
  const [fStrat,setFStrat]= useState('');
  const [fSess,setFSess] = useState('');
  const [fRes,setFRes]   = useState('');
  const [sort,setSort]   = useState({k:'trade_date', dir:-1});
  const [view,setView]   = useState('list');          // 'list' | 'calendar'
  const [ym,setYm]       = useState(null);            // visible month, null until rows land
  const [day,setDay]     = useState(null);            // selected day, 'YYYY-MM-DD' or null
  const menuRef = useRef(null);

  async function load(){
    const { data, error } = await client.from('journal_entries').select('*')
      .eq('student_id', session.user.id)
      .order('trade_date',{ascending:false}).order('created_at',{ascending:false});
    if(error) setErr(error.message); else { setRows(data||[]); setErr(''); }
  }
  useEffect(()=>{ load(); /* eslint-disable-next-line */ },[]);

  // Land on the month of the most recent trade rather than on today — a student
  // opening the calendar after two quiet weeks should still see their work.
  useEffect(()=>{
    if(ym || !rows) return;
    setYm(rows.length ? monthOf(rows[0].trade_date) : phMonth());
    /* eslint-disable-next-line */
  },[rows]);

  // Close the export menu on an outside click.
  useEffect(()=>{
    if(!menu) return;
    const h = e => { if(menuRef.current && !menuRef.current.contains(e.target)) setMenu(false); };
    document.addEventListener('mousedown',h);
    return ()=>document.removeEventListener('mousedown',h);
  },[menu]);

  // A signal handed over from the Signals tab opens a pre-filled form.
  useEffect(()=>{
    if(!prefill) return;
    setForm(Object.assign(blankForm(), prefill));
    if(onPrefillUsed) onPrefillUsed();
    /* eslint-disable-next-line */
  },[prefill]);

  const filtered = useMemo(()=>{
    let r = rows || [];
    const days = view==='list' ? ((RANGES.find(x=>x.v===range)||{}).days || 0) : 0;
    if(days){ const from = daysAgoISO(days); r = r.filter(x => x.trade_date >= from); }
    if(fStrat) r = r.filter(x => x.strategy_tag === fStrat);
    if(fSess)  r = r.filter(x => x.session_tag === fSess);
    if(fRes === 'win')  r = r.filter(x => num(x.pnl) !== null && Number(x.pnl) > 0);
    if(fRes === 'loss') r = r.filter(x => num(x.pnl) !== null && Number(x.pnl) < 0);
    if(fRes === 'open') r = r.filter(x => num(x.pnl) === null);
    return r;
  },[rows, range, fStrat, fSess, fRes, view]);

  // Everything below the toolbar reads from `scoped`, so the stat cards, the
  // equity curve, the breakdowns and the export can never describe a different
  // set of trades than the one on screen.
  const scoped = useMemo(()=>
    (view==='calendar' && ym) ? filtered.filter(r=>inMonth(r.trade_date, ym)) : filtered,
    [filtered, view, ym]);

  const stats = useMemo(()=>analyse(scoped),[scoped]);
  const byStrat = useMemo(()=>bucket(scoped.filter(r=>r.strategy_tag),'strategy_tag',STRAT_SHORT),[scoped]);
  const bySess  = useMemo(()=>bucket(scoped.filter(r=>r.session_tag),'session_tag',SESSION_SHORT),[scoped]);

  // Trades on the selected day, oldest first — a day reads as a sequence.
  const dayRows = useMemo(()=> !day ? [] :
    filtered.filter(r=>r.trade_date===day)
      .slice().sort((a,b)=>String(a.created_at||'').localeCompare(String(b.created_at||''))),
    [filtered, day]);

  const sorted = useMemo(()=>{
    const r = scoped.slice();
    const k = sort.k, d = sort.dir;
    r.sort((a,b)=>{
      let x = a[k], y = b[k];
      if(k === 'pnl' || k === 'lot_size'){ x = num(x); y = num(y); }
      if(k === 'r'){ x = rMultiple(a); y = rMultiple(b); }
      // Unscored rows sink to the bottom in either direction — they carry no
      // information and would otherwise dominate the top of an ascending sort.
      if(x === null || x === undefined || x === '') return 1;
      if(y === null || y === undefined || y === '') return -1;
      if(typeof x === 'number' && typeof y === 'number') return (x-y)*d;
      return String(x).localeCompare(String(y))*d;
    });
    return r;
  },[scoped, sort]);

  function toggleSort(k){ setSort(s => s.k===k ? {k, dir:-s.dir} : {k, dir:-1}); }

  const rangeLabel = (view==='calendar' && ym)
    ? MONTHS[ym.m] + ' ' + ym.y
    : (RANGES.find(x=>x.v===range)||RANGES[0]).label;
  const memberName = (member && (member.nickname || member.full_name)) || (session.user.email||'Member');
  const memberId   = (member && member.member_id) || '';

  function exportCSV(){
    setMenu(false);
    downloadCSV(sorted, 'CGH-journal-' + phToday());
  }
  function exportPDF(){
    setMenu(false);
    if(!isVIP){ if(onNeedVip) onNeedVip('The branded PDF statement is a VIP feature.'); return; }
    const logo = new URL('assets/brand/cgh-256.png', document.baseURI).href;
    printReport(buildReportHTML(sorted, stats, {
      filename:'CGH Trading Journal', logo, name:memberName, memberId,
      rangeLabel, byStrat, bySess,
      generated: new Date().toLocaleString(undefined,{dateStyle:'medium',timeStyle:'short'}),
    }));
  }

  function openAdd(onDate){
    const f = blankForm();
    if(typeof onDate === 'string') f.trade_date = onDate;
    setForm(f);
  }
  async function openEdit(r){
    const f = Object.assign({}, blankForm(), r);
    ['lot_size','entry_price','exit_price','stop_loss','take_profit','pnl'].forEach(k=>{
      f[k] = r[k] === null || r[k] === undefined ? '' : r[k];
    });
    f.strategy_tag = r.strategy_tag || '';
    f.session_tag  = r.session_tag  || '';
    f.emotion      = r.emotion      || '';
    f.notes        = r.notes || '';
    f.private_note = '';
    setForm(f);
    const { data } = await client.from('journal_private').select('body').eq('entry_id', r.id).maybeSingle();
    if(data) setForm(cur => cur && cur.id === r.id ? {...cur, private_note: data.body || ''} : cur);
  }

  async function save(){
    if(!form.pair.trim()){ setErr('Enter the pair or asset.'); return; }
    setErr(''); setBusy(true);
    const payload = {
      student_id: session.user.id,
      trade_date: form.trade_date,
      pair: form.pair.trim().toUpperCase(),
      direction: form.direction,
      lot_size:    num(form.lot_size),
      entry_price: num(form.entry_price),
      exit_price:  num(form.exit_price),
      stop_loss:   num(form.stop_loss),
      take_profit: num(form.take_profit),
      pnl:         num(form.pnl),
      strategy_tag: form.strategy_tag || null,
      session_tag:  form.session_tag  || null,
      emotion:      form.emotion      || null,
      followed_plan: form.followed_plan,
      signal_id:     form.signal_id || null,
      notes: (form.notes||'').trim() || null,
    };
    let error, id = form.id;
    if(id){ ({ error } = await client.from('journal_entries').update(payload).eq('id', id)); }
    else {
      const res = await client.from('journal_entries').insert(payload).select('id').single();
      error = res.error; id = res.data && res.data.id;
    }
    if(!error && id){
      const body = (form.private_note||'').trim();
      if(body) await client.from('journal_private').upsert({entry_id:id, student_id:session.user.id, body, updated_at:new Date().toISOString()});
      else     await client.from('journal_private').delete().eq('entry_id', id);
    }
    setBusy(false);
    if(error){ setErr(error.message); return; }
    setForm(null); load();
  }

  async function del(r){
    if(!confirm('Delete this journal entry? This cannot be undone.')) return;
    setRows(rs => rs.filter(x => x.id !== r.id));
    const { error } = await client.from('journal_entries').delete().eq('id', r.id);
    if(error){ setErr(error.message); load(); }
  }

  if(!rows) return <div style={{padding:40,textAlign:'center'}}><span className="cg-spin"></span></div>;

  const set = (k,v) => setForm(f => ({...f, [k]:v}));

  return <div>
    {/* header */}
    <div style={{display:'flex',justifyContent:'space-between',alignItems:'flex-start',gap:16,flexWrap:'wrap',marginBottom:16}}>
      <div style={{minWidth:0}}>
        <h2 style={{fontSize:'1.4rem',margin:'0 0 4px'}}>Trading Journal</h2>
        <p style={{color:'var(--ink-dim)',fontSize:'.88rem',margin:0,maxWidth:560}}>
          Log every trade and let the numbers show you what actually works.
          Your coach and CGH HQ can review your entries to mentor you — the
          <strong style={{color:'var(--gold-2)'}}> private note</strong> on each trade stays yours alone.
        </p>
      </div>
      <div className="jr-actions" style={{position:'relative'}} ref={menuRef}>
        <button className="jr-btn-sm" onClick={()=>setMenu(m=>!m)}>
          <svg viewBox="0 0 24 24"><path d="M12 4v11"/><path d="M8 11l4 4 4-4"/><path d="M4 19h16"/></svg>
          Download
        </button>
        {menu && <div className="jr-menu">
          <button onClick={exportPDF}>
            {isVIP ? 'Branded PDF statement' : 'Branded PDF statement — VIP'}
            <span className="jr-menu-sub">Full report with your equity curve</span>
          </button>
          <button onClick={exportCSV}>
            CSV spreadsheet
            <span className="jr-menu-sub">Opens in Excel or Google Sheets</span>
          </button>
        </div>}
        <button className="btn btn-primary" style={{width:'auto'}} onClick={()=>openAdd()}>+ Log a trade</button>
      </div>
    </div>

    {/* filters */}
    <div className="jr-filters">
      <div className="jr-seg" role="group" aria-label="View">
        <button type="button" className={view==='list'?'on':''} onClick={()=>{setView('list'); setDay(null);}}>
          <svg viewBox="0 0 24 24"><path d="M8 6h13M8 12h13M8 18h13M3.5 6h.01M3.5 12h.01M3.5 18h.01"/></svg>List
        </button>
        <button type="button" className={view==='calendar'?'on':''} onClick={()=>setView('calendar')}>
          <svg viewBox="0 0 24 24"><rect x="3" y="5" width="18" height="16" rx="2"/><path d="M3 10h18M8 3v4M16 3v4"/></svg>Calendar
        </button>
      </div>
      {view==='list' && <select value={range} onChange={e=>setRange(e.target.value)}>
        {RANGES.map(r=><option key={r.v} value={r.v}>{r.label}</option>)}
      </select>}
      <select value={fStrat} onChange={e=>setFStrat(e.target.value)}>
        <option value="">All strategies</option>
        {STRATS.map(s=><option key={s.v} value={s.v}>{s.label}</option>)}
      </select>
      <select value={fSess} onChange={e=>setFSess(e.target.value)}>
        <option value="">All sessions</option>
        {SESSIONS.map(s=><option key={s.v} value={s.v}>{SESSION_SHORT[s.v]}</option>)}
      </select>
      <select value={fRes} onChange={e=>setFRes(e.target.value)}>
        <option value="">All results</option>
        <option value="win">Wins only</option>
        <option value="loss">Losses only</option>
        <option value="open">No P&amp;L yet</option>
      </select>
      {((view==='list'&&range!=='all')||fStrat||fSess||fRes) &&
        <button className="jr-btn-sm" onClick={()=>{setRange('all');setFStrat('');setFSess('');setFRes('');}}>Clear</button>}
      <span style={{marginLeft:'auto',fontSize:'.75rem',color:'var(--ink-faint)',fontFamily:'var(--font-mono)'}}>
        {scoped.length} of {rows.length} {rows.length===1?'entry':'entries'}
      </span>
    </div>

    {err && <div style={{color:'var(--red)',fontSize:'.84rem',fontFamily:'var(--font-mono)',marginBottom:14}}>{err}</div>}

    {/* headline stats — everyone */}
    <div className="jr-stats">
      <Stat label="Trades logged" value={stats.count}/>
      <Stat label="Win rate" value={stats.winRate===null?'—':stats.winRate+'%'}/>
      <Stat label="Net P&L" value={money0(stats.total)}
        tone={stats.total>0?'var(--green)':stats.total<0?'var(--red)':null}/>
      <Stat label="Profit factor" value={stats.profitFactor===null?'—':stats.profitFactor.toFixed(2)}/>
    </div>

    {/* calendar view */}
    {view==='calendar' && ym && <React.Fragment>
      <MonthCalendar rows={filtered} ym={ym} selected={day}
        onPrev={()=>{ setYm(m=>addMonth(m,-1)); setDay(null); }}
        onNext={()=>{ setYm(m=>addMonth(m, 1)); setDay(null); }}
        onToday={()=>{ setYm(phMonth()); setDay(null); }}
        onSelect={setDay}/>

      {day && <div className="jr-day">
        <div className="jr-day-head">
          <div style={{fontWeight:700,fontSize:'.95rem'}}>{fmtDate(day)}</div>
          {dayRows.length>0 && <span style={{fontFamily:'var(--font-mono)',fontSize:'.82rem',fontWeight:600,
            color: dayRows.reduce((a,r)=>a+(num(r.pnl)||0),0)>0 ? 'var(--green)'
                 : dayRows.reduce((a,r)=>a+(num(r.pnl)||0),0)<0 ? 'var(--red)' : 'var(--ink-faint)'}}>
            {money0(dayRows.reduce((a,r)=>a+(num(r.pnl)||0),0))}
          </span>}
          <span style={{fontSize:'.76rem',color:'var(--ink-faint)'}}>
            {dayRows.length} {dayRows.length===1?'trade':'trades'}</span>
          <div style={{marginLeft:'auto',display:'flex',gap:8}}>
            <button className="jr-btn-sm" onClick={()=>openAdd(day)}>+ Log a trade on this day</button>
            <button className="jr-btn-sm" onClick={()=>setDay(null)}>Close</button>
          </div>
        </div>
        {dayRows.length===0
          ? <div style={{padding:'22px 0',textAlign:'center',color:'var(--ink-faint)',fontSize:'.84rem'}}>
              Nothing logged on this day yet.</div>
          : <TradeList rows={dayRows} sort={sort} showSort={false} onEdit={openEdit} onDel={del}/>}
      </div>}

      {scoped.length===0 && !form && <div style={{padding:'26px 20px',textAlign:'center',color:'var(--ink-faint)',
        border:'1px dashed var(--border)',borderRadius:'var(--radius)'}}>
        No trades in {MONTHS[ym.m]} {ym.y}{(fStrat||fSess||fRes)?' matching these filters':''}.
      </div>}
    </React.Fragment>}

    {/* pro analytics — VIP */}
    {isVIP ? <div className="jr-pro">
      <div style={{display:'flex',alignItems:'center',gap:9,marginBottom:2,flexWrap:'wrap'}}>
        <h3 style={{margin:0,fontSize:'1rem'}}>Performance</h3>
        <span style={{fontFamily:'var(--font-mono)',fontSize:'.62rem',letterSpacing:'.08em',
          color:'var(--gold-2)',border:'1px solid var(--border-gold)',borderRadius:999,padding:'2px 8px'}}>VIP</span>
        <span style={{marginLeft:'auto',fontSize:'.74rem',color:'var(--ink-faint)'}}>{rangeLabel}</span>
      </div>
      <p style={{color:'var(--ink-faint)',fontSize:'.76rem',margin:'0 0 6px'}}>Cumulative P&amp;L across your logged trades.</p>
      <EquityCurve curve={stats.curve}/>

      <div className="jr-metrics">
        <Metric label="Avg R" value={stats.avgR===null?'—':(stats.avgR>=0?'+':'')+stats.avgR.toFixed(2)+'R'}
          tone={stats.avgR===null?'var(--ink-faint)':stats.avgR>=0?'var(--green)':'var(--red)'}/>
        <Metric label="Avg win"  value={stats.avgWin===null?'—':'$'+stats.avgWin.toFixed(2)} tone="var(--green)"/>
        <Metric label="Avg loss" value={stats.avgLoss===null?'—':'$'+stats.avgLoss.toFixed(2)} tone="var(--red)"/>
        <Metric label="Best / worst"
          value={stats.best===null?'—':money0(stats.best)+' / '+money0(stats.worst)}/>
        <Metric label="Current streak"
          value={stats.streak===0?'—':stats.streak+(stats.streakWin?' W':' L')}
          tone={stats.streak===0?'var(--ink-faint)':stats.streakWin?'var(--green)':'var(--red)'}/>
      </div>

      {stats.rCount < stats.count && <p style={{fontSize:'.72rem',color:'var(--ink-faint)',margin:'9px 0 0'}}>
        R is calculated on the {stats.rCount} of {stats.count} {stats.count===1?'trade':'trades'} that have an entry, exit and stop loss.
      </p>}

      <div className="jr-split">
        <Breakdown title="By strategy" rows={byStrat} empty="Tag your trades with a strategy to see this."/>
        <Breakdown title="By session"  rows={bySess}  empty="Tag your trades with a session to see this."/>
      </div>

      {/* discipline */}
      <div style={{marginTop:18,padding:'14px 16px',border:'1px solid var(--border)',borderRadius:10,background:'var(--surface)'}}>
        <div style={{display:'flex',alignItems:'center',gap:12,flexWrap:'wrap'}}>
          <div style={{minWidth:0,flex:1}}>
            <div style={{fontSize:'.82rem',fontWeight:600,marginBottom:3}}>Discipline score</div>
            <div style={{fontSize:'.75rem',color:'var(--ink-faint)'}}>
              {stats.discipline===null
                ? 'Answer “Did I follow my plan?” when you log a trade to start tracking this.'
                : 'You followed your plan on '+stats.discipline+'% of your last '+stats.disciplineOf+' answered trades.'}
            </div>
          </div>
          <div style={{fontFamily:'var(--font-mono)',fontSize:'1.5rem',fontWeight:600,
            color: stats.discipline===null?'var(--ink-faint)'
                 : stats.discipline>=80?'var(--green)':stats.discipline>=60?'var(--gold-2)':'var(--red)'}}>
            {stats.discipline===null?'—':stats.discipline+'%'}
          </div>
        </div>
        {stats.discipline!==null && <div style={{marginTop:10,height:6,borderRadius:999,background:'var(--border)',overflow:'hidden'}}>
          <div style={{height:'100%',width:stats.discipline+'%',borderRadius:999,
            background: stats.discipline>=80?'var(--green)':stats.discipline>=60?'var(--gold-2)':'var(--red)'}}/>
        </div>}
      </div>
    </div>
    : <div className="jr-lock">
        <div style={{minWidth:0}}>
          <div style={{display:'flex',alignItems:'center',gap:9,marginBottom:5}}>
            <h3 style={{fontSize:'1.05rem',margin:0}}>Performance analytics</h3>
            <span style={{fontFamily:'var(--font-mono)',fontSize:'.62rem',letterSpacing:'.08em',
              color:'var(--gold-2)',border:'1px solid var(--border-gold)',borderRadius:999,padding:'2px 8px'}}>VIP</span>
          </div>
          <p style={{color:'var(--ink-dim)',fontSize:'.87rem',margin:0,maxWidth:580}}>
            Your equity curve, average R, profit factor, a win-rate breakdown by strategy and session,
            your discipline score, and the branded PDF statement. Keep logging — your entries are
            saved and every metric fills in the moment you upgrade.
          </p>
        </div>
        <button className="btn btn-primary" style={{width:'auto'}}
          onClick={()=>onNeedVip && onNeedVip('Performance analytics and the PDF statement are VIP features.')}>Get VIP Access →</button>
      </div>}

    {/* form */}
    {form && <div style={{padding:'20px 22px',border:'1px solid var(--border-gold)',borderRadius:'var(--radius-sm)',background:'var(--navy-950)',marginBottom:18}}>
      <div style={{fontSize:'.95rem',fontWeight:700,marginBottom:14}}>{form.id?'Edit trade':'Log a trade'}</div>

      <div className="form-3" style={{marginBottom:13}}>
        <div className="field"><label>Date</label>
          <input type="date" value={form.trade_date} onChange={e=>set('trade_date',e.target.value)}/></div>
        <div className="field"><label>Pair / asset</label>
          <input value={form.pair} onChange={e=>set('pair',e.target.value)} placeholder="XAU/USD"/></div>
        <div className="field"><label>Direction</label>
          <select value={form.direction} onChange={e=>set('direction',e.target.value)}>
            <option value="long">Long / Buy</option><option value="short">Short / Sell</option></select></div>
      </div>

      <div className="form-3" style={{marginBottom:13}}>
        <div className="field"><label>Lot size</label>
          <input type="number" step="any" value={form.lot_size} onChange={e=>set('lot_size',e.target.value)} placeholder="0.08"/></div>
        <div className="field"><label>Entry price</label>
          <input type="number" step="any" value={form.entry_price} onChange={e=>set('entry_price',e.target.value)}/></div>
        <div className="field"><label>Exit price</label>
          <input type="number" step="any" value={form.exit_price} onChange={e=>set('exit_price',e.target.value)}/></div>
      </div>

      <div className="form-3" style={{marginBottom:13}}>
        <div className="field"><label>Stop loss</label>
          <input type="number" step="any" value={form.stop_loss} onChange={e=>set('stop_loss',e.target.value)}/></div>
        <div className="field"><label>Take profit</label>
          <input type="number" step="any" value={form.take_profit} onChange={e=>set('take_profit',e.target.value)}/></div>
        <div className="field"><label>P&amp;L ($)</label>
          <input type="number" step="any" value={form.pnl} onChange={e=>set('pnl',e.target.value)} placeholder="120 or -45"/></div>
      </div>

      <div className="form-3" style={{marginBottom:13}}>
        <div className="field"><label>Strategy</label>
          <select value={form.strategy_tag} onChange={e=>set('strategy_tag',e.target.value)}>
            <option value="">— not tagged —</option>
            {STRATS.map(s=><option key={s.v} value={s.v}>{s.label}</option>)}</select></div>
        <div className="field"><label>Session</label>
          <select value={form.session_tag} onChange={e=>set('session_tag',e.target.value)}>
            <option value="">— not tagged —</option>
            {SESSIONS.map(s=><option key={s.v} value={s.v}>{s.label}</option>)}</select></div>
        <div className="field"><label>How did you feel?</label>
          <select value={form.emotion} onChange={e=>set('emotion',e.target.value)}>
            <option value="">— skip —</option>
            {EMOTIONS.map(s=><option key={s.v} value={s.v}>{s.label}</option>)}</select></div>
      </div>

      <div className="field" style={{marginBottom:13}}>
        <label>Did you follow your plan?</label>
        <div style={{display:'flex',gap:8,marginTop:5}}>
          {[[true,'Yes'],[false,'No'],[null,'Skip']].map(([v,l])=>(
            <button key={l} type="button" onClick={()=>set('followed_plan',v)}
              style={{cursor:'pointer',padding:'7px 15px',borderRadius:9,fontSize:'.78rem',fontWeight:600,
                border:'1px solid '+(form.followed_plan===v?'var(--border-gold)':'var(--border)'),
                background: form.followed_plan===v?'rgba(212,175,55,.12)':'var(--surface)',
                color: form.followed_plan===v?'var(--gold-2)':'var(--ink-dim)'}}>{l}</button>))}
        </div>
      </div>

      <div className="field" style={{marginBottom:13}}><label>Notes</label>
        <input value={form.notes||''} onChange={e=>set('notes',e.target.value)}
          placeholder="Setup, reasoning, what you'd do differently…"/></div>

      <div className="field" style={{marginBottom:16}}>
        <label>Private note — only you can read this</label>
        <input value={form.private_note||''} onChange={e=>set('private_note',e.target.value)}
          placeholder="Kept out of your coach's view entirely."/>
      </div>

      <div style={{display:'flex',gap:10}}>
        <button className="btn btn-primary" style={{width:'auto'}} disabled={busy} onClick={save}>
          {busy ? <><span className="cg-spin"></span> Saving…</> : (form.id?'Save changes':'Add entry')}</button>
        <button className="btn btn-ghost" style={{width:'auto'}} onClick={()=>setForm(null)}>Cancel</button>
      </div>
    </div>}

    {/* list view */}
    {view==='list' && <React.Fragment>
      {sorted.length===0 && !form && <div style={{padding:'40px 20px',textAlign:'center',color:'var(--ink-faint)',
        border:'1px dashed var(--border)',borderRadius:'var(--radius)'}}>
        {rows.length===0
          ? <>No entries yet. Click <strong>+ Log a trade</strong> to start your journal.</>
          : <>No trades match these filters.</>}
      </div>}
      {sorted.length>0 && <TradeList rows={sorted} sort={sort} showSort={true}
        onSort={toggleSort} onEdit={openEdit} onDel={del}/>}
    </React.Fragment>}
  </div>;
}

window.JournalPanel = JournalPanel;
// Exposed for the coach/admin read-only view and for tests.
window.cgJournalAnalyse = analyse;
window.cgJournalBucket  = bucket;
window.cgJournalR       = rMultiple;
})();
