<template>
  <div class="page">
    <section class="card hero glass">
      <div>
        <p class="eyebrow">Atlas</p>
        <h1>Onboarding</h1>
        <p class="lede">Use your request code to view status and next steps.</p>
      </div>
    </section>

    <section class="card module">
      <div class="module-head">
        <h2>Request Code</h2>
        <span class="pill mono" :class="statusPillClass(status)">
          {{ statusLabel(status) }}
        </span>
      </div>

      <div class="status-form">
        <input v-model="requestCode" class="input mono" type="text" placeholder="username~XXXXXXXXXX" :disabled="loading" />
        <button class="primary" type="button" @click="check" :disabled="loading || !requestCode.trim()">
          {{ loading ? "Checking..." : "Check" }}
        </button>
      </div>

      <div v-if="requestUsername" class="status-meta">
        <div class="meta-row">
          <span class="label mono">Username</span>
          <button class="copy mono" type="button" @click="copyUsername">
            {{ requestUsername }}
            <span v-if="usernameCopied" class="copied">copied</span>
          </button>
        </div>
      </div>

      <div v-if="status === 'pending_email_verification'" class="steps">
        <h3>Confirm your email</h3>
        <p class="muted">
          Open the verification email from Atlas and click the link to confirm your address. After verification, an admin can approve your request.
        </p>
        <p class="muted">
          If you did not receive an email, return to
          <a href="/request-access">Request Access</a>
          and submit again using a reachable external address.
        </p>
      </div>

      <div v-if="status === 'pending'" class="steps">
        <h3>Awaiting approval</h3>
        <p class="muted">An Atlas admin has to approve this request before an account can be provisioned.</p>
      </div>

      <div v-if="status === 'accounts_building'" class="steps">
        <h3>Accounts building</h3>
        <p class="muted">
          Your request is approved. Atlas is now preparing accounts and credentials. Check back in a minute.
        </p>
        <div v-if="tasks.length" class="task-box">
          <div class="module-head" style="margin-bottom: 10px;">
            <h3>Automation</h3>
            <span class="pill mono" :class="blocked ? 'pill-bad' : 'pill-ok'">
              {{ blocked ? "blocked" : "running" }}
            </span>
          </div>
          <ul class="task-list">
            <li v-for="item in tasks" :key="item.task" class="task-row">
              <span class="mono task-name">{{ item.task }}</span>
              <span class="pill mono" :class="taskPillClass(item.status)">{{ item.status }}</span>
              <span v-if="item.detail" class="mono task-detail">{{ item.detail }}</span>
            </li>
          </ul>
          <p v-if="blocked" class="muted" style="margin-top: 10px;">
            One or more automation steps failed. Fix the error above, then check again.
          </p>
        </div>
      </div>

      <div v-if="status === 'awaiting_onboarding' || status === 'ready'" class="steps">
        <div class="onboarding-head">
          <h3>Onboarding checklist</h3>
          <span class="pill mono" :class="status === 'ready' ? 'pill-info' : 'pill-ok'">
            {{ status === "ready" ? "ready" : "in progress" }}
          </span>
        </div>

        <p class="muted">
          Some steps are verified automatically from Keycloak (password). Others can't be verified yet — mark them complete once you're done.
        </p>

        <div v-if="initialPassword" class="initial-password">
          <h3>Temporary password</h3>
          <p class="muted">
            Use this password to log in for the first time (Nextcloud, Element). You won't be forced to change it
            immediately — you'll rotate it after Vaultwarden is set up. This password is shown once — copy it now. If you
            refresh this page, it may disappear.
          </p>
          <div class="request-code-row">
            <span class="label mono">Password</span>
            <button class="copy mono" type="button" @click="copyInitialPassword">
              {{ initialPassword }}
              <span v-if="copied" class="copied">copied</span>
            </button>
          </div>
          <p class="muted">
            Log in at
            <a href="https://sso.bstein.dev" target="_blank" rel="noreferrer">sso.bstein.dev</a>
            or go directly to
            <a href="https://cloud.bstein.dev" target="_blank" rel="noreferrer">cloud.bstein.dev</a>.
          </p>
        </div>

        <div v-if="!auth.authenticated" class="login-callout">
          <p class="muted">Log in to check off onboarding steps.</p>
          <button class="primary" type="button" @click="loginToContinue" :disabled="loading">Log in</button>
        </div>

        <ul class="checklist">
          <li class="check-item" :class="checkItemClass('vaultwarden_master_password')">
            <label>
              <input
                type="checkbox"
                :checked="isStepDone('vaultwarden_master_password')"
                :disabled="!auth.authenticated || loading || isStepBlocked('vaultwarden_master_password')"
                @change="toggleStep('vaultwarden_master_password', $event)"
              />
              <span>Set a Vaultwarden master password</span>
              <span class="pill mono auto-pill" :class="stepPillClass('vaultwarden_master_password')">
                {{ stepPillLabel("vaultwarden_master_password") }}
              </span>
            </label>
            <p class="muted">
              Open <a href="https://vault.bstein.dev" target="_blank" rel="noreferrer">Passwords</a> and set a strong master
              password you won't forget. Your master password is the one password to rule all passwords: use a long passphrase
              (64+ characters is a good target), and never write it down or share it with anyone. If you lose it, Atlas can't
              recover your vault. If you can't sign in yet, check your Atlas mailbox in
              <a href="https://cloud.bstein.dev" target="_blank" rel="noreferrer">Nextcloud Mail</a> for the invite link.
            </p>
            <details class="howto">
              <summary class="mono">Master password guidance</summary>
              <ul class="muted howto-list">
                <li>Prefer a multi-word passphrase over a single word.</li>
                <li>Never store it in plaintext or share it with anyone.</li>
                <li>If you forget it, Vaultwarden can’t decrypt your data.</li>
              </ul>
            </details>
          </li>

          <li class="check-item" :class="checkItemClass('keycloak_password_rotated')">
            <label>
              <input type="checkbox" :checked="isStepDone('keycloak_password_rotated')" disabled />
              <span>Sign in to Element and update your Keycloak profile</span>
              <span class="pill mono auto-pill" :class="keycloakRotationPillClass()">
                {{ keycloakRotationPillLabel() }}
              </span>
            </label>
            <div class="mfa-actions">
              <button
                class="secondary"
                type="button"
                @click="requestKeycloakPasswordRotation"
                :disabled="
                  !auth.authenticated ||
                  loading ||
                  isStepDone('keycloak_password_rotated') ||
                  isStepBlocked('keycloak_password_rotated') ||
                  keycloakPasswordRotationRequested
                "
              >
                Start Keycloak update
              </button>
              <a class="mono" href="https://live.bstein.dev" target="_blank" rel="noreferrer">Open Element</a>
            </div>
            <p class="muted">
              After Vaultwarden is ready, sign in to Element with the temporary password. Keycloak will prompt you to set
              a new password and your name. Store the new password in Vaultwarden. Atlas will mark this step complete once
              Keycloak no longer requires a password update.
            </p>
          </li>

          <li class="check-item mfa-optional" :class="mfaItemClass()">
            <div class="mfa-row">
              <div class="mfa-text">
                <span class="mfa-title">Optional: enable MFA (TOTP) for Keycloak</span>
                <p class="muted mfa-description">
                  Add a second factor with a mobile authenticator app. This is optional and won't block the rest of onboarding.
                </p>
              </div>
              <span class="pill mono auto-pill" :class="mfaPillClass()">{{ mfaPillLabel() }}</span>
            </div>

            <div class="mfa-actions">
              <button
                class="secondary"
                type="button"
                @click="setMfaOptional('skipped')"
                :disabled="!auth.authenticated || loading || isMfaBlocked() || isMfaDecided()"
              >
                Skip
              </button>
              <button
                class="primary"
                type="button"
                @click="setMfaOptional('done')"
                :disabled="!auth.authenticated || loading || isMfaBlocked() || isMfaDecided()"
              >
                Mark complete
              </button>
            </div>

            <details class="mfa-qr" @toggle="maybeGenerateMfaQrs">
              <summary class="mono">Show app install QR codes</summary>
              <p v-if="mfaQrError" class="muted mfa-error">{{ mfaQrError }}</p>
              <div v-else class="mfa-qr-grid">
                <div class="mfa-qr-card">
                  <span class="mono mfa-qr-label">Aegis (Android)</span>
                  <img v-if="aegisQr" class="mfa-qr-img" :src="aegisQr" alt="Aegis Android app QR code" />
                  <a class="mono mfa-qr-link" :href="AEGIS_URL" target="_blank" rel="noreferrer">Open store</a>
                </div>
                <div class="mfa-qr-card">
                  <span class="mono mfa-qr-label">FreeOTP (iPhone)</span>
                  <img v-if="freeOtpQr" class="mfa-qr-img" :src="freeOtpQr" alt="FreeOTP iPhone app QR code" />
                  <a class="mono mfa-qr-link" :href="FREEOTP_URL" target="_blank" rel="noreferrer">Open store</a>
                </div>
              </div>
            </details>
          </li>

          <li class="check-item" :class="checkItemClass('element_recovery_key')">
            <label>
              <input type="checkbox" :checked="isStepDone('element_recovery_key')" disabled />
              <span>Create an Element recovery key</span>
              <span class="pill mono auto-pill" :class="stepPillClass('element_recovery_key')">
                {{ stepPillLabel("element_recovery_key") }}
              </span>
            </label>
            <div class="recovery-verify">
              <input
                v-model="elementRecoveryKey"
                class="input mono"
                type="text"
                placeholder="Paste recovery key (hashed locally)"
                :disabled="
                  !auth.authenticated || loading || isStepDone('element_recovery_key') || isStepBlocked('element_recovery_key')
                "
              />
              <button
                class="primary verify"
                type="button"
                @click="verifyElementRecoveryKey"
                :disabled="
                  !auth.authenticated ||
                  loading ||
                  isStepDone('element_recovery_key') ||
                  isStepBlocked('element_recovery_key') ||
                  !elementRecoveryKey.trim()
                "
              >
                Verify
              </button>
            </div>
            <p class="muted">
              In Element, create a recovery key so you can restore encrypted history if you lose a device. Atlas stores only a
              SHA-256 hash so the recovery key itself is never saved server-side. Open
              <a href="https://live.bstein.dev/#/settings" target="_blank" rel="noreferrer">Element settings</a> → Encryption.
            </p>
          </li>

          <li class="check-item" :class="checkItemClass('element_recovery_key_stored')">
            <label>
              <input
                type="checkbox"
                :checked="isStepDone('element_recovery_key_stored')"
                :disabled="!auth.authenticated || loading || isStepBlocked('element_recovery_key_stored')"
                @change="toggleStep('element_recovery_key_stored', $event)"
              />
              <span>Store the recovery key in Vaultwarden</span>
              <span class="pill mono auto-pill" :class="stepPillClass('element_recovery_key_stored')">
                {{ stepPillLabel("element_recovery_key_stored") }}
              </span>
            </label>
            <p class="muted">Add the Element recovery key to Vaultwarden so it's stored safely.</p>
          </li>

          <li class="check-item" :class="checkItemClass('firefly_login')">
            <label>
              <input
                type="checkbox"
                :checked="isStepDone('firefly_login')"
                :disabled="!auth.authenticated || loading || isStepBlocked('firefly_login')"
                @change="toggleStep('firefly_login', $event)"
              />
              <span>Sign in to Firefly III</span>
              <span class="pill mono auto-pill" :class="stepPillClass('firefly_login')">
                {{ stepPillLabel("firefly_login") }}
              </span>
            </label>
            <p class="muted">
              Open <a href="https://money.bstein.dev" target="_blank" rel="noreferrer">money.bstein.dev</a> and sign in
              with the credentials from your <a href="/account">Account</a> page. Change the password immediately and
              save it in Vaultwarden. In the Abacus app, set the server URL to money.bstein.dev and log in once.
            </p>
          </li>

          <li class="check-item" :class="checkItemClass('health_data_notice')">
            <label>
              <input
                type="checkbox"
                :checked="isStepDone('health_data_notice')"
                :disabled="!auth.authenticated || loading || isStepBlocked('health_data_notice')"
                @change="toggleStep('health_data_notice', $event)"
              />
              <span>Review the Wger health data notice</span>
              <span class="pill mono auto-pill" :class="stepPillClass('health_data_notice')">
                {{ stepPillLabel("health_data_notice") }}
              </span>
            </label>
            <p class="muted">
              Wger is a personal wellness tool, not medical advice. Use it at your own risk. Your health data belongs to
              you and will never be sold or used beyond providing the service. We apply best practices to protect it,
              but no system is risk-free.
            </p>
          </li>

          <li class="check-item" :class="checkItemClass('wger_login')">
            <label>
              <input
                type="checkbox"
                :checked="isStepDone('wger_login')"
                :disabled="!auth.authenticated || loading || isStepBlocked('wger_login')"
                @change="toggleStep('wger_login', $event)"
              />
              <span>Sign in to Wger</span>
              <span class="pill mono auto-pill" :class="stepPillClass('wger_login')">
                {{ stepPillLabel("wger_login") }}
              </span>
            </label>
            <p class="muted">
              Open <a href="https://health.bstein.dev" target="_blank" rel="noreferrer">health.bstein.dev</a> and sign in
              with the credentials shown on your <a href="/account">Account</a> page. Change the password immediately and
              store it in Vaultwarden. In the mobile app, set the server to health.bstein.dev and sign in once.
            </p>
          </li>

          <li class="check-item" :class="checkItemClass('vaultwarden_browser_extension')">
            <label>
              <input
                type="checkbox"
                :checked="isStepDone('vaultwarden_browser_extension')"
                :disabled="!auth.authenticated || loading || isStepBlocked('vaultwarden_browser_extension')"
                @change="toggleStep('vaultwarden_browser_extension', $event)"
              />
              <span>Install the Vaultwarden browser extension</span>
              <span class="pill mono auto-pill" :class="stepPillClass('vaultwarden_browser_extension')">
                {{ stepPillLabel("vaultwarden_browser_extension") }}
              </span>
            </label>
            <p class="muted">
              Install Bitwarden in your browser and point it at vault.bstein.dev (Settings → Account → Environment → Self-hosted).
              <a href="https://bitwarden.com/download" target="_blank" rel="noreferrer">Bitwarden downloads</a>.
            </p>
          </li>

          <li class="check-item" :class="checkItemClass('vaultwarden_desktop_app')">
            <label>
              <input
                type="checkbox"
                :checked="isStepDone('vaultwarden_desktop_app')"
                :disabled="!auth.authenticated || loading || isStepBlocked('vaultwarden_desktop_app')"
                @change="toggleStep('vaultwarden_desktop_app', $event)"
              />
              <span>Install the Vaultwarden desktop app</span>
              <span class="pill mono auto-pill" :class="stepPillClass('vaultwarden_desktop_app')">
                {{ stepPillLabel("vaultwarden_desktop_app") }}
              </span>
            </label>
            <p class="muted">
              Install the Bitwarden desktop app and set the server to vault.bstein.dev (Settings → Account → Environment → Self-hosted).
              <a href="https://bitwarden.com/download" target="_blank" rel="noreferrer">Bitwarden downloads</a>.
            </p>
          </li>

          <li class="check-item" :class="checkItemClass('vaultwarden_mobile_app')">
            <label>
              <input
                type="checkbox"
                :checked="isStepDone('vaultwarden_mobile_app')"
                :disabled="!auth.authenticated || loading || isStepBlocked('vaultwarden_mobile_app')"
                @change="toggleStep('vaultwarden_mobile_app', $event)"
              />
              <span>Install Bitwarden on your phone</span>
              <span class="pill mono auto-pill" :class="stepPillClass('vaultwarden_mobile_app')">
                {{ stepPillLabel("vaultwarden_mobile_app") }}
              </span>
            </label>
            <p class="muted">
              Install the mobile app, set the server to vault.bstein.dev, and enable biometrics for fast unlock.
              <a href="https://bitwarden.com/download" target="_blank" rel="noreferrer">Bitwarden downloads</a>.
            </p>
          </li>

          <li class="check-item" :class="checkItemClass('actual_login')">
            <label>
              <input
                type="checkbox"
                :checked="isStepDone('actual_login')"
                :disabled="!auth.authenticated || loading || isStepBlocked('actual_login')"
                @change="toggleStep('actual_login', $event)"
              />
              <span>Optional: sign in to Actual Budget</span>
              <span class="pill mono auto-pill" :class="stepPillClass('actual_login')">
                {{ stepPillLabel("actual_login") }}
              </span>
            </label>
            <p class="muted">
              Open <a href="https://budget.bstein.dev" target="_blank" rel="noreferrer">budget.bstein.dev</a> and sign in
              with your Keycloak account.
            </p>
          </li>

          <li v-for="step in extraSteps" :key="step.id" class="check-item" :class="checkItemClass(step.id)">
            <label>
              <input
                type="checkbox"
                :checked="isStepDone(step.id)"
                :disabled="!auth.authenticated || loading || isStepBlocked(step.id)"
                @change="toggleStep(step.id, $event)"
              />
              <span>{{ step.title }}</span>
              <span class="pill mono auto-pill" :class="stepPillClass(step.id)">
                {{ stepPillLabel(step.id) }}
              </span>
            </label>
            <p class="muted">
              {{ step.description }}
              <a v-if="step.primaryLink" :href="step.primaryLink.href" target="_blank" rel="noreferrer">{{
                step.primaryLink.text
              }}</a>
              .
            </p>
          </li>
        </ul>
        <div class="mobile-guides">
          <div class="module-head">
            <h3>Mobile app guides</h3>
            <span class="pill mono">step-by-step</span>
          </div>
          <p class="muted">
            Each guide expands with screenshots. Set the server URL before logging in so everything points at Atlas.
          </p>
          <div class="guide-grid">
            <div v-for="guide in mobileGuides" :key="guide.id" class="card module guide-card">
              <div class="module-head">
                <h3>{{ guide.title }}</h3>
                <span class="pill mono">{{ guide.app }}</span>
              </div>
              <p class="muted">{{ guide.description }}</p>
              <div class="kv">
                <div v-for="field in guide.fields" :key="`${guide.id}-${field.label}`" class="row">
                  <span class="k mono">{{ field.label }}</span>
                  <span class="v mono">
                    <a v-if="field.href" class="guide-link" :href="field.href" target="_blank" rel="noreferrer">
                      {{ field.value }}
                    </a>
                    <span v-else>{{ field.value }}</span>
                  </span>
                </div>
              </div>
              <div class="guide-actions">
                <a class="pill mono guide-link" :href="guide.url" target="_blank" rel="noreferrer">Open</a>
              </div>
              <details class="guide-details">
                <summary class="mono">Photo guide</summary>
                <div v-if="guideShots[guide.id] && guideShots[guide.id].length" class="guide-images">
                  <figure v-for="(shot, index) in guideShots[guide.id]" :key="shot.url" class="guide-shot">
                    <img :src="shot.url" :alt="`${guide.title} step ${index + 1}`" loading="lazy" />
                    <figcaption v-if="shot.label" class="mono">{{ shot.label }}</figcaption>
                  </figure>
                </div>
                <p v-else class="muted">Guide coming soon.</p>
              </details>
            </div>
          </div>
        </div>

        <div v-if="status === 'ready'" class="ready-box">
          <h3>You're ready</h3>
          <p class="muted">
            Your Atlas account is provisioned and onboarding is complete. You can log in at
            <a href="https://cloud.bstein.dev" target="_blank" rel="noreferrer">cloud.bstein.dev</a>,
            <a href="https://notes.bstein.dev" target="_blank" rel="noreferrer">notes.bstein.dev</a>, and
            <a href="https://tasks.bstein.dev" target="_blank" rel="noreferrer">tasks.bstein.dev</a>.
          </p>
        </div>
      </div>

      <div v-if="status === 'denied'" class="steps">
        <h3>Denied</h3>
        <p class="muted">This request was denied. Contact the Atlas admin if you think this is a mistake.</p>
      </div>

      <div v-if="error" class="error-box">
        <div class="mono">{{ error }}</div>
      </div>
    </section>
  </div>
</template>

<script setup>
import { onMounted, ref } from "vue";
import QRCode from "qrcode";
import { useRoute } from "vue-router";
import { auth, authFetch, login } from "../auth";

const route = useRoute();

const AEGIS_URL = "https://play.google.com/store/apps/details?id=com.beemdevelopment.aegis";
const FREEOTP_URL = "https://apps.apple.com/app/freeotp-authenticator/id872559395";

const requestCode = ref("");
const requestUsername = ref("");
const status = ref("");
const loading = ref(false);
const error = ref("");
const onboarding = ref({ required_steps: [], optional_steps: [], completed_steps: [], optional: {} });
const initialPassword = ref("");
const copied = ref(false);
const usernameCopied = ref(false);
const tasks = ref([]);
const blocked = ref(false);
const elementRecoveryKey = ref("");
const aegisQr = ref("");
const freeOtpQr = ref("");
const mfaQrError = ref("");
const mfaQrReady = ref(false);
const keycloakPasswordRotationRequested = ref(false);
const extraSteps = [
  {
    id: "elementx_setup",
    title: "Install Element X and sign in",
    description:
      "Install Element X on mobile and sign in with your Atlas username/password. Use Element Web → Settings → Sessions to connect your phone via QR.",
    primaryLink: { href: "https://live.bstein.dev", text: "Element" },
  },
  {
    id: "mail_client_setup",
    title: "Set up mail on a device",
    description:
      "Use the IMAP/SMTP details on your Account page to add mail to your phone or desktop client (FairEmail on Android, Apple Mail on iOS, Thunderbird on desktop).",
    primaryLink: { href: "/account", text: "Account" },
  },
  {
    id: "jellyfin_login",
    title: "Sign in to Jellyfin",
    description: "Sign in with your Atlas username/password (LDAP-backed).",
    primaryLink: { href: "https://stream.bstein.dev", text: "Jellyfin" },
  },
  {
    id: "outline_login",
    title: "Open Outline and create your first doc",
    description: "Create a space for your docs and invite collaborators when you're ready.",
    primaryLink: { href: "https://notes.bstein.dev", text: "Outline" },
  },
  {
    id: "planka_login",
    title: "Open Planka and create a board",
    description: "Spin up a project board and invite teammates to collaborate.",
    primaryLink: { href: "https://tasks.bstein.dev", text: "Planka" },
  },
];

// Guide images: drop files into src/assets/onboarding/<guideId>/01-step.png to auto-load and order.
const guideShots = buildGuideShots(
  import.meta.glob("../assets/onboarding/**/*.{png,jpg,jpeg,webp}", { eager: true, as: "url" }),
);

const mobileGuides = [
  {
    id: "vaultwarden",
    title: "Vaultwarden",
    app: "Bitwarden",
    description: "Password manager with autofill across devices.",
    url: "https://vault.bstein.dev",
    fields: [
      { label: "Server", value: "vault.bstein.dev" },
      { label: "Login", value: "Atlas email + password" },
    ],
  },
  {
    id: "elementx",
    title: "Element X",
    app: "Matrix chat",
    description: "Secure chat for Othrys rooms and direct messages.",
    url: "https://live.bstein.dev",
    fields: [
      { label: "Homeserver", value: "live.bstein.dev" },
      { label: "Login", value: "Atlas username + password" },
    ],
  },
  {
    id: "wger",
    title: "Wger",
    app: "Health tracking",
    description: "Workout + nutrition tracking for Atlas.",
    url: "https://health.bstein.dev",
    fields: [
      { label: "Server", value: "health.bstein.dev" },
      { label: "Login", value: "Use Account page credentials" },
    ],
  },
  {
    id: "firefly",
    title: "Firefly III",
    app: "Abacus",
    description: "Personal finance manager for budgets and spending.",
    url: "https://money.bstein.dev",
    fields: [
      { label: "Server", value: "money.bstein.dev" },
      { label: "Login", value: "Use Account page credentials" },
    ],
  },
  {
    id: "nextcloud",
    title: "Nextcloud",
    app: "Files + mail",
    description: "Files, calendar, and mail on the go.",
    url: "https://cloud.bstein.dev",
    fields: [
      { label: "Server", value: "cloud.bstein.dev" },
      { label: "Login", value: "Atlas username + password" },
    ],
  },
  {
    id: "jellyfin",
    title: "Jellyfin",
    app: "Media streaming",
    description: "Personal media library for Atlas.",
    url: "https://stream.bstein.dev",
    fields: [
      { label: "Server", value: "stream.bstein.dev" },
      { label: "Login", value: "Atlas username + password" },
    ],
  },
];

function buildGuideShots(rawShots) {
  const grouped = {};
  for (const [path, url] of Object.entries(rawShots)) {
    const normalized = path.replace(/\\/g, "/");
    const parts = normalized.split("/assets/onboarding/");
    if (parts.length < 2) continue;
    const [guideId, file] = parts[1].split("/");
    if (!guideId || !file) continue;
    const order = guideOrder(file);
    const label = guideLabel(file);
    if (!grouped[guideId]) grouped[guideId] = [];
    grouped[guideId].push({ url, order, label, file });
  }
  Object.values(grouped).forEach((shots) => {
    shots.sort((a, b) => (a.order - b.order) || a.file.localeCompare(b.file));
  });
  return grouped;
}

function guideOrder(filename) {
  const prefix = filename.match(/^(\d{1,3})/);
  if (prefix) return Number(prefix[1]);
  const step = filename.match(/step[-_]?(\d{1,3})/i);
  if (step) return Number(step[1]);
  return Number.MAX_SAFE_INTEGER;
}

function guideLabel(filename) {
  const base = filename.replace(/\.(png|jpe?g|webp)$/i, "");
  return base.replace(/^\d+[-_]?/, "").replace(/[-_]/g, " ").trim();
}

function statusLabel(value) {
  const key = (value || "").trim();
  if (key === "pending_email_verification") return "confirm email";
  if (key === "pending") return "awaiting approval";
  if (key === "accounts_building") return "accounts building";
  if (key === "awaiting_onboarding") return "awaiting onboarding";
  if (key === "ready") return "ready";
  if (key === "denied") return "rejected";
  return key || "unknown";
}

function statusPillClass(value) {
  const key = (value || "").trim();
  if (key === "pending_email_verification") return "pill-warn";
  if (key === "pending") return "pill-wait";
  if (key === "accounts_building") return "pill-warn";
  if (key === "awaiting_onboarding") return "pill-ok";
  if (key === "ready") return "pill-info";
  if (key === "denied") return "pill-bad";
  return "pill-warn";
}

function isStepDone(step) {
  const steps = onboarding.value?.completed_steps || [];
  return Array.isArray(steps) ? steps.includes(step) : false;
}

function requiredStepOrder() {
  if (Array.isArray(onboarding.value?.required_steps) && onboarding.value.required_steps.length) {
    return onboarding.value.required_steps;
  }
  return [
    "vaultwarden_master_password",
    "keycloak_password_rotated",
    "element_recovery_key",
    "element_recovery_key_stored",
    "firefly_login",
    "health_data_notice",
    "wger_login",
  ];
}

function activeRequiredStep() {
  const order = requiredStepOrder();
  for (const step of order) {
    if (!isStepDone(step)) return step;
  }
  return "";
}

function mfaOptionalState() {
  const state = onboarding.value?.optional?.keycloak_mfa_optional?.state;
  if (state === "done" || state === "skipped") return state;
  return "pending";
}

function isMfaDecided() {
  const state = mfaOptionalState();
  return state === "done" || state === "skipped";
}

function isMfaBlocked() {
  return !isStepDone("keycloak_password_rotated");
}

function mfaItemClass() {
  const state = mfaOptionalState();
  return {
    blocked: isMfaBlocked(),
    done: state === "done",
    skipped: state === "skipped",
    optional: true,
  };
}

function mfaPillLabel() {
  if (isMfaBlocked()) return "blocked";
  const state = mfaOptionalState();
  if (state === "done") return "done";
  if (state === "skipped") return "skipped";
  return "optional";
}

function mfaPillClass() {
  if (isMfaBlocked()) return "pill-wait";
  const state = mfaOptionalState();
  if (state === "done") return "pill-ok";
  if (state === "skipped") return "pill-info";
  return "pill-warn";
}

function keycloakRotationPillLabel() {
  if (isStepDone("keycloak_password_rotated")) return "done";
  if (isStepBlocked("keycloak_password_rotated")) return "blocked";
  if (keycloakPasswordRotationRequested.value) return "update now";
  return "ready";
}

function keycloakRotationPillClass() {
  if (isStepDone("keycloak_password_rotated")) return "pill-ok";
  if (isStepBlocked("keycloak_password_rotated")) return "pill-wait";
  if (keycloakPasswordRotationRequested.value) return "pill-warn";
  return "pill-info";
}

async function maybeGenerateMfaQrs(event) {
  if (mfaQrReady.value) return;
  const details = event?.target;
  if (details && details.tagName === "DETAILS" && !details.open) return;
  mfaQrError.value = "";
  try {
    aegisQr.value = await QRCode.toDataURL(AEGIS_URL, { width: 220, margin: 2 });
    freeOtpQr.value = await QRCode.toDataURL(FREEOTP_URL, { width: 220, margin: 2 });
    mfaQrReady.value = true;
  } catch (err) {
    mfaQrError.value = err?.message || "Failed to generate QR codes";
  }
}

function isStepBlocked(step) {
  const order = requiredStepOrder();
  const idx = order.indexOf(step);
  if (idx <= 0) return false;
  for (let i = 0; i < idx; i += 1) {
    if (!isStepDone(order[i])) return true;
  }
  return false;
}

function checkItemClass(step) {
  const activeStep = activeRequiredStep();
  const done = isStepDone(step);
  const blockedStep = isStepBlocked(step);
  const active = !done && !blockedStep && activeStep === step;
  return { done, blocked: blockedStep, active };
}

function stepPillLabel(step) {
  if (isStepDone(step)) return "done";
  if (isStepBlocked(step)) return "blocked";
  return "pending";
}

function stepPillClass(step) {
  if (isStepDone(step)) return "pill-ok";
  if (isStepBlocked(step)) return "pill-wait";
  return "pill-warn";
}

function taskPillClass(status) {
  const key = (status || "").trim();
  if (key === "ok") return "pill-ok";
  if (key === "error") return "pill-bad";
  if (key === "pending") return "pill-warn";
  return "pill-warn";
}

async function check() {
  if (loading.value) return;
  error.value = "";
  loading.value = true;
  try {
    const resp = await fetch("/api/access/request/status", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ request_code: requestCode.value.trim() }),
    });
    const data = await resp.json().catch(() => ({}));
    if (!resp.ok) throw new Error(data.error || resp.statusText || `status ${resp.status}`);
    status.value = data.status || "unknown";
    requestUsername.value = data.username || "";
    onboarding.value = data.onboarding || { required_steps: [], optional_steps: [], completed_steps: [], optional: {} };
    keycloakPasswordRotationRequested.value = Boolean(data.onboarding?.keycloak?.password_rotation_requested);
    tasks.value = Array.isArray(data.tasks) ? data.tasks : [];
    blocked.value = Boolean(data.blocked);
    initialPassword.value = data.initial_password || "";
  } catch (err) {
    error.value = err.message || "Failed to check status";
    tasks.value = [];
    blocked.value = false;
    keycloakPasswordRotationRequested.value = false;
  } finally {
    loading.value = false;
  }
}

async function copyInitialPassword() {
  if (!initialPassword.value) return;
  try {
    if (navigator?.clipboard?.writeText) {
      await navigator.clipboard.writeText(initialPassword.value);
    } else {
      const textarea = document.createElement("textarea");
      textarea.value = initialPassword.value;
      textarea.setAttribute("readonly", "");
      textarea.style.position = "fixed";
      textarea.style.top = "-9999px";
      textarea.style.left = "-9999px";
      document.body.appendChild(textarea);
      textarea.select();
      textarea.setSelectionRange(0, textarea.value.length);
      document.execCommand("copy");
      document.body.removeChild(textarea);
    }
    copied.value = true;
    setTimeout(() => (copied.value = false), 1500);
  } catch (err) {
    error.value = err?.message || "Failed to copy password";
  }
}

async function copyUsername() {
  if (!requestUsername.value) return;
  try {
    if (navigator?.clipboard?.writeText) {
      await navigator.clipboard.writeText(requestUsername.value);
    } else {
      const textarea = document.createElement("textarea");
      textarea.value = requestUsername.value;
      textarea.setAttribute("readonly", "");
      textarea.style.position = "fixed";
      textarea.style.top = "-9999px";
      textarea.style.left = "-9999px";
      document.body.appendChild(textarea);
      textarea.select();
      textarea.setSelectionRange(0, textarea.value.length);
      document.execCommand("copy");
      document.body.removeChild(textarea);
    }
    usernameCopied.value = true;
    setTimeout(() => (usernameCopied.value = false), 1500);
  } catch (err) {
    error.value = err?.message || "Failed to copy username";
  }
}

async function loginToContinue() {
  const trimmedCode = requestCode.value.trim();
  const hint = requestUsername.value.trim() || trimmedCode.split("~", 1)[0] || "";
  await login(`/onboarding?code=${encodeURIComponent(trimmedCode)}`, hint);
}

async function setMfaOptional(state) {
  if (!auth.authenticated) {
    error.value = "Log in to update onboarding steps.";
    return;
  }
  if (isMfaBlocked()) {
    error.value = "Rotate your Keycloak password first.";
    return;
  }
  if (state !== "done" && state !== "skipped") return;
  error.value = "";
  loading.value = true;
  try {
    const resp = await authFetch("/api/access/request/onboarding/mfa", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ request_code: requestCode.value.trim(), state }),
    });
    const data = await resp.json().catch(() => ({}));
    if (!resp.ok) throw new Error(data.error || resp.statusText || `status ${resp.status}`);
    status.value = data.status || status.value;
    onboarding.value = data.onboarding || onboarding.value;
  } catch (err) {
    error.value = err.message || "Failed to update MFA step";
  } finally {
    loading.value = false;
  }
}

async function requestKeycloakPasswordRotation() {
  if (!auth.authenticated) {
    error.value = "Log in to request password rotation.";
    return;
  }
  if (isStepBlocked("keycloak_password_rotated")) {
    error.value = "Complete earlier onboarding steps first.";
    return;
  }
  if (keycloakPasswordRotationRequested.value) return;
  error.value = "";
  loading.value = true;
  try {
    const resp = await authFetch("/api/access/request/onboarding/keycloak-password-rotate", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ request_code: requestCode.value.trim() }),
    });
    const data = await resp.json().catch(() => ({}));
    if (!resp.ok) throw new Error(data.error || resp.statusText || `status ${resp.status}`);
    status.value = data.status || status.value;
    onboarding.value = data.onboarding || onboarding.value;
    keycloakPasswordRotationRequested.value = Boolean(data.onboarding?.keycloak?.password_rotation_requested);
  } catch (err) {
    error.value = err.message || "Failed to request password rotation";
  } finally {
    loading.value = false;
  }
}

async function toggleStep(step, event) {
  const checked = Boolean(event?.target?.checked);
  if (!auth.authenticated) {
    event?.preventDefault?.();
    return;
  }
  if (step === "keycloak_password_rotated") {
    event?.preventDefault?.();
    return;
  }
  if (step === "element_recovery_key") {
    event?.preventDefault?.();
    return;
  }
  error.value = "";
  loading.value = true;
  try {
    const resp = await authFetch("/api/access/request/onboarding/attest", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ request_code: requestCode.value.trim(), step, completed: checked }),
    });
    const data = await resp.json().catch(() => ({}));
    if (!resp.ok) throw new Error(data.error || resp.statusText || `status ${resp.status}`);
    status.value = data.status || status.value;
    onboarding.value = data.onboarding || onboarding.value;
  } catch (err) {
    error.value = err.message || "Failed to update onboarding";
  } finally {
    loading.value = false;
  }
}

async function sha256Hex(text) {
  const encoder = new TextEncoder();
  const data = encoder.encode(text);
  const digest = await crypto.subtle.digest("SHA-256", data);
  return Array.from(new Uint8Array(digest))
    .map((byte) => byte.toString(16).padStart(2, "0"))
    .join("");
}

async function verifyElementRecoveryKey() {
  if (!auth.authenticated) {
    error.value = "Log in to verify your recovery key.";
    return;
  }
  if (isStepBlocked("element_recovery_key")) {
    error.value = "Complete earlier onboarding steps first.";
    return;
  }
  const raw = elementRecoveryKey.value.trim();
  if (!raw) return;
  error.value = "";
  loading.value = true;
  try {
    const hash = await sha256Hex(raw);
    const resp = await authFetch("/api/access/request/onboarding/element-recovery", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ request_code: requestCode.value.trim(), sha256: hash }),
    });
    const data = await resp.json().catch(() => ({}));
    if (!resp.ok) throw new Error(data.error || resp.statusText || `status ${resp.status}`);
    status.value = data.status || status.value;
    onboarding.value = data.onboarding || onboarding.value;
    elementRecoveryKey.value = "";
  } catch (err) {
    error.value = err.message || "Failed to verify recovery key";
  } finally {
    loading.value = false;
  }
}

onMounted(async () => {
  const code = route.query.code || route.query.request_code || "";
  if (typeof code === "string" && code.trim()) {
    requestCode.value = code.trim();
    await check();
  }
});
</script>

<style scoped>
.page {
  max-width: 960px;
  margin: 0 auto;
  padding: 32px 22px 72px;
}

.hero {
  margin-bottom: 12px;
  padding: 18px;
}

.eyebrow {
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-muted);
  margin: 0 0 6px;
  font-size: 13px;
}

h1 {
  margin: 0 0 6px;
  font-size: 32px;
}

.lede {
  margin: 0;
  color: var(--text-muted);
  max-width: 640px;
}

.module {
  padding: 18px;
}

.module-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 18px;
}

.status-form {
  display: flex;
  gap: 10px;
  margin-top: 12px;
}

.status-meta {
  margin-top: 12px;
  padding: 12px;
  border-radius: 14px;
  border: 1px solid rgba(255, 255, 255, 0.1);
  background: rgba(0, 0, 0, 0.18);
}

.meta-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
}

.meta-row .label {
  color: var(--text-muted);
}

.meta-row .value {
  color: var(--text-strong);
}

.input {
  flex: 1;
  padding: 10px 12px;
  border-radius: 10px;
  border: 1px solid rgba(255, 255, 255, 0.14);
  background: rgba(0, 0, 0, 0.25);
  color: var(--text-primary);
}

button.primary {
  background: linear-gradient(90deg, #4f8bff, #7dd0ff);
  color: #0b1222;
  padding: 10px 14px;
  border: none;
  border-radius: 10px;
  cursor: pointer;
  font-weight: 700;
}

.steps {
  margin-top: 16px;
}

.steps h3 {
  margin: 0 0 8px;
}

.onboarding-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 8px;
}

.login-callout {
  margin-top: 10px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  padding: 12px;
  border-radius: 14px;
  border: 1px solid rgba(255, 255, 255, 0.1);
  background: rgba(0, 0, 0, 0.18);
}

.checklist {
  margin: 14px 0 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: 12px;
}

.check-item {
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 14px;
  padding: 12px 12px 10px;
  background: rgba(255, 255, 255, 0.02);
}

.check-item.blocked {
  opacity: 0.55;
}

.check-item.active {
  border-color: rgba(125, 208, 255, 0.45);
  background: rgba(79, 139, 255, 0.08);
  box-shadow: 0 0 0 1px rgba(79, 139, 255, 0.2);
}

.check-item.done {
  border-color: rgba(92, 214, 167, 0.35);
  background: rgba(92, 214, 167, 0.05);
}

.check-item.skipped {
  border-color: rgba(146, 158, 182, 0.25);
}

.check-item.done label,
.check-item.active label {
  color: var(--text-primary);
}

.check-item label {
  display: flex;
  align-items: center;
  gap: 10px;
  font-weight: 650;
  color: var(--text-strong);
}

.auto-pill {
  margin-left: auto;
  font-size: 12px;
  padding: 3px 10px;
  border-radius: 999px;
}

.check-item input[type="checkbox"] {
  width: 18px;
  height: 18px;
}

.recovery-verify {
  display: flex;
  gap: 10px;
  margin-top: 10px;
  align-items: stretch;
}

.recovery-verify .input {
  flex: 1;
}

.recovery-verify .verify {
  min-width: 96px;
}

.mfa-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
}

.mfa-title {
  font-weight: 650;
  color: var(--text-strong);
}

.mfa-description {
  margin: 6px 0 0;
}

.mfa-actions {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
  margin-top: 10px;
}

button.secondary {
  padding: 10px 14px;
  border-radius: 10px;
  border: 1px solid rgba(255, 255, 255, 0.14);
  background: rgba(0, 0, 0, 0.22);
  color: var(--text-primary);
  cursor: pointer;
  font-weight: 650;
}

.mfa-qr {
  margin-top: 12px;
  border-radius: 12px;
  border: 1px solid rgba(255, 255, 255, 0.08);
  background: rgba(0, 0, 0, 0.16);
  padding: 10px 12px;
}

.mfa-qr summary {
  cursor: pointer;
  color: var(--text-muted);
}

.mfa-qr-grid {
  margin-top: 12px;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: 12px;
}

.mfa-qr-card {
  display: grid;
  justify-items: center;
  gap: 8px;
  padding: 12px;
  border-radius: 12px;
  border: 1px solid rgba(255, 255, 255, 0.08);
  background: rgba(255, 255, 255, 0.02);
}

.mfa-qr-label {
  color: var(--text-muted);
  font-size: 12px;
}

.mfa-qr-img {
  width: 180px;
  height: 180px;
  border-radius: 10px;
  background: #ffffff;
  padding: 6px;
}

.mfa-qr-link {
  color: rgba(125, 208, 255, 0.9);
  font-size: 12px;
  text-decoration: none;
}

.mfa-error {
  margin-top: 10px;
}

.howto {
  margin-top: 10px;
  border-radius: 12px;
  border: 1px solid rgba(255, 255, 255, 0.08);
  background: rgba(0, 0, 0, 0.16);
  padding: 10px 12px;
}

.howto summary {
  cursor: pointer;
  color: var(--text-muted);
}

.howto-list {
  margin: 10px 0 0;
  padding-left: 18px;
  display: grid;
  gap: 6px;
}

.mobile-guides {
  margin-top: 18px;
  display: grid;
  gap: 12px;
}

.guide-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 12px;
}

.guide-card {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.guide-actions {
  margin-top: 10px;
  display: flex;
  gap: 10px;
}

.guide-link {
  color: rgba(125, 208, 255, 0.9);
  text-decoration: none;
}

.guide-details {
  margin-top: 10px;
  border-radius: 12px;
  border: 1px solid rgba(255, 255, 255, 0.08);
  background: rgba(0, 0, 0, 0.16);
  padding: 10px 12px;
}

.guide-details summary {
  cursor: pointer;
  color: var(--text-muted);
}

.guide-images {
  margin-top: 12px;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 10px;
}

.guide-shot {
  margin: 0;
  display: grid;
  gap: 6px;
}

.guide-shot img {
  width: 100%;
  border-radius: 10px;
  border: 1px solid rgba(255, 255, 255, 0.08);
  background: rgba(0, 0, 0, 0.25);
}

.guide-shot figcaption {
  color: var(--text-muted);
  font-size: 12px;
}

@media (max-width: 560px) {
  .recovery-verify {
    flex-direction: column;
  }

  .recovery-verify .verify {
    width: 100%;
  }
}

.ready-box {
  margin-top: 14px;
  padding: 14px;
  border-radius: 14px;
  border: 1px solid rgba(120, 180, 255, 0.25);
  background: rgba(120, 180, 255, 0.06);
}

.initial-password {
  margin-top: 14px;
  padding: 14px;
  border-radius: 14px;
  border: 1px solid rgba(255, 220, 120, 0.25);
  background: rgba(255, 220, 120, 0.06);
}

.task-box {
  margin-top: 14px;
  padding: 14px;
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 14px;
  background: rgba(0, 0, 0, 0.25);
}

.task-list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: grid;
  gap: 10px;
}

.task-row {
  display: grid;
  gap: 6px;
  grid-template-columns: 1fr auto;
  align-items: center;
}

.task-name {
  color: var(--text);
}

.task-detail {
  grid-column: 1 / -1;
  color: var(--text-muted);
  font-size: 12px;
}

.request-code-row {
  margin-top: 12px;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.copy {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  border-radius: 12px;
  border: 1px solid rgba(255, 255, 255, 0.14);
  background: rgba(0, 0, 0, 0.22);
  color: var(--text-primary);
  padding: 10px 12px;
  cursor: pointer;
}

.copied {
  font-size: 12px;
  color: rgba(120, 255, 160, 0.9);
}

.steps ol {
  margin: 0;
  padding-left: 18px;
  color: var(--text-muted);
}

.muted {
  color: var(--text-muted);
}

.error-box {
  margin-top: 12px;
  border-radius: 12px;
  border: 1px solid rgba(255, 87, 87, 0.5);
  background: rgba(255, 87, 87, 0.06);
  padding: 10px 12px;
}
</style>
