/**
 * Antara — login page wiring (real backend, no localStorage hacks).
 *
 * Reads:    window.AUTH_CFG, window.AUTH_CSRF, window.AUTH_API_URL,
 *           window.AUTH_NEXT_URL.
 *
 * Drives the visual template authored in /login/index.php, keeping every
 * existing CSS class and DOM id intact, but replacing the demo
 * runBiometricWebAuthn / form-submit handlers with calls to the real REST API.
 */

(function () {
  "use strict";

  const CFG = window.AUTH_CFG || {};
  const API_URL = window.AUTH_API_URL || "/core/auth/api.php";
  const NEXT_URL = window.AUTH_NEXT_URL || "/index.php";
  let CSRF = window.AUTH_CSRF || "";

  /* ---------------- helpers ---------------- */

  function $(id) { return document.getElementById(id); }
  function on(el, ev, fn) { if (el) el.addEventListener(ev, fn); }

  function setText(el, msg, ok) {
    if (!el) return;
    el.textContent = msg || "";
    el.classList.toggle("is-ok", !!ok);
    el.classList.toggle("bio-feedback--ok", !!ok);
  }

  function setBusy(el, busy) {
    if (!el) return;
    if (busy) {
      el.setAttribute("aria-busy", "true");
      el.disabled = true;
    } else {
      el.removeAttribute("aria-busy");
      el.disabled = false;
    }
  }

  async function apiCall(action, opts) {
    opts = opts || {};
    const url = API_URL + (API_URL.indexOf("?") >= 0 ? "&" : "?") + "action=" + encodeURIComponent(action);
    const headers = Object.assign({ "Accept": "application/json" }, opts.headers || {});
    if (opts.body && !headers["Content-Type"]) headers["Content-Type"] = "application/json";
    if (CSRF) headers["X-CSRF-Token"] = CSRF;
    const res = await fetch(url, {
      method: opts.method || (opts.body ? "POST" : "GET"),
      credentials: "same-origin",
      headers: headers,
      body: opts.body ? JSON.stringify(opts.body) : undefined,
    });
    let data;
    try { data = await res.json(); } catch (_) { data = { success: false, error: "Bad JSON response" }; }
    if (data && data.data && typeof data.data.csrf === "string") {
      CSRF = data.data.csrf;
    }
    if (!data || data.success !== true) {
      const err = new Error((data && data.error) || ("HTTP " + res.status));
      err.code = data && data.code;
      err.http = res.status;
      throw err;
    }
    return data.data;
  }

  function b64uToUint8(b64u) {
    const pad = "=".repeat((4 - (b64u.length % 4)) % 4);
    const b64 = (b64u + pad).replace(/-/g, "+").replace(/_/g, "/");
    const bin = atob(b64);
    const out = new Uint8Array(bin.length);
    for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
    return out;
  }

  function bufToB64u(buf) {
    const u8 = new Uint8Array(buf);
    let bin = "";
    for (let i = 0; i < u8.length; i++) bin += String.fromCharCode(u8[i]);
    return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
  }

  function pkcRequestFromServer(opts) {
    const out = Object.assign({}, opts);
    out.challenge = b64uToUint8(opts.challenge);
    if (Array.isArray(opts.allowCredentials)) {
      out.allowCredentials = opts.allowCredentials.map(c => Object.assign({}, c, { id: b64uToUint8(c.id) }));
    }
    return out;
  }

  function pkcCreationFromServer(opts) {
    const out = Object.assign({}, opts);
    out.challenge = b64uToUint8(opts.challenge);
    out.user = Object.assign({}, opts.user, { id: b64uToUint8(opts.user.id) });
    if (Array.isArray(opts.excludeCredentials)) {
      out.excludeCredentials = opts.excludeCredentials.map(c => Object.assign({}, c, { id: b64uToUint8(c.id) }));
    }
    return out;
  }

  function credentialAssertionToJson(cred) {
    return {
      id: cred.id,
      rawId: bufToB64u(cred.rawId),
      type: cred.type,
      response: {
        clientDataJSON: bufToB64u(cred.response.clientDataJSON),
        authenticatorData: bufToB64u(cred.response.authenticatorData),
        signature: bufToB64u(cred.response.signature),
        userHandle: cred.response.userHandle ? bufToB64u(cred.response.userHandle) : null,
      },
    };
  }

  function credentialAttestationToJson(cred) {
    const transports = (cred.response.getTransports && cred.response.getTransports()) || [];
    return {
      id: cred.id,
      rawId: bufToB64u(cred.rawId),
      type: cred.type,
      response: {
        clientDataJSON: bufToB64u(cred.response.clientDataJSON),
        attestationObject: bufToB64u(cred.response.attestationObject),
        transports: transports,
      },
    };
  }

  /* ---------------- visual goodies ---------------- */

  function tickClock() {
    const now = new Date();
    const t = $("clock-time");
    const d = $("clock-date");
    if (window.AntaraClockFormat && typeof window.AntaraClockFormat.lines === "function") {
      const L = window.AntaraClockFormat.lines(now);
      if (t) t.textContent = L.time;
      if (d) d.textContent = L.date;
      return;
    }
    const hh = String(now.getHours()).padStart(2, "0");
    const mm = String(now.getMinutes()).padStart(2, "0");
    if (t) t.textContent = hh + ":" + mm;
    if (d) d.textContent = now.toLocaleDateString("es-ES", { day: "numeric", month: "short", year: "numeric" }).toUpperCase();
  }

  function spawnParticles(count) {
    const root = $("particles");
    if (!root) return;
    const frag = document.createDocumentFragment();
    for (let i = 0; i < count; i++) {
      const p = document.createElement("span");
      p.className = "particle";
      p.style.left = (Math.random() * 100) + "%";
      p.style.animationDuration = (12 + Math.random() * 18) + "s";
      p.style.animationDelay = (Math.random() * 10) + "s";
      frag.appendChild(p);
    }
    root.appendChild(frag);
  }

  function settleScene() {
    const scene = $("scene");
    const panel = $("panel");
    if (!scene || !panel) return;
    const dur = 1450;
    setTimeout(() => {
      scene.classList.remove("scene--entrance");
      scene.classList.add("scene--settled-2d");
      panel.classList.remove("panel--animating-entrance");
      panel.classList.add("panel--settled-2d");
    }, dur + 60);
  }

  function bindIsTyping() {
    const inputs = document.querySelectorAll('input[type="text"], input[type="password"], input[type="tel"], input[type="email"]');
    inputs.forEach((el) => {
      el.addEventListener("focus", () => document.body.classList.add("is-typing"));
      el.addEventListener("blur", () => {
        // Only remove when no other input is focused.
        setTimeout(() => {
          if (!document.activeElement || document.activeElement.tagName !== "INPUT") {
            document.body.classList.remove("is-typing");
          }
        }, 30);
      });
    });
  }

  /* ---------------- login form ---------------- */

  const LOGIN_USER_KEY = "antara_login_username";

  function normalizeLoginUsername(raw) {
    const v = (raw || "").trim();
    if (!v) return "";
    if (v.indexOf("@") >= 0) return v.toLowerCase();
    return v.replace(/\s+/g, "");
  }

  function mapAuthError(err) {
    const code = err && err.code;
    const msg = (err && err.message) || "";
    const map = {
      BAD_CREDENTIALS: "Correo, teléfono o contraseña incorrectos. Revisa tus datos e inténtalo de nuevo.",
      BAD_INPUT: "Ingresa tu correo o teléfono y tu contraseña.",
      LOCKED: msg || "Demasiados intentos fallidos. Espera un momento e inténtalo de nuevo.",
      PWD_DISABLED: "El inicio de sesión con contraseña está deshabilitado por el administrador.",
      BLOCKED: "Tu cuenta está bloqueada. Contacta al administrador.",
      INACTIVE: "Tu cuenta está inactiva. Contacta al administrador.",
      VAULT_UNREADABLE: "No se puede acceder al sistema de usuarios. Contacta al administrador.",
    };
    if (code && map[code]) return map[code];
    if (/invalid username or password/i.test(msg)) {
      return map.BAD_CREDENTIALS;
    }
    if (/too many attempts/i.test(msg)) {
      return "Demasiados intentos fallidos. Espera un momento e inténtalo de nuevo.";
    }
    if (/password login is disabled/i.test(msg)) {
      return map.PWD_DISABLED;
    }
    return msg || "No se pudo iniciar sesión. Comprueba tu conexión e inténtalo de nuevo.";
  }

  function persistLoginUsername(value) {
    try {
      if (value) sessionStorage.setItem(LOGIN_USER_KEY, value);
      else sessionStorage.removeItem(LOGIN_USER_KEY);
    } catch (_) {}
  }

  function restoreLoginUsernameField() {
    const input = $("username");
    if (!input || (input.value || "").trim() !== "") return;
    try {
      const saved = sessionStorage.getItem(LOGIN_USER_KEY);
      if (saved) input.value = saved;
    } catch (_) {}
  }

  function bindLoginUsernamePersistence() {
    const input = $("username");
    if (!input) return;
    restoreLoginUsernameField();
    input.addEventListener("input", () => {
      persistLoginUsername(normalizeLoginUsername(input.value));
    });
  }

  function bindIdentityToggle() {
    const btn = $("toggle-identity");
    const input = $("username");
    const label = $("username-label");
    if (!btn || !input || !label) return;
    let usingPhone = true;
    function paint() {
      const toEmail = btn.querySelector('[data-mode-icon="to-email"]');
      const toPhone = btn.querySelector('[data-mode-icon="to-phone"]');
      if (toEmail) toEmail.classList.toggle("is-visible", usingPhone);
      if (toPhone) toPhone.classList.toggle("is-visible", !usingPhone);
      if (usingPhone) {
        label.textContent = "Teléfono o correo";
        input.type = "tel"; input.inputMode = "tel"; input.autocomplete = "tel";
        input.placeholder = "Tu número celular o correo";
        btn.title = "Usar correo";
      } else {
        label.textContent = "Correo electrónico";
        input.type = "email"; input.inputMode = "email"; input.autocomplete = "email";
        input.placeholder = "tu@correo.com";
        btn.title = "Usar teléfono";
      }
    }
    btn.addEventListener("click", () => { usingPhone = !usingPhone; paint(); input.focus(); });
    paint();
  }

  function bindPasswordToggle(toggleId, inputId) {
    const t = $(toggleId), i = $(inputId);
    if (!t || !i) return;
    t.addEventListener("click", () => {
      const hidden = i.type === "password";
      i.type = hidden ? "text" : "password";
      t.setAttribute("aria-pressed", hidden ? "true" : "false");
      t.setAttribute("aria-label", hidden ? "Ocultar contraseña" : "Mostrar contraseña");
    });
  }

  function bindLoginForm() {
    const form = $("login-form");
    const btn = $("btn-submit-login");
    const fb = $("form-feedback");
    if (!form) return;
    form.addEventListener("submit", async (e) => {
      e.preventDefault();
      const userInput = $("username");
      const passInput = $("password");
      const username = normalizeLoginUsername(userInput && userInput.value);
      const password = passInput ? passInput.value || "" : "";
      if (!username || !password) {
        setText(fb, "Ingresa tu correo o teléfono y tu contraseña.", false);
        return;
      }
      if (userInput) userInput.value = username;
      persistLoginUsername(username);
      setText(fb, "Verificando…", true);
      setBusy(btn, true);
      try {
        await apiCall("authLogin", { body: { username, password } });
        persistLoginUsername("");
        if (window.AntaraAuthTabSync && typeof window.AntaraAuthTabSync.notifyLogin === "function") {
          window.AntaraAuthTabSync.notifyLogin(NEXT_URL);
        }
        setText(fb, "Autenticado. Redirigiendo…", true);
        setTimeout(() => { location.replace(NEXT_URL); }, 250);
      } catch (err) {
        if (userInput) userInput.value = username;
        persistLoginUsername(username);
        if (passInput) {
          passInput.value = "";
          passInput.focus();
        }
        if (err && err.code === "PASSWORD_RESET_REQUIRED") {
          setText(
            fb,
            "Tu cuenta requiere una contraseña nueva (migración desde el sistema anterior o política del administrador). " +
              "Pide a un administrador que te la asigne desde el panel /admin/users/ o usa el canal de soporte interno.",
            false
          );
        } else {
          setText(fb, mapAuthError(err), false);
        }
      } finally {
        setBusy(btn, false);
      }
    });
  }

  /* ---------------- biometric ---------------- */

  async function biometricAvailable() {
    if (!CFG.allow_webauthn) return false;
    if (!window.PublicKeyCredential || !navigator.credentials) return false;
    try {
      if (PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable) {
        const ok = await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
        return !!ok;
      }
      return true;
    } catch (_) { return false; }
  }

  async function tryBiometricLogin(opts) {
    opts = opts || {};
    const fb = opts.feedbackEl || $("bio-feedback");
    const btn = opts.buttonEl || $("btn-biometric");
    setText(fb, "", false);
    setBusy(btn, true);
    try {
      const username = ($("username") && $("username").value || "").trim();
      const beginRes = await apiCall("webauthnLoginBegin", { body: { login: username || null } });

      if (beginRes && beginRes.noCredentials) {
        if (beginRes.allow_user_registration) {
          openSignupWizard();
          setText(fb, "No hay passkey para esa cuenta. Crea una nueva.", false);
        } else {
          setText(fb, "La creación de nuevos usuarios está deshabilitada por el administrador.", false);
        }
        return;
      }

      const publicKey = pkcRequestFromServer(beginRes);
      const assertion = await navigator.credentials.get({ publicKey });
      if (!assertion || assertion.type !== "public-key") throw new Error("Respuesta inválida del autenticador.");
      const finishRes = await apiCall("webauthnLoginFinish", { body: { credential: credentialAssertionToJson(assertion) } });
      if (window.AntaraAuthTabSync && typeof window.AntaraAuthTabSync.notifyLogin === "function") {
        window.AntaraAuthTabSync.notifyLogin(NEXT_URL);
      }
      setText(fb, "Verificado. Redirigiendo…", true);
      setTimeout(() => { location.replace(NEXT_URL); }, 250);
    } catch (err) {
      const name = err && err.name;
      if (name === "NotAllowedError") {
        setText(fb, "Lectura biométrica cancelada.", false);
      } else if (name === "InvalidStateError") {
        setText(fb, "La credencial no es válida en este dispositivo.", false);
      } else {
        setText(fb, "WebAuthn: " + (err.message || String(err)), false);
      }
    } finally {
      setBusy(btn, false);
    }
  }

  async function initBiometricSection() {
    const section = $("biometric-section");
    const sectionFb = $("biometric-section-fallback");
    const ok = await biometricAvailable();
    if (!ok) return;
    if (section) {
      section.classList.add("biometric-section--available");
      section.removeAttribute("aria-hidden");
      const btn = $("btn-biometric");
      if (btn) {
        btn.setAttribute("tabindex", "0");
        btn.addEventListener("click", () => tryBiometricLogin());
      }
    }
    if (sectionFb) {
      sectionFb.classList.add("biometric-section--available");
      sectionFb.removeAttribute("aria-hidden");
      const btn2 = $("btn-biometric-only");
      if (btn2) btn2.addEventListener("click", () => tryBiometricLogin({ buttonEl: btn2 }));
    }
  }

  /* ---------------- wizard ---------------- */

  const WIZARD_ORDER = ["intro", "photo", "crop", "name", "email", "phone", "password", "done"];
  const WIZARD_LABELS = {
    intro: "Paso 1 · Bienvenida",
    photo: "Paso 2 · Foto de perfil",
    crop:  "Paso 3 · Recorte",
    name:  "Paso 4 · Nombre",
    email: "Paso 5 · Correo",
    phone: "Paso 6 · Celular",
    password: "Paso 7 · Contraseña",
    done:  "Paso 8 · Finalizar",
  };
  const wizardState = {
    history: ["intro"],
    pendingPasskey: false,
    registeredPasskeyOnce: false,
    cropObjectUrl: null,
    avatarPreviewUrl: null,
  };
  let wizardCropUnbind = null;

  function teardownWizardCrop() {
    if (typeof wizardCropUnbind === "function") {
      try { wizardCropUnbind(); } catch (_) {}
    }
    wizardCropUnbind = null;
    const ci = $("wizard-crop-img");
    if (ci) ci.removeAttribute("src");
    const zr = $("wizard-crop-zoom-range");
    if (zr) zr.value = "100";
  }

  function revokeWizardCropUrl() {
    if (wizardState.cropObjectUrl) {
      try { URL.revokeObjectURL(wizardState.cropObjectUrl); } catch (_) {}
      wizardState.cropObjectUrl = null;
    }
  }

  function revokeWizardAvatarPreviewUrl() {
    if (wizardState.avatarPreviewUrl) {
      try { URL.revokeObjectURL(wizardState.avatarPreviewUrl); } catch (_) {}
      wizardState.avatarPreviewUrl = null;
    }
  }

  function syncWizardPhotoStepPreview() {
    const img = $("wizard-photo-preview-img");
    const box = $("wizard-photo-preview");
    if (!img || !box) return;
    if (wizardState.avatarPreviewUrl) {
      img.src = wizardState.avatarPreviewUrl;
      img.hidden = false;
      box.classList.add("has-img");
    } else {
      img.removeAttribute("src");
      img.hidden = true;
      box.classList.remove("has-img");
    }
  }

  function openSignupWizard() {
    if (!CFG.allow_user_registration) {
      const fb = $("form-feedback");
      setText(fb, "La creación de nuevos usuarios está deshabilitada por el administrador.", false);
      return;
    }
    const w = $("signup-wizard"); if (!w) return;
    w.removeAttribute("hidden");
    w.setAttribute("aria-hidden", "false");
    document.body.classList.add("signup-wizard-open");
    requestAnimationFrame(() => w.classList.add("is-open"));
    wizardShow("intro", true);
  }

  function closeSignupWizard() {
    const w = $("signup-wizard"); if (!w) return;
    teardownWizardCrop();
    revokeWizardCropUrl();
    revokeWizardAvatarPreviewUrl();
    syncWizardPhotoStepPreview();
    w.classList.remove("is-open");
    w.setAttribute("aria-hidden", "true");
    document.body.classList.remove("signup-wizard-open");
    setTimeout(() => w.setAttribute("hidden", ""), 300);
  }

  function wizardBuildDots() {
    const root = $("wizard-progress"); if (!root || root.childElementCount) return;
    for (let i = 0; i < WIZARD_ORDER.length; i++) {
      const d = document.createElement("span");
      d.className = "wizard-progress__dot";
      d.dataset.stepDot = WIZARD_ORDER[i];
      root.appendChild(d);
    }
  }

  function wizardScrollToTop() {
    const scrollEl = document.querySelector(".signup-wizard__scroll");
    if (scrollEl) {
      scrollEl.scrollTop = 0;
    }
  }

  function wizardShow(stepId, resetHistory) {
    if (stepId === "photo") {
      teardownWizardCrop();
      revokeWizardCropUrl();
    }
    if (resetHistory) wizardState.history = [stepId];
    const steps = document.querySelectorAll(".wizard-step");
    steps.forEach((el) => el.classList.toggle("is-active", el.getAttribute("data-wizard-step") === stepId));
    const idx = WIZARD_ORDER.indexOf(stepId);
    document.querySelectorAll(".wizard-progress__dot").forEach((d, i) => d.classList.toggle("is-done", i <= idx));
    const sub = $("signup-wizard-step-label");
    if (sub) sub.textContent = WIZARD_LABELS[stepId] || "";
    if (stepId === "photo") syncWizardPhotoStepPreview();
    wizardScrollToTop();
  }

  function wizardGo(stepId) {
    if (wizardState.history[wizardState.history.length - 1] !== stepId) wizardState.history.push(stepId);
    wizardShow(stepId, false);
  }

  function wizardBack() {
    wizardState.history.pop();
    const prev = wizardState.history[wizardState.history.length - 1];
    if (!prev) { closeSignupWizard(); return; }
    wizardShow(prev, false);
  }

  function bindPrivacyModal() {
    const modal = $("privacy-modal");
    if (!modal) return;
    let prevOverflow = "";
    function open() {
      modal.removeAttribute("hidden");
      modal.setAttribute("aria-hidden", "false");
      prevOverflow = document.body.style.overflow || "";
      document.body.style.overflow = "hidden";
      requestAnimationFrame(() => modal.classList.add("is-open"));
    }
    function close() {
      modal.classList.remove("is-open");
      modal.setAttribute("aria-hidden", "true");
      document.body.style.overflow = prevOverflow;
      setTimeout(() => modal.setAttribute("hidden", ""), 300);
    }
    document.querySelectorAll("[data-open-privacy-modal]").forEach((b) => b.addEventListener("click", open));
    document.querySelectorAll("[data-close-privacy-modal]").forEach((b) => b.addEventListener("click", close));
  }

  function wizardPasswordMeetsPolicy(pwd) {
    const a = pwd || "";
    return a.length >= 8 && /[A-Za-z]/.test(a) && /\d/.test(a);
  }

  function passwordStrength(pwd) {
    if (!pwd) return -1;
    let score = 0;
    if (pwd.length >= 8) score++;
    if (pwd.length >= 12) score++;
    if (/[A-Z]/.test(pwd) && /[a-z]/.test(pwd)) score++;
    if (/\d/.test(pwd)) score++;
    if (/[^A-Za-z0-9]/.test(pwd)) score++;
    return Math.min(3, score - 1);
  }

  function setHint(el, msg, ok) {
    if (!el) return;
    el.textContent = msg || "";
    el.classList.toggle("is-ok", !!ok);
  }

  function bindWizardCore() {
    const w = $("signup-wizard"); if (!w) return;
    wizardBuildDots();

    on($("link-signup-wizard"), "click", (e) => { e.preventDefault(); openSignupWizard(); });
    on($("wizard-close-login"), "click", () => closeSignupWizard());

    document.querySelectorAll("[data-wizard-regresar]").forEach((b) => b.addEventListener("click", wizardBack));

    const WAC = typeof window !== "undefined" ? window.AntaraAvatarSquareCrop : null;

    on($("wizard-from-intro-next"), "click", () => wizardGo("photo"));
    on($("wizard-skip-photo"), "click", () => {
      teardownWizardCrop();
      revokeWizardCropUrl();
      revokeWizardAvatarPreviewUrl();
      const av = $("wizard-avatar-img");
      const wrap = $("wizard-avatar-preview");
      const head = $("wizard-head-avatar");
      const headImg = $("wizard-head-avatar-img");
      if (av) { av.removeAttribute("src"); }
      if (wrap) wrap.classList.remove("has-img");
      if (head) head.classList.remove("is-visible");
      if (headImg) headImg.removeAttribute("src");
      syncWizardPhotoStepPreview();
      wizardGo("name");
    });
    on($("wizard-pick-gallery"), "click", () => $("wizard-file-gallery") && $("wizard-file-gallery").click());
    on($("wizard-pick-camera"),  "click", () => $("wizard-file-camera") && $("wizard-file-camera").click());

    function mountWizardCrop() {
      const img = $("wizard-crop-img");
      const vp = $("wizard-crop-viewport");
      const zr = $("wizard-crop-zoom-range");
      if (!WAC || !vp || !img || !zr) return;
      if (typeof wizardCropUnbind === "function") {
        try { wizardCropUnbind(); } catch (_) {}
      }
      wizardCropUnbind = WAC.bindSquareAvatarCrop({
        viewport: vp,
        img,
        zoomRange: zr,
        frameInsetRatio: 0.1,
      });
    }

    function onPickFile(input) {
      if (!input || !input.files || !input.files[0]) return;
      const file = input.files[0];
      teardownWizardCrop();
      revokeWizardCropUrl();
      wizardState.cropObjectUrl = URL.createObjectURL(file);
      const img = $("wizard-crop-img");
      wizardGo("crop");
      if (!img) return;
      const tryMount = () => {
        const vp = $("wizard-crop-viewport");
        if (img.naturalWidth > 0 && vp && vp.clientWidth > 0) mountWizardCrop();
      };
      img.onload = function () {
        img.onload = null;
        requestAnimationFrame(() => requestAnimationFrame(tryMount));
      };
      img.src = wizardState.cropObjectUrl;
      requestAnimationFrame(() => requestAnimationFrame(() => {
        if (img.complete && img.naturalWidth) tryMount();
      }));
    }
    on($("wizard-file-gallery"), "change", (e) => onPickFile(e.currentTarget));
    on($("wizard-file-camera"),  "change", (e) => onPickFile(e.currentTarget));

    on($("wizard-crop-change"), "click", () => {
      teardownWizardCrop();
      revokeWizardCropUrl();
      wizardGo("photo");
    });
    on($("wizard-crop-apply"), "click", async () => {
      const img = $("wizard-crop-img");
      const vp = $("wizard-crop-viewport");
      if (!WAC || !img || !vp) {
        wizardGo("name");
        return;
      }
      try {
        const zr = $("wizard-crop-zoom-range");
        const blob = await WAC.exportSquareAvatarBlob(img, vp, {
          mime: "image/jpeg",
          quality: 0.92,
          zoomRange: zr,
        });
        teardownWizardCrop();
        revokeWizardCropUrl();
        revokeWizardAvatarPreviewUrl();
        wizardState.avatarPreviewUrl = URL.createObjectURL(blob);
        const av = $("wizard-avatar-img");
        const wrap = $("wizard-avatar-preview");
        const headImg = $("wizard-head-avatar-img");
        if (av) {
          av.src = wizardState.avatarPreviewUrl;
        }
        if (wrap) wrap.classList.add("has-img");
        if (headImg) {
          headImg.src = wizardState.avatarPreviewUrl;
        }
        const head = $("wizard-head-avatar");
        if (head) head.classList.add("is-visible");
        syncWizardPhotoStepPreview();
      } catch (_) {
        /* Si falla el export, continuar al siguiente paso sin imagen */
      }
      wizardGo("name");
    });

    on($("wizard-from-name-next"), "click", () => {
      const v = ($("wizard-fullname").value || "").trim();
      if (v.length < 2) { setHint($("wizard-name-step-msg"), "Escribe tu nombre completo."); return; }
      setHint($("wizard-name-step-msg"), "");
      wizardGo("email");
    });
    on($("wizard-from-email-next"), "click", () => {
      const v = ($("wizard-email").value || "").trim();
      if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v)) { setHint($("wizard-email-step-msg"), "Correo electrónico inválido."); return; }
      setHint($("wizard-email-step-msg"), "");
      wizardGo("phone");
    });
    on($("wizard-from-phone-next"), "click", () => {
      const v = ($("wizard-phone").value || "").trim();
      if (v && !/^\+?\d{6,20}$/.test(v.replace(/\s+/g, ""))) {
        setHint($("wizard-phone-step-msg"), "Número telefónico inválido.");
        return;
      }
      setHint($("wizard-phone-step-msg"), "");
      wizardGo("password");
    });

    bindPasswordToggle("wizard-toggle-pass-a", "wizard-password");
    bindPasswordToggle("wizard-toggle-pass-b", "wizard-password2");

    on($("wizard-password"), "input", (e) => {
      setHint($("wizard-password-step-msg"), "");
      const score = passwordStrength(e.currentTarget.value);
      const fill = $("wizard-password-meter");
      const lbl  = $("wizard-password-strength-text");
      if (fill) {
        fill.classList.remove("level-0", "level-1", "level-2", "level-3");
        fill.classList.add("level-" + Math.max(0, score));
      }
      if (lbl) {
        lbl.classList.remove("level-0", "level-3");
        lbl.textContent = "Fortaleza: " + ["—","Débil","Media","Fuerte","Excelente"][Math.max(0, score) + 1];
        if (score <= 0) lbl.classList.add("level-0");
        if (score >= 3) lbl.classList.add("level-3");
      }
    });

    on($("wizard-password2"), "input", () => {
      const a = $("wizard-password").value || "";
      const b = $("wizard-password2").value || "";
      const hint = $("wizard-password-match-hint");
      if (!b) { setHint(hint, ""); return; }
      if (a !== b) setHint(hint, "Las contraseñas no coinciden.");
      else setHint(hint, "Coincide.", true);
    });

    on($("wizard-from-password-next"), "click", () => {
      const a = $("wizard-password").value || "";
      const b = $("wizard-password2").value || "";
      const msg = $("wizard-password-step-msg");
      if (a.length < 8) { setHint(msg, "Mínimo 8 caracteres."); return; }
      if (!/[A-Za-z]/.test(a)) { setHint(msg, "Incluye al menos una letra."); return; }
      if (!/\d/.test(a)) { setHint(msg, "Incluye al menos un número."); return; }
      if (a !== b) { setHint(msg, "Las contraseñas no coinciden."); return; }
      setHint(msg, "");
      wizardGo("done");
    });

    /* Final step: optional fingerprint registration THEN account creation */
    on($("wizard-btn-fingerprint"), "click", async () => {
      if (!CFG.allow_webauthn) {
        setText($("wizard-bio-feedback"), "WebAuthn deshabilitado por el administrador.", false);
        return;
      }
      const ok = await biometricAvailable();
      if (!ok) {
        setText($("wizard-bio-feedback"), "Este dispositivo no soporta passkeys.", false);
        return;
      }
      // Mark intent — we will register after the account is created.
      wizardState.pendingPasskey = true;
      setText($("wizard-bio-feedback"), "Se registrará la huella al crear la cuenta.", true);
    });

    on($("wizard-btn-create-mock"), "click", async () => {
      const btn = $("wizard-btn-create-mock");
      const fb  = $("wizard-mock-msg");
      const bioFb = $("wizard-bio-feedback");
      const fullname = ($("wizard-fullname").value || "").trim();
      const email    = ($("wizard-email").value || "").trim();
      const phone    = ($("wizard-phone").value || "").trim().replace(/\s+/g, "");
      const password = $("wizard-password").value || "";

      if (!CFG.allow_user_registration) {
        setHint(fb, "La creación de nuevos usuarios está deshabilitada por el administrador.");
        return;
      }
      if (fullname.length < 2 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
        setHint(fb, "Completa todos los campos antes de continuar.");
        return;
      }
      const pwA = $("wizard-password").value || "";
      const pwB = $("wizard-password2").value || "";
      if (pwA !== pwB || !wizardPasswordMeetsPolicy(pwA)) {
        wizardGo("password");
        const pmsg = $("wizard-password-step-msg");
        setHint(fb, "");
        if (pwA !== pwB) {
          setHint(pmsg, "Las contraseñas no coinciden.");
        } else if (pwA.length < 8) {
          setHint(pmsg, "Mínimo 8 caracteres.");
        } else if (!/[A-Za-z]/.test(pwA)) {
          setHint(pmsg, "Incluye al menos una letra.");
        } else {
          setHint(pmsg, "Incluye al menos un número.");
        }
        return;
      }

      setBusy(btn, true);
      setHint(fb, "Creando cuenta…");
      try {
        await apiCall("authRegister", { body: { username: fullname, email, phone, password } });

        if (wizardState.pendingPasskey && CFG.allow_webauthn) {
          try {
            const beginRes = await apiCall("webauthnRegisterBegin", { body: {} });
            const publicKey = pkcCreationFromServer(beginRes);
            const cred = await navigator.credentials.create({ publicKey });
            if (!cred || cred.type !== "public-key") throw new Error("Respuesta inválida del autenticador.");
            await apiCall("webauthnRegisterFinish", { body: { credential: credentialAttestationToJson(cred) } });
            wizardState.registeredPasskeyOnce = true;
            setText(bioFb, "Passkey registrada correctamente.", true);
          } catch (we) {
            setText(bioFb, "No se pudo registrar la passkey: " + (we.message || we), false);
          }
        }

        setHint(fb, "Cuenta creada. Redirigiendo…", true);
        if (window.AntaraAuthTabSync && typeof window.AntaraAuthTabSync.notifyLogin === "function") {
          window.AntaraAuthTabSync.notifyLogin(NEXT_URL);
        }
        setTimeout(() => { location.replace(NEXT_URL); }, 350);
      } catch (err) {
        setHint(fb, err.message || "No se pudo crear la cuenta.");
      } finally {
        setBusy(btn, false);
      }
    });
  }

  /* ---------------- boot ---------------- */

  function boot() {
    tickClock(); setInterval(tickClock, 1000);
    spawnParticles(36);
    settleScene();
    bindIsTyping();
    bindIdentityToggle();
    bindLoginUsernamePersistence();
    bindPasswordToggle("toggle-password", "password");
    bindLoginForm();
    bindWizardCore();
    bindPrivacyModal();
    initBiometricSection();
  }

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", boot);
  } else {
    boot();
  }
})();
