// student-dashboard.jsx — Minimal, elderly-friendly student portal.
// Wired to Supabase via window.useDashboardData() + window.useAuth().
//
// AUTH MODEL (2026-05-19) — email-as-username, magic-link as primary login:
//   Onboarding creates a Supabase user with the email the student typed in
//   step 3 of the wizard (e.g. kogawa3446@bitmah.com). That's the same
//   email they later type into the magic-link login form — so "sign up,
//   log out, log back in" works. Phone is stored as contact info only
//   (student_info.phone) and is no longer part of the auth identity.
//
//   A random password is still generated and stashed in localStorage as
//   a per-device fallback (legacy phone-login path, gated behind
//   FEATURES.phoneLogin, currently off).
//
// SECURITY NOTE: an earlier version of this file derived the password
// from the phone digits (`mp-<digits>-mastery-v1`). That meant anyone
// who knew a student's phone number could sign in as them. Fixed
// 2026-05-19 — see pentest writeup. NEVER reintroduce a deterministic
// password helper here.

const STD_MONTHS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
const STD_WDAYS  = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];

// ── auth helpers ────────────────────────────────────────────────────
const phoneDigits = (s) => String(s || '').replace(/\D/g, '');
const phoneToEmail = (p) => `student-${phoneDigits(p)}@masterystudent.app`;

// Generate a fresh random password (CSPRNG, 192 bits). Stored only in
// the user's localStorage so this device can sign back in. Other
// devices must use the magic-link / email-recovery path.
const newRandomPassword = () => {
  const bytes = new Uint8Array(24);
  (window.crypto || window.msCrypto).getRandomValues(bytes);
  return 'mp1-' + Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
};
const phoneCredKey   = (p) => `mastery_phone_cred_${phoneDigits(p)}`;
const persistPhoneCred = (p, pw) => { try { localStorage.setItem(phoneCredKey(p), pw); } catch (e) {} };
const readPhoneCred  = (p) => { try { return localStorage.getItem(phoneCredKey(p)); } catch (e) { return null; } };

// ── country / phone-format helpers ──────────────────────────────────
// fmt 'us'    — NXX-NXX-XXXX (NANP: US/CA), strict 10 digits
// fmt 'uk'    — NNNN NNN NNN, up to 10 digits
// fmt 'group' — space every 3 digits, generous cap for everyone else
const COUNTRIES = [
  { code:'US', name:'United States',  dial:'1',   flag:'🇺🇸', fmt:'us',    max:10 },
  { code:'CA', name:'Canada',         dial:'1',   flag:'🇨🇦', fmt:'us',    max:10 },
  { code:'GB', name:'United Kingdom', dial:'44',  flag:'🇬🇧', fmt:'uk',    max:10 },
  { code:'IE', name:'Ireland',        dial:'353', flag:'🇮🇪', fmt:'group', max:11 },
  { code:'AU', name:'Australia',      dial:'61',  flag:'🇦🇺', fmt:'group', max:10 },
  { code:'NZ', name:'New Zealand',    dial:'64',  flag:'🇳🇿', fmt:'group', max:10 },
  { code:'IN', name:'India',          dial:'91',  flag:'🇮🇳', fmt:'group', max:10 },
  { code:'ZA', name:'South Africa',   dial:'27',  flag:'🇿🇦', fmt:'group', max:10 },
  { code:'MX', name:'Mexico',         dial:'52',  flag:'🇲🇽', fmt:'group', max:10 },
  { code:'BR', name:'Brazil',         dial:'55',  flag:'🇧🇷', fmt:'group', max:11 },
  { code:'AR', name:'Argentina',      dial:'54',  flag:'🇦🇷', fmt:'group', max:11 },
  { code:'FR', name:'France',         dial:'33',  flag:'🇫🇷', fmt:'group', max:10 },
  { code:'DE', name:'Germany',        dial:'49',  flag:'🇩🇪', fmt:'group', max:12 },
  { code:'ES', name:'Spain',          dial:'34',  flag:'🇪🇸', fmt:'group', max:9  },
  { code:'IT', name:'Italy',          dial:'39',  flag:'🇮🇹', fmt:'group', max:11 },
  { code:'NL', name:'Netherlands',    dial:'31',  flag:'🇳🇱', fmt:'group', max:9  },
  { code:'SE', name:'Sweden',         dial:'46',  flag:'🇸🇪', fmt:'group', max:10 },
  { code:'AE', name:'UAE',            dial:'971', flag:'🇦🇪', fmt:'group', max:9  },
  { code:'SG', name:'Singapore',      dial:'65',  flag:'🇸🇬', fmt:'group', max:8  },
  { code:'JP', name:'Japan',          dial:'81',  flag:'🇯🇵', fmt:'group', max:10 },
];
const DEFAULT_COUNTRY = COUNTRIES[0];

// If the user pastes/types a "+CC ..." prefix, swap the dropdown to that
// country and consume the prefix. Tries longest dial first so +353 wins
// over the +3/+35 false leads.
const detectCountryFromInput = (s) => {
  const m = String(s || '').match(/^\s*\+(\d{1,4})(.*)$/);
  if (!m) return null;
  const prefix = m[1];
  for (let len = 3; len >= 1; len--) {
    const head = prefix.slice(0, len);
    const c = COUNTRIES.find(x => x.dial === head);
    if (c) return { country: c, rest: prefix.slice(len) + (m[2] || '') };
  }
  return null;
};

// Cosmetic separators for the local part. Pure formatting — caller is
// responsible for keeping the digits-only state in sync.
const formatLocal = (digits, fmt) => {
  const d = String(digits || '').replace(/\D/g, '');
  if (fmt === 'us') {
    const a = d.slice(0,3), b = d.slice(3,6), c = d.slice(6,10);
    if (d.length <= 3) return a;
    if (d.length <= 6) return `${a}-${b}`;
    return `${a}-${b}-${c}`;
  }
  if (fmt === 'uk') {
    const a = d.slice(0,4), b = d.slice(4,7), c = d.slice(7,10);
    return [a,b,c].filter(Boolean).join(' ');
  }
  return d.replace(/(\d{3})(?=\d)/g, '$1 ');
};

// "+1 212-867-5309" — stored in student_info.phone for the admin to read.
const formatPhoneDisplay = (country, localDigits) =>
  `+${country.dial}${localDigits ? ' ' + formatLocal(localDigits, country.fmt) : ''}`;

// Strict for NANP (must be 10), lenient elsewhere — global formats vary.
const phoneOk = (country, localDigits) =>
  country.fmt === 'us' ? localDigits.length === 10 : localDigits.length >= 7;

const isValidEmail = (s) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(s || '').trim());

// ── format helpers ──────────────────────────────────────────────────
const stdFmtDate = (iso) => {
  if (!iso) return '';
  const d = new Date(iso);
  return `${STD_WDAYS[d.getDay()]} ${STD_MONTHS[d.getMonth()]} ${d.getDate()}`;
};
const stdH = (h) => h === 12 ? '12:00 PM' : h > 12 ? `${h-12}:00 PM` : `${h}:00 AM`;
const stdHFromIso = (iso) => stdH(new Date(iso).getHours());

const stdBtnStyle = (primary) => ({
  display:'block', width:'100%', padding:'18px 0', borderRadius:14,
  border: primary ? 'none' : '2.5px solid oklch(86% 0.02 265)',
  background: primary ? 'oklch(22% 0.06 265)' : '#fff',
  color: primary ? '#fff' : 'oklch(30% 0.05 265)',
  fontSize:17, fontWeight:700, cursor:'pointer',
  fontFamily:"'Plus Jakarta Sans', sans-serif", transition:'all 0.15s',
  textAlign:'center', textDecoration:'none',
});

const stdFieldStyle = {
  width:'100%', padding:'18px 20px', borderRadius:14,
  border:'2.5px solid oklch(88% 0.015 265)', fontSize:18,
  fontFamily:"'Plus Jakarta Sans', sans-serif",
  color:'oklch(18% 0.04 265)', background:'#fff',
  outline:'none', boxSizing:'border-box', lineHeight:1,
};

const STD_SUBJECTS = (window.SITE_DATA?.categories || []).flatMap(c => c.subjects || []);

// ── Availability picker (multi-block) ───────────────────────────────
// Format: { 0: { enabled, blocks: [{from,to}, ...] }, 1: ..., 6: ... }  (0=Mon … 6=Sun)
const AVAIL_DAYS  = ['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday'];
const AVAIL_HOURS = [7,8,9,10,11,12,13,14,15,16,17,18,19,20];
const availFmtH   = h => h === 12 ? '12:00 PM' : h > 12 ? `${h-12}:00 PM` : `${h}:00 AM`;
const defaultDay  = () => ({ enabled:false, blocks:[{from:9,to:17}] });

// Project a wall-clock weekly availability map from anchor TZ → viewer TZ.
// Input shape: { 0: { enabled, blocks: [{from,to}] }, ... }  (0=Mon … 6=Sun)
// Output: same shape, projected so the displayed hours land in viewer TZ.
//
// Why this matters: when an admin in Beirut sets "Mon 9 AM – 5 PM" for a
// student, the JSON is stored as wall-clock 9-17 anchored to Beirut. A
// student in NY opening their dashboard would otherwise see "9 AM – 5 PM"
// (the raw digits) which is meaningless to them. With projection they
// see "Mon 2 AM – 10 AM" (the equivalent NY time).
//
// Edge cases handled:
//  - Day shift: 1 AM Mon Beirut → ~6 PM Sun NY. We split the block across
//    two viewer-TZ days and merge contiguous blocks per day.
//  - Half-hour offsets (India, Newfoundland): we round to the nearest hour.
//    Worst case the displayed hour is off by 30 min, but the calendar
//    industry standard for weekly availability display is hour buckets.
const projectAvailability = (avail, anchorTz, viewerTz) => {
  if (!avail || !anchorTz || !viewerTz || anchorTz === viewerTz) return avail;
  const T = window.Calendar?.time;
  if (!T?.makeDate || !T?.partsIn) return avail;
  // 2024-01-01 was a Monday. di=0 → Jan 1, di=1 → Jan 2, … di=6 → Jan 7.
  const dayOfMonthForDi = di => 1 + di;
  // Sun-first JS weekday (0-6) → Mon-first di (0-6).
  const dowToDi = d => (d + 6) % 7;
  const out = Object.fromEntries([0,1,2,3,4,5,6].map(i => [i, { enabled:false, blocks:[] }]));
  for (const [diStr, day] of Object.entries(avail)) {
    const di = Number(diStr);
    if (!day?.enabled || !(day.blocks || []).length) continue;
    for (const block of day.blocks) {
      if (typeof block.from !== 'number' || typeof block.to !== 'number') continue;
      const fromAbs = T.makeDate(2024, 1, dayOfMonthForDi(di), block.from, 0, anchorTz);
      const toAbs   = T.makeDate(2024, 1, dayOfMonthForDi(di), block.to,   0, anchorTz);
      const fp = T.partsIn(fromAbs, viewerTz);
      const tp = T.partsIn(toAbs,   viewerTz);
      const fromDi = dowToDi(new Date(Date.UTC(fp.year, fp.month-1, fp.day)).getUTCDay());
      const toDi   = dowToDi(new Date(Date.UTC(tp.year, tp.month-1, tp.day)).getUTCDay());
      const fromH  = fp.hour + (fp.minute >= 30 ? 1 : 0);
      const toH    = tp.hour + (tp.minute >= 30 ? 1 : 0);
      if (fromDi === toDi) {
        out[fromDi].enabled = true;
        out[fromDi].blocks.push({ from:fromH, to:toH });
      } else {
        out[fromDi].enabled = true;
        out[fromDi].blocks.push({ from:fromH, to:24 });
        if (toH > 0) { out[toDi].enabled = true; out[toDi].blocks.push({ from:0, to:toH }); }
      }
    }
  }
  // Merge contiguous/overlapping blocks per day for a clean display.
  for (const k of Object.keys(out)) {
    const blocks = out[k].blocks.sort((a,b) => a.from - b.from);
    const merged = [];
    for (const b of blocks) {
      if (merged.length && b.from <= merged[merged.length-1].to) {
        merged[merged.length-1].to = Math.max(merged[merged.length-1].to, b.to);
      } else { merged.push({ ...b }); }
    }
    out[k].blocks = merged;
    if (!merged.length) out[k].enabled = false;
  }
  return out;
};

const AvailabilityStep = ({ avail, note, onChangeAvail, onChangeNote }) => {
  const toggle = i => {
    const curr = avail[i] || defaultDay();
    onChangeAvail({ ...avail, [i]: { ...curr, enabled:!curr.enabled } });
  };
  const setBlockTime = (di, bi, key, val) => {
    const curr = avail[di] || { ...defaultDay(), enabled:true };
    const blocks = (curr.blocks || [{from:9,to:17}]).map((b,idx) => idx===bi ? {...b, [key]:Number(val)} : b);
    onChangeAvail({ ...avail, [di]: {...curr, blocks} });
  };
  const addBlock = di => {
    const curr = avail[di] || { ...defaultDay(), enabled:true };
    const blocks = curr.blocks || [{from:9,to:17}];
    const lastTo = blocks[blocks.length-1]?.to || 12;
    const from = Math.min(lastTo, 19), to = Math.min(from+3, 20);
    onChangeAvail({ ...avail, [di]: {...curr, blocks:[...blocks, {from,to}]} });
  };
  const removeBlock = (di, bi) => {
    const curr = avail[di];
    if (!curr) return;
    const blocks = (curr.blocks || []).filter((_,idx) => idx!==bi);
    onChangeAvail({ ...avail, [di]: {...curr, blocks: blocks.length ? blocks : [{from:9,to:17}]} });
  };

  const selS = { padding:'7px 10px', borderRadius:8, border:'1.5px solid oklch(86% 0.02 265)', fontSize:14, fontFamily:"'Plus Jakarta Sans', sans-serif", color:'oklch(22% 0.06 265)', background:'#fff', cursor:'pointer', outline:'none' };

  return (
    <div>
      <div style={{ display:'flex', flexDirection:'column', gap:8, marginBottom:20 }}>
        {AVAIL_DAYS.map((day, i) => {
          const d = avail[i] || defaultDay();
          const blocks = d.blocks || [{from:9,to:17}];
          return (
            <div key={i} style={{ borderRadius:12, border:`2px solid ${d.enabled?'oklch(22% 0.06 265)':'oklch(88% 0.015 265)'}`, background:d.enabled?'oklch(97% 0.01 265)':'#fff', transition:'all 0.15s', overflow:'hidden' }}>
              <div style={{ display:'flex', alignItems:'center', gap:12, padding:'12px 16px' }}>
                <button type="button" onClick={() => toggle(i)} style={{ width:44, height:24, borderRadius:12, border:'none', background:d.enabled?'oklch(22% 0.06 265)':'oklch(84% 0.01 265)', cursor:'pointer', position:'relative', flexShrink:0, transition:'background 0.2s' }}>
                  <div style={{ width:18, height:18, borderRadius:'50%', background:'#fff', position:'absolute', top:3, left:d.enabled?23:3, transition:'left 0.2s', boxShadow:'0 1px 3px rgba(0,0,0,0.2)' }} />
                </button>
                <span style={{ fontSize:16, fontWeight:d.enabled?700:500, color:d.enabled?'oklch(18% 0.04 265)':'oklch(62% 0.03 265)', minWidth:102, flexShrink:0 }}>{day}</span>
                {!d.enabled && <span style={{ fontSize:13, color:'oklch(70% 0.02 265)', fontStyle:'italic' }}>Not available</span>}
              </div>
              {d.enabled && (
                <div style={{ padding:'0 16px 14px 16px', display:'flex', flexDirection:'column', gap:8 }}>
                  {blocks.map((block, bi) => (
                    <div key={bi} style={{ display:'flex', alignItems:'center', gap:8 }}>
                      <select value={block.from} onChange={e => setBlockTime(i,bi,'from',e.target.value)} style={selS}>
                        {AVAIL_HOURS.map(h => <option key={h} value={h}>{availFmtH(h)}</option>)}
                      </select>
                      <span style={{ fontSize:14, color:'oklch(55% 0.03 265)' }}>to</span>
                      <select value={block.to} onChange={e => setBlockTime(i,bi,'to',e.target.value)} style={selS}>
                        {AVAIL_HOURS.filter(h => h > block.from).map(h => <option key={h} value={h}>{availFmtH(h)}</option>)}
                      </select>
                      {blocks.length > 1 && (
                        <button type="button" onClick={() => removeBlock(i,bi)} style={{ background:'none', border:'none', cursor:'pointer', fontSize:18, color:'oklch(68% 0.02 265)', lineHeight:1, padding:'0 4px' }}>×</button>
                      )}
                    </div>
                  ))}
                  <button type="button" onClick={() => addBlock(i)} style={{ alignSelf:'flex-start', background:'none', border:'none', fontSize:13, fontWeight:600, color:'oklch(36% 0.08 265)', cursor:'pointer', fontFamily:"'Plus Jakarta Sans', sans-serif", padding:'2px 0' }}>+ Add another block</button>
                </div>
              )}
            </div>
          );
        })}
      </div>
    </div>
  );
};

// ── Cancel Confirm ──────────────────────────────────────────────────
const CancelConfirm = ({ lesson, onConfirm, onClose }) => (
  <div style={{ position:'fixed', inset:0, background:'rgba(0,0,0,0.25)', display:'flex', alignItems:'center', justifyContent:'center', zIndex:8000, padding:24 }} onClick={onClose}>
    <div style={{ background:'#fff', borderRadius:20, padding:'32px 28px', maxWidth:360, width:'100%', boxShadow:'0 20px 60px rgba(0,0,0,0.18)', fontFamily:"'Plus Jakarta Sans', sans-serif" }} onClick={e => e.stopPropagation()}>
      <div style={{ fontWeight:700, fontSize:20, color:'oklch(18% 0.03 265)', marginBottom:10 }}>Cancel this lesson?</div>
      <div style={{ fontSize:16, color:'oklch(52% 0.03 265)', lineHeight:1.7, marginBottom:24 }}>
        {stdFmtDate(lesson.scheduledAt)} at {stdHFromIso(lesson.scheduledAt)}<br />
        <span style={{ fontSize:14 }}>Please cancel at least 24 hours in advance.</span>
      </div>
      <div style={{ display:'flex', flexDirection:'column', gap:10 }}>
        <button onClick={onConfirm} style={{ padding:'16px', borderRadius:12, border:'none', background:'oklch(95% 0.06 25)', color:'oklch(34% 0.1 25)', fontSize:16, fontWeight:700, cursor:'pointer', fontFamily:"'Plus Jakarta Sans', sans-serif" }}>Yes, cancel it</button>
        <button onClick={onClose}   style={{ padding:'16px', borderRadius:12, border:'2px solid oklch(88% 0.01 265)', background:'#fff', color:'oklch(36% 0.04 265)', fontSize:16, fontWeight:600, cursor:'pointer', fontFamily:"'Plus Jakarta Sans', sans-serif" }}>Keep it</button>
      </div>
    </div>
  </div>
);

// ── Reschedule modal (student picks a new slot from instructor's avail) ──
const StudentPortal = ({ onBack }) => {
  const auth = window.useAuth();
  const data = window.useDashboardData();
  const [view, setView] = React.useState('checking');

  React.useEffect(() => {
    if (auth.loading) { setView('checking'); return; }
    if (!auth.user) { setView('login'); return; }
    // Admin View-As: app.jsx mounted us with window.MASTERY.viewAs set to
    // the target student. The admin's own auth.profile is null/'admin' so
    // the role gate below would otherwise drop us on the login UI. Skip
    // it: data.refresh() reads viewAs and pulls the impersonated user's
    // rows via the admin_as_user_* RPCs (gated server-side on is_admin()).
    if (window.MASTERY?.viewAs?.kind === 'student') {
      data.refresh();
      setView('home');
      return;
    }
    // FLASH FIX: if we have a user but the profile fetch is still pending,
    // keep showing 'checking'. The profile/admin lookups complete a moment
    // after auth.loading flips to false (see bootstrap in logic.js), and
    // flipping to 'login' during that window flashes the student login UI
    // even when the user is actually a logged-in student or instructor.
    if (auth.user && !auth.profile && !auth.isAdmin) { setView('checking'); return; }
    if (auth.profile?.role === 'student') {
      data.refresh();
      setView('home');
    } else if (auth.profile?.role === 'instructor') {
      setView('wrong-role');
    } else {
      setView('login');
    }
  }, [auth.loading, auth.user?.id, auth.profile?.role, auth.isAdmin]);

  const handleSignOut = async () => {
    // In View-As mode the admin is the actual signed-in user. Calling
    // supa.auth.signOut() would log THEM out — not what they want when
    // they click "Sign out" inside the embedded student dashboard. Just
    // exit the view; the parent (app.jsx closeView) drops the URL param
    // and returns the admin to their dashboard.
    if (window.MASTERY?.viewAs) { onBack(); return; }
    try { await window.supa.auth.signOut(); } catch (e) {}
    onBack();
  };

  if (view === 'checking') {
    return <div style={{ minHeight:'100vh', display:'flex', alignItems:'center', justifyContent:'center', background:'oklch(98.5% 0.007 60)', color:'oklch(55% 0.03 265)', fontFamily:"'Plus Jakarta Sans', sans-serif" }}>Loading…</div>;
  }
  if (view === 'wrong-role') {
    return (
      <div style={{ minHeight:'100vh', display:'flex', alignItems:'center', justifyContent:'center', background:'oklch(98.5% 0.007 60)', fontFamily:"'Plus Jakarta Sans', sans-serif", padding:'24px' }}>
        <div style={{ maxWidth:400, textAlign:'center' }}>
          <div style={{ fontFamily:"'Cormorant Garamond', serif", fontSize:22, fontWeight:700, color:'oklch(22% 0.06 265)', marginBottom:14 }}>Wrong portal</div>
          <div style={{ fontSize:15, color:'oklch(50% 0.03 265)', marginBottom:24, lineHeight:1.7 }}>You're signed in as an instructor. Sign out below to access the student portal.</div>
          <button onClick={handleSignOut} style={{ ...stdBtnStyle(true), display:'inline-block', width:'auto', padding:'14px 28px', fontSize:15 }}>Sign out</button>
        </div>
      </div>
    );
  }
  if (view === 'home') {
    return <window.StudentHome onBack={handleSignOut} />;
  }
  return <window.StudentLogin onLogin={() => setView('home')} onBack={onBack} />;
};

Object.assign(window, {
  // Public components mounted by app.jsx
  StudentPortal,
  // Helpers + small modals + sibling components published so the
  // extracted student-home.jsx, student-onboarding.jsx, and
  // student-login.jsx files can pick them up via window. (The
  // Babel-standalone setup gives each .jsx its own lexical scope,
  // so cross-file sharing has to go through window.)
  COUNTRIES,
  DEFAULT_COUNTRY,
  phoneDigits,
  phoneToEmail,
  phoneCredKey,
  newRandomPassword,
  persistPhoneCred,
  readPhoneCred,
  stdBtnStyle,
  stdFieldStyle,
  AvailabilityStep,
  CancelConfirm,
});
// StudentLogin lives in student-login.jsx and StudentOnboarding lives
// in student-onboarding.jsx — both self-publish onto window at load.
