function useParallaxTilt() {
  const [tilt, setTilt] = React.useState({ x: 0, y: 0 });
  const ref = React.useRef(null);

  function handleMouseMove(e) {
    const rect = ref.current.getBoundingClientRect();
    const nx = (e.clientX - rect.left - rect.width  / 2) / (rect.width  / 2);
    const ny = (e.clientY - rect.top  - rect.height / 2) / (rect.height / 2);
    setTilt({ x: nx, y: ny });
  }

  function handleMouseLeave() { setTilt({ x: 0, y: 0 }); }

  return { ref, tilt, handleMouseMove, handleMouseLeave };
}
window.useParallaxTilt = useParallaxTilt;

// Sparse, dim starfield behind every student screen: stars twinkle slowly and drift
// up-right along the beam's axis. Star state lives at module level (normalised 0..1
// coords) so the sky carries on unbroken when the screen changes instead of reshuffling.
const AYANI_STARS = [];
function ayaniEnsureStars(count) {
  while (AYANI_STARS.length < count) {
    const depth = Math.random(); // 0 = far (small, dim, slow) .. 1 = near
    AYANI_STARS.push({
      x: Math.random(), y: Math.random(),
      r: 0.35 + depth * 0.75,
      a: 0.12 + depth * 0.4,
      speed: 0.002 + depth * 0.006,          // viewport-heights per second
      tw: (Math.PI * 2) / (4 + Math.random() * 7), // 4-11s twinkle period
      phase: Math.random() * Math.PI * 2,
    });
  }
}

// `meteors` (Login only) adds an occasional thin streak crossing parallel to the beam.
// On Login the canvas is clipped left of the form column (styles.css), so streaks only
// spawn in the visible part of the sky.
function AyaniStarfield({ meteors }) {
  const ref = React.useRef(null);
  React.useEffect(() => {
    const canvas = ref.current;
    const ctx = canvas.getContext('2d');
    const still = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    let w = 0, h = 0, skyW = 0, raf = 0, last = performance.now();
    let streaks = [], nextStreak = 1.5;

    function resize() {
      const dpr = Math.min(window.devicePixelRatio || 1, 2);
      w = canvas.clientWidth; h = canvas.clientHeight;
      canvas.width = w * dpr; canvas.height = h * dpr;
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
      const cs = getComputedStyle(canvas);
      skyW = w - (cs.clipPath !== 'none' ? parseFloat(cs.getPropertyValue('--ay-login-col')) || 0 : 0);
      ayaniEnsureStars(Math.round((w * h) / 14000)); // ~100 stars at 1600x900
      draw(last);
    }

    function draw(now) {
      ctx.clearRect(0, 0, w, h);
      const count = Math.round((w * h) / 14000);
      for (let i = 0; i < count; i++) {
        const s = AYANI_STARS[i];
        const alpha = still ? s.a : s.a * (0.55 + 0.45 * Math.sin(now / 1000 * s.tw + s.phase));
        ctx.globalAlpha = alpha;
        ctx.fillStyle = '#e6e6e6';
        ctx.beginPath();
        ctx.arc(s.x * w, s.y * h, s.r, 0, Math.PI * 2);
        ctx.fill();
      }
      ctx.globalAlpha = 1;
      for (const m of streaks) {
        const fade = Math.sin(Math.min(1, m.life / m.max) * Math.PI);
        const tx = m.x - 0.72 * m.len, ty = m.y + 0.69 * m.len;
        const g = ctx.createLinearGradient(tx, ty, m.x, m.y);
        g.addColorStop(0, 'rgba(240,240,240,0)');
        g.addColorStop(1, `rgba(240,240,240,${0.75 * fade})`);
        ctx.strokeStyle = g; ctx.lineWidth = 1.1; ctx.lineCap = 'round';
        ctx.beginPath(); ctx.moveTo(tx, ty); ctx.lineTo(m.x, m.y); ctx.stroke();
        ctx.fillStyle = `rgba(255,255,255,${0.9 * fade})`;
        ctx.beginPath(); ctx.arc(m.x, m.y, 1.1, 0, Math.PI * 2); ctx.fill();
      }
    }

    function tick(now) {
      const dt = Math.min((now - last) / 1000, 0.1);
      last = now;
      const aspect = h / (w || 1);
      for (const s of AYANI_STARS) {
        s.x += s.speed * aspect * dt * 0.7071;
        s.y -= s.speed * dt * 0.7071;
        if (s.x > 1) s.x -= 1;
        if (s.y < 0) s.y += 1;
      }
      if (meteors) {
        nextStreak -= dt;
        if (nextStreak <= 0) {
          nextStreak = 2.5 + Math.random() * 4;
          streaks.push({
            x: (0.03 + Math.random() * 0.72) * skyW, y: (0.35 + Math.random() * 0.7) * h,
            v: (0.55 + Math.random() * 0.35) * w, len: (0.08 + Math.random() * 0.08) * w,
            life: 0, max: 0.7 + Math.random() * 0.4,
          });
        }
        for (const m of streaks) { m.life += dt; m.x += m.v * 0.72 * dt; m.y -= m.v * 0.69 * dt; }
        streaks = streaks.filter(m => m.life < m.max);
      }
      draw(now);
      raf = requestAnimationFrame(tick);
    }

    const ro = new ResizeObserver(resize);
    ro.observe(canvas);
    resize();
    if (!still) raf = requestAnimationFrame(tick);
    return () => { cancelAnimationFrame(raf); ro.disconnect(); };
  }, [meteors]);
  return <canvas ref={ref} className="ay-starfield" aria-hidden="true" />;
}
window.AyaniStarfield = AyaniStarfield;

// "Tide" on the beam: a band of light parallel to the beam rolls across its width, edge
// to edge, every 7s. Drawn on a canvas over the <img> and clipped to the beam's own light
// by a luminance mask built once from the image, so black stays black. Works in the
// image's own coordinates (1920x1080 at half resolution) and is mapped onto the <img>'s
// rendered box with the same cover / left-center crop, so it tracks the art on every
// layout, desktop and phone.
const AYANI_TIDE = { w: 960, h: 540, period: 7, mask: null, fx: null };
function ayaniTideLayers(img) {
  if (AYANI_TIDE.mask) return true;
  const { w, h } = AYANI_TIDE;
  const mask = document.createElement('canvas'); mask.width = w; mask.height = h;
  const mx = mask.getContext('2d', { willReadFrequently: true });
  mx.drawImage(img, 0, 0, w, h);
  let d;
  try { d = mx.getImageData(0, 0, w, h); } catch (e) { return false; } // tainted (file://)
  for (let i = 0; i < d.data.length; i += 4) {
    const l = (d.data[i] * 0.3 + d.data[i + 1] * 0.59 + d.data[i + 2] * 0.11) / 255;
    d.data[i] = d.data[i + 1] = d.data[i + 2] = 255;
    d.data[i + 3] = Math.pow(l, 1.25) * 255;
  }
  mx.putImageData(d, 0, 0);
  const fx = document.createElement('canvas'); fx.width = w; fx.height = h;
  AYANI_TIDE.mask = mask; AYANI_TIDE.fx = fx;
  return true;
}

function ayaniDrawTide(canvas, img, now) {
  const { w: FW, h: FH, period, mask, fx } = AYANI_TIDE;
  const fxx = fx.getContext('2d');
  const ph = (now / 1000 % period) / period;
  const e = ph < 0.5 ? 2 * ph * ph : 1 - Math.pow(-2 * ph + 2, 2) / 2;
  // Beam axis centre and its across-the-beam unit normal, in half-res image px.
  const cx = 205, cy = 222.5, nx = 0.7137, ny = 0.7005, half = 100;
  const off = (-190 + e * 380) * 0.5;
  const gx = cx + nx * off, gy = cy + ny * off;
  fxx.globalCompositeOperation = 'source-over';
  fxx.clearRect(0, 0, FW, FH);
  const g = fxx.createLinearGradient(gx - nx * half, gy - ny * half, gx + nx * half, gy + ny * half);
  g.addColorStop(0, 'rgba(255,255,255,0)');
  g.addColorStop(0.5, 'rgba(255,255,255,0.8)');
  g.addColorStop(1, 'rgba(255,255,255,0)');
  fxx.fillStyle = g; fxx.fillRect(0, 0, FW, FH);
  fxx.globalCompositeOperation = 'destination-in';
  fxx.drawImage(mask, 0, 0);

  const ctx = canvas.getContext('2d');
  const dpr = canvas.width / (canvas.clientWidth || 1);
  ctx.setTransform(1, 0, 0, 1, 0, 0);
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  // object-fit: cover; object-position: left center -- mirrors `.ay-login-art img`.
  const iw = img.offsetWidth, ih = img.offsetHeight;
  const sc = Math.max(iw / 1920, ih / 1080);
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  ctx.globalAlpha = Math.sin(ph * Math.PI) * 0.9 + 0.1;
  ctx.drawImage(fx, img.offsetLeft, img.offsetTop + (ih - 1080 * sc) / 2, 1920 * sc, 1080 * sc);
  ctx.globalAlpha = 1;
}

// The background shared by Login/Menu/Channel/Video: the starfield plus the beam-and-
// moon art with the Tide light rolling across the beam (styles: `.ay-login-art` in
// styles.css). `reveal` adds the one-off draw-in on load and `meteors` the shooting
// stars -- both Login only.
function AyaniBeam({ reveal, meteors }) {
  const artRef = React.useRef(null), imgRef = React.useRef(null), fxRef = React.useRef(null);
  React.useEffect(() => {
    if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
    const art = artRef.current, img = imgRef.current, canvas = fxRef.current;
    let raf = 0, ro = null, stopped = false;
    function size() {
      const dpr = Math.min(window.devicePixelRatio || 1, 2);
      canvas.width = Math.round(art.clientWidth * dpr);
      canvas.height = Math.round(art.clientHeight * dpr);
    }
    function tick(now) {
      ayaniDrawTide(canvas, img, now);
      raf = requestAnimationFrame(tick);
    }
    function start() {
      if (stopped || !ayaniTideLayers(img)) return;
      ro = new ResizeObserver(size); ro.observe(art); size();
      raf = requestAnimationFrame(tick);
    }
    if (img.complete && img.naturalWidth) start(); else img.addEventListener('load', start, { once: true });
    return () => { stopped = true; cancelAnimationFrame(raf); if (ro) ro.disconnect(); img.removeEventListener('load', start); };
  }, []);
  return (
    <>
      <AyaniStarfield meteors={meteors} />
      <div ref={artRef} className={'ay-login-bg ay-login-art' + (reveal ? ' ay-login-art--reveal' : '')}>
        <img ref={imgRef} src="assets/beamAndMoon.jpg" alt="" />
        <canvas ref={fxRef} className="ay-beam-fx" aria-hidden="true" />
      </div>
    </>
  );
}
window.AyaniBeam = AyaniBeam;

// Returns the current authenticated user's profile ({ id, role, status, email,
// username }) or null if there's no session / no profile row. Shared by LoginScreen
// (post-sign-in routing) and App (session restore on load).
async function ayaniFetchProfile() {
  const client = window.supabaseClient;
  const { data: { user } } = await client.auth.getUser();
  if (!user) return null;
  const { data, error } = await client
    .from('profiles')
    .select('id, role, status, email, username')
    .eq('id', user.id)
    .single();
  if (error || !data) return null;
  return data;
}
window.ayaniFetchProfile = ayaniFetchProfile;

// Modules for MenuScreen (BUILD_PLAN.md 3.1). `locked` comes from whether the
// student has a module_access grant (RLS-gated); `progress` is the average pct
// across that module's lessons (0 for locked modules -- RLS returns no lessons/
// progress rows for a module the student can't access anyway).
async function ayaniFetchModules() {
  const client = window.supabaseClient;
  const [
    { data: modules, error: modErr },
    { data: access, error: accErr },
    { data: lessons, error: lesErr },
    { data: progress, error: progErr },
  ] = await Promise.all([
    client.from('modules').select('id, code, title, order').order('order'),
    client.from('module_access').select('module_id'),
    client.from('lessons').select('id, module_id'),
    client.from('progress').select('lesson_id, pct'),
  ]);
  if (modErr) throw modErr;
  if (accErr) throw accErr;
  if (lesErr) throw lesErr;
  if (progErr) throw progErr;

  const accessSet = new Set((access || []).map(a => a.module_id));
  const pctByLesson = new Map((progress || []).map(p => [p.lesson_id, Number(p.pct)]));

  return (modules || []).map(m => {
    const locked = !accessSet.has(m.id);
    const modLessons = locked ? [] : (lessons || []).filter(l => l.module_id === m.id);
    const progressPct = modLessons.length
      ? Math.round(modLessons.reduce((sum, l) => sum + (pctByLesson.get(l.id) || 0), 0) / modLessons.length)
      : 0;
    return { id: m.id, code: m.code, title: m.title, order: m.order, locked, progress: progressPct };
  });
}
window.ayaniFetchModules = ayaniFetchModules;

// Lessons for ChannelScreen (BUILD_PLAN.md 3.1), by module code. Returns
// { module, lessons } where each lesson carries its own watch `progress` pct.
// RLS only returns lessons for a module the student has access to, so an
// unauthorized/locked module simply comes back with an empty lesson list.
async function ayaniFetchLessons(moduleCode) {
  const client = window.supabaseClient;
  const { data: mod, error: modErr } = await client
    .from('modules')
    .select('id, code, title, order')
    .eq('code', moduleCode)
    .maybeSingle();
  if (modErr) throw modErr;
  if (!mod) return { module: null, lessons: [] };

  const [{ data: lessons, error: lesErr }, { data: progress, error: progErr }] = await Promise.all([
    client.from('lessons').select('id, n, title, tag, divider_label, order').eq('module_id', mod.id).order('order'),
    client.from('progress').select('lesson_id, pct'),
  ]);
  if (lesErr) throw lesErr;
  if (progErr) throw progErr;

  const pctByLesson = new Map((progress || []).map(p => [p.lesson_id, Number(p.pct)]));
  const withProgress = (lessons || []).map(l => ({ ...l, progress: pctByLesson.get(l.id) || 0 }));
  return { module: mod, lessons: withProgress };
}
window.ayaniFetchLessons = ayaniFetchLessons;

// Login activity (BUILD_PLAN.md 4.4): records one { timestamp, user agent } row for
// the signed-in user per browser tab -- called on sign-in and on session restore so
// "last seen" reflects visits, not just explicit logins. Nothing else is captured
// (no IP, no location). Fire-and-forget; a failed insert never affects the user.
async function ayaniLogActivity() {
  const KEY = 'ayani_activity_logged'; // holds the user id already logged in this tab
  try {
    const client = window.supabaseClient;
    const { data: { user } } = await client.auth.getUser();
    if (!user) return;
    try { if (sessionStorage.getItem(KEY) === user.id) return; } catch (_) {}
    const { error } = await client.from('activity_logs').insert({
      student_id: user.id,
      user_agent: navigator.userAgent.slice(0, 500),
    });
    if (!error) { try { sessionStorage.setItem(KEY, user.id); } catch (_) {} }
  } catch (_) {}
}
window.ayaniLogActivity = ayaniLogActivity;

// Documents for a lesson's Templates/Extra tabs (BUILD_PLAN.md 5.1). RLS only
// returns rows for lessons in a module the student has access to, so a locked
// lesson simply has no documents. Downloads use short-lived signed URLs minted by
// the client -- storage.objects' read policy is what actually gates access.
async function ayaniFetchDocuments(lessonId) {
  const { data, error } = await window.supabaseClient
    .from('documents')
    .select('id, label, kind, file_path, filename')
    .eq('lesson_id', lessonId)
    .order('label');
  if (error) throw error;
  return data || [];
}
window.ayaniFetchDocuments = ayaniFetchDocuments;

async function ayaniDocumentDownloadUrl(doc) {
  const { data, error } = await window.supabaseClient.storage
    .from('documents')
    .createSignedUrl(doc.file_path, 60, { download: doc.filename });
  if (error) throw error;
  return data.signedUrl;
}
window.ayaniDocumentDownloadUrl = ayaniDocumentDownloadUrl;

// Inline (no attachment disposition) URL for previewing a PDF template in an iframe.
// Longer-lived than the download URL since the viewer may re-request pages.
async function ayaniDocumentPreviewUrl(doc) {
  const { data, error } = await window.supabaseClient.storage
    .from('documents')
    .createSignedUrl(doc.file_path, 3600);
  if (error) throw error;
  return data.signedUrl;
}
window.ayaniDocumentPreviewUrl = ayaniDocumentPreviewUrl;
