/* global React */
/* ============================================================
   Campus backend — Supabase Auth + course API.

   · CampusAuth: real email/password accounts (Supabase Auth).
   · CourseStore: enrolment, page completion, bookmarks and timed quizzes,
     all persisted through the campus_* functions in Postgres
     (supabase/migrations/20261114200000_campus_course_assessment.sql).
     The database grades quizzes, enforces time limits and attempts, and
     holds the answer keys; the browser never sees them before they are due.

   Reads are synchronous from an in-memory cache (filled by load()) so pages
   render immediately; writes are async and update the cache from the
   server's answer. Components re-render on the "bh-progress" event.
   ============================================================ */

const BH_CONFIG = window.BH_SUPABASE || {};
const hasValue = v => typeof v === "string" && v && !v.startsWith("%");
const sb = hasValue(BH_CONFIG.url) && hasValue(BH_CONFIG.key) && window.supabase
  ? window.supabase.createClient(BH_CONFIG.url, BH_CONFIG.key, { auth: { persistSession: true, autoRefreshToken: true, detectSessionInUrl: true } })
  : null;

const emitProgress = () => window.dispatchEvent(new Event("bh-progress"));
const toast = (msg, tone = "error") => window.dispatchEvent(new CustomEvent("bh-toast", { detail: { msg, tone } }));

const ERROR_TEXT = {
  not_authenticated: "Tu sesión ha caducado. Inicia sesión de nuevo.",
  not_enrolled: "Inscríbete en el curso para continuar.",
  course_not_found: "Este curso no está disponible en este momento.",
  lesson_not_found: "Esta página no está disponible en este momento.",
  quiz_not_found: "Este test no está disponible en este momento.",
  quiz_completes_by_passing: "Los tests se completan al aprobarlos.",
  quiz_already_passed: "Ya has aprobado este test.",
  no_attempts_left: "Has agotado los intentos de este test.",
  time_expired: "Se ha agotado el tiempo del intento.",
  attempt_closed: "Este intento ya está cerrado.",
  attempt_not_found: "No se ha encontrado el intento.",
  backend_unavailable: "El campus no está conectado a su base de datos. Revisa la configuración.",
  notebook_conflict: "Hay una versión más reciente de tus apuntes en otra pestaña.",
  course_not_completed: "Completa todas las actividades del curso para solicitar el certificado.",
  profile_name_required: "Añade tu nombre completo en tu perfil: es el que aparecerá en el certificado.",
  invalid_notebook: "No se pueden guardar estos apuntes. Revisa su tamaño y contenido.",
  "Invalid login credentials": "Email o contraseña incorrectos.",
  "Email not confirmed": "Confirma tu email antes de entrar: revisa tu bandeja de entrada.",
  "User already registered": "Ya existe una cuenta con este email. Inicia sesión.",
  "Password should be at least": "La contraseña debe tener al menos 6 caracteres.",
};
const errorCode = e => {
  const m = e?.message || String(e || "");
  return Object.keys(ERROR_TEXT).find(k => m.includes(k)) || null;
};
const errorText = e => ERROR_TEXT[errorCode(e)] || "No se ha podido conectar con el campus. Comprueba tu conexión e inténtalo de nuevo.";

async function rpc(fn, args) {
  if (!sb) throw new Error("backend_unavailable");
  const { data, error } = await sb.rpc(fn, args);
  if (error) throw error;
  return data;
}

/* ---------------- Auth ---------------- */
const PROFILE_FIELDS = "id, email, full_name, bio, job_title, country, city, locale, timezone, created_at";
const EDITABLE_PROFILE_FIELDS = ["full_name", "bio", "job_title", "country", "city"];
const PENDING_ENROLL = "bh-pending-enroll";
const PENDING_SAVE = "bh-pending-course-save";
const CampusAuth = {
  configured: !!sb,
  user: null,
  ready: false,
  async init() {
    try { window.localStorage.removeItem("bh-campus-session"); } catch {} // old prototype flag
    if (!sb) { CampusAuth.ready = true; window.dispatchEvent(new Event("bh-session")); return; }
    const { data } = await sb.auth.getSession();
    CampusAuth.user = data.session?.user || null;
    CampusAuth.ready = true;
    window.dispatchEvent(new Event("bh-session"));
    if (CampusAuth.user) CampusAuth.afterSignIn();
    // A tab left open for days still counts as access when the student returns to it.
    document.addEventListener("visibilitychange", () => {
      if (document.visibilityState === "visible") CampusAuth.touchLastSeen();
    });
    sb.auth.onAuthStateChange((_event, session) => {
      const before = CampusAuth.user?.id;
      CampusAuth.user = session?.user || null;
      if (before !== CampusAuth.user?.id) {
        window.dispatchEvent(new Event("bh-session"));
        // Supabase holds its auth lock during this callback. Fetch outside it.
        setTimeout(() => {
          CourseStore.reloadAll();
          if (CampusAuth.user) CampusAuth.afterSignIn();
        }, 0);
      }
    });
  },
  // "Último acceso" for administration. The database also throttles to 15 min;
  // this client-side guard just avoids sending the request at all.
  lastSeenSent: { uid: null, at: 0 },
  touchLastSeen() {
    const uid = CampusAuth.user?.id, sent = CampusAuth.lastSeenSent;
    if (!sb || !uid || (sent.uid === uid && Date.now() - sent.at < 15 * 60 * 1000)) return;
    CampusAuth.lastSeenSent = { uid, at: Date.now() };
    rpc("campus_touch_last_seen", {}).catch(() => { CampusAuth.lastSeenSent = { uid: null, at: 0 }; });
  },
  // A course chosen before creating the account is enrolled on first sign-in.
  async afterSignIn() {
    CampusAuth.touchLastSeen();
    CampusAuth.getProfile().catch(() => {}); // real name for the app bar, space and certificates
    let pending = null;
    try { pending = window.localStorage.getItem(PENDING_ENROLL); } catch {}
    if (pending && await CourseStore.enroll(pending)) {
      try { window.localStorage.removeItem(PENDING_ENROLL); } catch {}
    }
    let saved = null;
    try { saved = window.localStorage.getItem(PENDING_SAVE); } catch {}
    if (saved && await CourseStore.setSaved(saved, true)) {
      try { window.localStorage.removeItem(PENDING_SAVE); } catch {}
    }
  },
  rememberEnrollment(courseId) { try { window.localStorage.setItem(PENDING_ENROLL, courseId); } catch {} },
  rememberCourseSave(courseId) { try { window.localStorage.setItem(PENDING_SAVE, courseId); } catch {} },
  async signIn(email, password) {
    if (!sb) return { error: errorText(new Error("backend_unavailable")) };
    const { data, error } = await sb.auth.signInWithPassword({ email, password });
    if (error) return { error: errorText(error) };
    CampusAuth.user = data.user;
    window.dispatchEvent(new Event("bh-session"));
    await CampusAuth.afterSignIn();
    return { user: data.user };
  },
  async signUp(email, password, fullName) {
    if (!sb) return { error: errorText(new Error("backend_unavailable")) };
    const redirect = `${window.location.origin}${window.location.pathname}#/login`;
    const { data, error } = await sb.auth.signUp({ email, password, options: { data: { full_name: fullName }, emailRedirectTo: redirect } });
    if (error) return { error: errorText(error) };
    if (!data.session) return { needsConfirmation: true };
    CampusAuth.user = data.user;
    window.dispatchEvent(new Event("bh-session"));
    await CampusAuth.afterSignIn();
    return { user: data.user };
  },
  async resetPassword(email) {
    if (!sb) return { error: errorText(new Error("backend_unavailable")) };
    const { error } = await sb.auth.resetPasswordForEmail(email, { redirectTo: `${window.location.origin}${window.location.pathname}#/login` });
    return error ? { error: errorText(error) } : { sent: true };
  },
  async signOut() {
    if (sb) await sb.auth.signOut();
    CampusAuth.user = null;
    CampusAuth.profile = null;
    window.dispatchEvent(new Event("bh-session"));
    CourseStore.reloadAll();
  },
  displayName() {
    const u = CampusAuth.user;
    if (CampusAuth.profile?.id === u?.id && CampusAuth.profile?.full_name) return CampusAuth.profile.full_name;
    return u?.user_metadata?.full_name || (u?.email ? u.email.split("@")[0] : "");
  },

  /* The student's own row in public.profiles (RLS: owner only). */
  profile: null,
  async getProfile() {
    if (!sb) throw new Error("backend_unavailable");
    const uid = CampusAuth.user?.id;
    if (!uid) throw new Error("not_authenticated");
    const { data, error } = await sb.from("profiles").select(PROFILE_FIELDS).eq("id", uid).maybeSingle();
    if (error) throw error;
    if (CampusAuth.user?.id === uid) { CampusAuth.profile = data; window.dispatchEvent(new Event("bh-session")); }
    return data;
  },
  // Only descriptive fields; email and role are managed elsewhere.
  async updateProfile(fields) {
    if (!sb) throw new Error("backend_unavailable");
    const uid = CampusAuth.user?.id;
    if (!uid) throw new Error("not_authenticated");
    const clean = Object.fromEntries(Object.entries(fields)
      .filter(([k]) => EDITABLE_PROFILE_FIELDS.includes(k))
      .map(([k, v]) => [k, (v || "").trim() || null]));
    const { data, error } = await sb.from("profiles").update(clean).eq("id", uid).select(PROFILE_FIELDS).single();
    if (error) throw error;
    if ("full_name" in clean) await sb.auth.updateUser({ data: { full_name: clean.full_name } });
    CampusAuth.profile = data;
    window.dispatchEvent(new Event("bh-session"));
    return data;
  },
};

/* ---------------- Course state ---------------- */
const cache = {}; // courseId → { status: "loading" | "ready" | "error", data }

function normalizeCourseState(s) {
  if (!s?.enrolled) return { enrolled: false, saved: !!s?.saved };
  return {
    enrolled: true,
    saved: !!s.saved,
    enrolledAt: s.enrolled_at,
    progressPercent: Number(s.progress_percent),
    completedAt: s.completed_at,
    done: Object.fromEntries((s.done || []).map(slug => [slug, true])),
    marks: s.bookmarks || {},
    quiz: Object.fromEntries(Object.entries(s.quizzes || {}).map(([slug, q]) => [slug, {
      score: q.best_score, total: q.total, passed: q.passed, attemptsUsed: q.attempts_used,
      maxAttempts: q.max_attempts, inProgress: q.in_progress, at: q.last_at,
    }])),
  };
}
function adopt(courseId, serverState) {
  cache[courseId] = { status: "ready", data: normalizeCourseState(serverState) };
  emitProgress();
}
const lastKey = courseId => `bh-last:${CampusAuth.user?.id || "guest"}:${courseId}`;

const CourseStore = {
  PASS_RATIO: 0.7,
  status(courseId) { return cache[courseId]?.status || "idle"; },
  get(courseId) {
    const d = cache[courseId]?.data;
    if (!d?.enrolled) return null;
    let last = null;
    try { last = window.localStorage.getItem(lastKey(courseId)); } catch {}
    return { ...d, last };
  },
  isEnrolled(courseId) { return !!cache[courseId]?.data?.enrolled; },
  isSaved(courseId) { return !!cache[courseId]?.data?.saved; },
  isMarked(courseId, itemId) { return !!cache[courseId]?.data?.marks?.[itemId]; },

  async load(courseId) {
    const userId = CampusAuth.user?.id;
    if (!CampusAuth.user) { cache[courseId] = { status: "ready", data: { enrolled: false } }; emitProgress(); return; }
    cache[courseId] = { ...(cache[courseId] || {}), status: cache[courseId]?.data ? "ready" : "loading" };
    emitProgress();
    try {
      const state = await rpc("campus_course_state", { p_course_slug: courseId });
      if (CampusAuth.user?.id === userId) adopt(courseId, state);
    }
    catch (e) {
      if (CampusAuth.user?.id !== userId) return;
      cache[courseId] = { status: "error", data: cache[courseId]?.data, error: errorText(e) }; emitProgress();
    }
  },
  // Safe to call during render: the fetch starts after the current render.
  ensure(courseId) {
    if (cache[courseId] || !CampusAuth.ready) return;
    cache[courseId] = { status: "loading" };
    setTimeout(() => CourseStore.load(courseId), 0);
  },
  reloadAll() {
    const ids = Object.keys(cache);
    ids.forEach(id => delete cache[id]);
    emitProgress();
    ids.forEach(id => CourseStore.load(id));
  },

  async setSaved(courseId, saved) {
    try { adopt(courseId, await rpc("campus_set_course_saved", { p_course_slug: courseId, p_saved: saved })); return true; }
    catch (e) { toast(errorText(e)); return false; }
  },

  async enroll(courseId) {
    try { adopt(courseId, await rpc("campus_enroll", { p_course_slug: courseId })); return true; }
    catch (e) { toast(errorText(e)); return false; }
  },
  async markDone(courseId, itemId, done = true) {
    const d = cache[courseId]?.data;
    if (!d?.enrolled) return false;
    const before = d.done;
    const next = { ...before };
    if (done) next[itemId] = true; else delete next[itemId];
    d.done = next; emitProgress();                       // optimistic
    try { adopt(courseId, await rpc("campus_set_lesson_done", { p_course_slug: courseId, p_lesson_slug: itemId, p_done: done })); return true; }
    catch (e) { d.done = before; emitProgress(); toast(errorText(e)); return false; }
  },
  async toggleMark(courseId, itemId) {
    const d = cache[courseId]?.data;
    if (!d?.enrolled) return false;
    const before = d.marks;
    const next = { ...before };
    if (next[itemId]) delete next[itemId]; else next[itemId] = new Date().toISOString();
    d.marks = next; emitProgress();                      // optimistic
    try { adopt(courseId, await rpc("campus_toggle_bookmark", { p_course_slug: courseId, p_lesson_slug: itemId })); return true; }
    catch (e) { d.marks = before; emitProgress(); toast(errorText(e)); return false; }
  },
  setLast(courseId, itemId) { try { window.localStorage.setItem(lastKey(courseId), itemId); } catch {} },

  /* Private course notebooks. */
  getNotebook(courseId) { return rpc("campus_get_notebook", { p_course_slug: courseId }); },
  saveNotebook(courseId, content, text, revision, lessonId) {
    return rpc("campus_save_notebook", {
      p_course_slug: courseId, p_content: content, p_text_content: text,
      p_expected_revision: revision, p_lesson_slug: lessonId || null,
    });
  },
  /* Certificates: requested by the student, issued by the academic team. */
  certificates() { return rpc("campus_my_certificates", {}); },
  requestCertificate(courseId) { return rpc("campus_request_certificate", { p_course_slug: courseId }); },
  /* Quizzes — thin wrappers; the reader keeps the attempt state. */
  quizState(courseId, quizId) { return rpc("campus_quiz_state", { p_course_slug: courseId, p_lesson_slug: quizId }); },
  startAttempt(courseId, quizId) { return rpc("campus_start_quiz_attempt", { p_course_slug: courseId, p_lesson_slug: quizId }); },
  saveAnswer(attemptId, questionKey, option) { return rpc("campus_save_quiz_answer", { p_attempt: attemptId, p_question_key: questionKey, p_option: option }); },
  async submitAttempt(courseId, attemptId, answers) {
    const state = await rpc("campus_submit_quiz_attempt", { p_attempt: attemptId, p_answers: answers });
    CourseStore.load(courseId);
    return state;
  },
  errorText,
  errorCode,
};

function useCourseProgress() {
  const [, force] = React.useReducer(x => x + 1, 0);
  React.useEffect(() => {
    window.addEventListener("bh-progress", force);
    window.addEventListener("bh-session", force);
    return () => { window.removeEventListener("bh-progress", force); window.removeEventListener("bh-session", force); };
  }, []);
}

/* ---------- Structured course helpers ---------- */

// Flat, ordered list of every study item: welcome, pages, unit tests, final exam.
function courseItems(course) {
  const items = [];
  (course.intro || []).forEach(p => items.push({ ...p, kind: "page", unit: null }));
  course.units.forEach(u => {
    u.pages.forEach((p, i) => items.push({ ...p, kind: "page", unit: u, num: `${u.n}.${i + 1}` }));
    if (u.quiz) items.push({ ...u.quiz, kind: "quiz", unit: u });
  });
  if (course.final) items.push({ ...course.final, kind: "exam", unit: null });
  return items;
}

function courseProgress(course) {
  const items = courseItems(course);
  const s = CourseStore.get(course.id);
  const done = items.filter(it => s?.done?.[it.id]).length;
  return { items, done, total: items.length, pct: items.length ? Math.round((done / items.length) * 100) : 0, state: s };
}

/* Sequential access: the course opens one step at a time. An item is open when
   every item before it is completed (unit tests count once passed), or when the
   student already completed it. Everything after the first pending item stays
   locked until that item is completed. */
function unlockedIds(course, state = CourseStore.get(course.id)) {
  const open = new Set();
  let reachable = true;
  for (const it of courseItems(course)) {
    const done = !!state?.done?.[it.id];
    if (reachable || done) open.add(it.id);
    if (!done) reachable = false;
  }
  return open;
}
const isUnlocked = (course, itemId, state) => unlockedIds(course, state).has(itemId);
// The pending item that has to be completed to open `itemId`.
function blockingItem(course, itemId, state = CourseStore.get(course.id)) {
  const items = courseItems(course);
  const idx = items.findIndex(it => it.id === itemId);
  return items.slice(0, Math.max(0, idx)).find(it => !state?.done?.[it.id]) || null;
}

function nextItem(course) {
  const { items, state } = courseProgress(course);
  const open = unlockedIds(course, state);
  if (state?.last && open.has(state.last)) {
    const idx = items.findIndex(it => it.id === state.last);
    if (idx >= 0 && !state.done?.[state.last]) return items[idx];
  }
  return items.find(it => !state?.done?.[it.id]) || items[0];
}

const itemHref = (course, item) => `#/curso/${course.id}/leccion/${item.id}`;

// Syllabus in the shape the existing detail / index pages already render.
function buildSyllabus(course) {
  const s = CourseStore.get(course.id);
  const cur = nextItem(course);
  const open = unlockedIds(course, s);
  const toLesson = it => ({
    locked: !!s?.enrolledAt && !open.has(it.id),
    id: it.id,
    type: it.kind === "page" ? "reading" : "quiz",
    title: it.kind === "page" ? (it.num ? `${it.num} · ${it.title}` : it.title) : it.title,
    dur: `${it.minutes} min`,
    done: !!s?.done?.[it.id],
    current: !!s?.enrolledAt && cur?.id === it.id,
    href: itemHref(course, it),
  });
  const mods = [];
  if (course.intro?.length) {
    mods.push({ title: "Antes de empezar", lessons: course.intro.map(p => toLesson({ ...p, kind: "page" })) });
  }
  course.units.forEach(u => {
    const lessons = [
      ...u.pages.map((p, i) => toLesson({ ...p, kind: "page", num: `${u.n}.${i + 1}` })),
      ...(u.quiz ? [toLesson({ ...u.quiz, kind: "quiz" })] : []),
    ];
    mods.push({ title: `Unidad ${u.n} · ${u.title}`, lessons });
  });
  if (course.final) mods.push({ title: "Evaluación final", lessons: [toLesson({ ...course.final, kind: "exam" })] });
  mods.forEach(m => {
    m.done = m.lessons.every(l => l.done);
    m.current = m.lessons.some(l => l.current);
  });
  return mods;
}

// Adds live enrolment / progress fields to a structured course definition.
function defineStructuredCourse(course) {
  Object.defineProperties(course, {
    enrolled: { get() { CourseStore.ensure(course.id); return CourseStore.isEnrolled(course.id); }, enumerable: true },
    progress: { get() { return courseProgress(course).pct; }, enumerable: true },
    completed: { get() { return courseProgress(course).pct === 100; }, enumerable: true },
    saved: { get() { CourseStore.ensure(course.id); return CourseStore.isSaved(course.id); }, enumerable: true },
    syllabus: { get() { return buildSyllabus(course); }, enumerable: true },
  });
  return course;
}

CampusAuth.init();

Object.assign(window, {
  CampusAuth, CourseStore, useCourseProgress, courseItems, courseProgress, nextItem, itemHref, buildSyllabus, defineStructuredCourse,
  unlockedIds, isUnlocked, blockingItem,
});
