/// Vault initialization wizard — 5-step flow for creating new relicario vaults.
///
/// Step 1: Choose host type (Gitea / GitHub)
/// Step 2: Configure connection (URL, repo, token) + test
/// Step 3: Create vault (carrier image, passphrase, generate secrets, push files)
/// Step 4: Name this device (generates ed25519 keypair, registers with vault)
/// Step 5: Finish (download reference image, push config to extension or copy JSON)

import { createGitHost, uint8ArrayToBase64 } from '../service-worker/git-host';
import { addDevice } from '../service-worker/devices';
import { probeVault } from './probe';
import {
  escapeHtml,
  ratePassphrase,
  scheduleRate,
  STRENGTH_LABELS,
  entropyText,
} from './setup-helpers';
import { GLYPH_NEXT } from '../shared/glyphs';
import type { VaultConfig } from '../shared/types';
import type { SessionHandle } from 'relicario-wasm';

// --- WASM module (loaded dynamically) ---

type WasmModule = typeof import('relicario-wasm');
let wasm: WasmModule | null = null;

async function loadWasm(): Promise<WasmModule> {
  if (wasm) return wasm;
  const mod = await import(
    // @ts-ignore TS2307 — resolved at runtime, not by TS/webpack
    /* webpackIgnore: true */ '../relicario_wasm.js'
  ) as WasmModule & { default: (input?: string | URL) => Promise<void> };
  await mod.default('../relicario_wasm_bg.wasm');
  wasm = mod;
  return mod;
}

// --- State ---

interface WizardState {
  step: number;                       // now 0..5; was 1..5
  mode: 'new' | 'attach' | null;      // null until Step 0 picks
  hostType: 'gitea' | 'github';
  hostUrl: string;
  repoPath: string;
  apiToken: string;
  connectionTested: boolean;
  vaultProbe: import('./probe').VaultProbe | null;
  carrierImageBytes: Uint8Array | null;
  referenceImageBytesAttach: Uint8Array | null;
  passphrase: string;
  passphraseConfirm: string;
  // zxcvbn meter state — -1 means "not yet scored" (empty passphrase).
  passphraseScore: number;
  passphraseGuessesLog10: number;   // -1 before first rating
  passphraseVisible: boolean;
  confirmVisible: boolean;
  referenceImageBytes: Uint8Array | null;
  verifiedHandle: SessionHandle | null;
  creating: boolean;
  attaching: boolean;
  error: string | null;
  extensionDetected: boolean;
  configPushed: boolean;
  deviceName: string;
}

const state: WizardState = {
  step: 0,
  mode: null,
  hostType: 'gitea',
  hostUrl: '',
  repoPath: '',
  apiToken: '',
  connectionTested: false,
  vaultProbe: null,
  carrierImageBytes: null,
  referenceImageBytesAttach: null,
  passphrase: '',
  passphraseConfirm: '',
  passphraseScore: -1,
  passphraseGuessesLog10: -1,
  passphraseVisible: false,
  confirmVisible: false,
  referenceImageBytes: null,
  verifiedHandle: null,
  creating: false,
  attaching: false,
  error: null,
  extensionDetected: false,
  configPushed: false,
  deviceName: '',
};

// --- State-coupled helpers (pure helpers live in ./setup-helpers.ts) ---

/// Update just the meter DOM without a full re-render (so the input keeps
/// focus and the user's cursor position is preserved). Also updates the
/// char counter and confirm-match indicator live.
function updateStrengthUi(): void {
  const bar = document.getElementById('strength-bar');
  const label = document.getElementById('strength-label');
  const entropy = document.getElementById('entropy-line');
  const counter = document.getElementById('passphrase-counter');
  const matchInd = document.getElementById('match-indicator');
  const create = document.getElementById('create-btn') as HTMLButtonElement | null;

  const score = state.passphraseScore;
  const guessesLog10 = state.passphraseGuessesLog10;

  if (bar) bar.className = `strength-bar${score >= 0 ? ` s${score}` : ''}`;

  if (label) {
    if (score < 0) {
      label.className = 'strength-label';
      label.innerHTML = '&nbsp;';
    } else {
      const meta = STRENGTH_LABELS[score] ?? STRENGTH_LABELS[0];
      label.className = `strength-label ${meta.cls}`;
      label.textContent = meta.text;
    }
  }

  if (entropy) {
    const txt = entropyText(guessesLog10);
    entropy.textContent = txt;
    entropy.style.visibility = txt ? 'visible' : 'hidden';
  }

  if (counter) {
    const n = state.passphrase.length;
    counter.textContent = n === 0 ? '' : `${n} character${n === 1 ? '' : 's'}`;
  }

  if (matchInd) {
    const p = state.passphrase;
    const c = state.passphraseConfirm;
    if (!p || !c) {
      matchInd.className = 'match-indicator';
      matchInd.textContent = '';
    } else if (p === c) {
      matchInd.className = 'match-indicator ok';
      matchInd.textContent = '✓';
    } else {
      matchInd.className = 'match-indicator bad';
      matchInd.textContent = '✗';
    }
  }

  const matchOk = !state.passphraseConfirm || state.passphrase === state.passphraseConfirm;
  if (create) {
    const disabled = state.creating || score < 3 || !state.passphraseConfirm || !matchOk;
    create.disabled = disabled;
    create.title = disabled
      ? (score < 3
          ? 'passphrase must score "good" or better'
          : !state.passphraseConfirm ? 'confirm your passphrase'
          : !matchOk ? 'passphrases do not match'
          : '')
      : '';
  }
}

// --- Render ---

function render(): void {
  const app = document.getElementById('app');
  if (!app) return;

  const progressHtml = `
    <div class="progress-bar">
      <div class="step ${state.step > 0 ? 'done' : state.step === 0 ? 'current' : ''}"></div>
      <div class="step ${state.step > 1 ? 'done' : state.step === 1 ? 'current' : ''}"></div>
      <div class="step ${state.step > 2 ? 'done' : state.step === 2 ? 'current' : ''}"></div>
      <div class="step ${state.step > 3 ? 'done' : state.step === 3 ? 'current' : ''}"></div>
      <div class="step ${state.step > 4 ? 'done' : state.step === 4 ? 'current' : ''}"></div>
      <div class="step ${state.step > 5 ? 'done' : state.step === 5 ? 'current' : ''}"></div>
    </div>
  `;

  let stepHtml = '';
  switch (state.step) {
    case 0: stepHtml = renderStep0(); break;
    case 1: stepHtml = renderStep1(); break;
    case 2: stepHtml = renderStep2(); break;
    case 3: stepHtml = state.mode === 'attach' ? renderStep3Attach() : renderStep3New(); break;
    case 4: stepHtml = renderStep4(); break;
    case 5: stepHtml = renderStep5(); break;
  }

  app.innerHTML = `
    <div class="surface-backdrop" style="min-height:100vh;">
      <div class="pad" style="padding-top:12px;">
        <img class="brand-logo" src="icons/relicario-logo.svg" alt="" style="margin-bottom:12px;">
        <div class="brand" style="margin-bottom:4px;">Relicario vault setup</div>
        ${progressHtml}
        ${state.error ? `<div class="error">${escapeHtml(state.error)}</div>` : ''}
        ${stepHtml}
      </div>
    </div>
  `;

  switch (state.step) {
    case 0: attachStep0(); break;
    case 1: attachStep1(); break;
    case 2: attachStep2(); break;
    case 3: state.mode === 'attach' ? attachStep3Attach() : attachStep3New(); break;
    case 4: attachStep4(); break;
    case 5: attachStep5(); break;
  }
}

// --- Step 0: Mode picker ---

function renderStep0(): string {
  const isNew = state.mode === 'new';
  const isAttach = state.mode === 'attach';
  return `
    <div class="wizard-step glass" style="padding: 24px; margin-top: 16px;">
      <h3>set up Relicario</h3>
      <p class="muted" style="margin-bottom:16px;">
        How are you using Relicario on this device?
      </p>
      <div class="mode-cards">
        <button class="mode-card glass ${isNew ? 'active' : ''}" data-mode="new">
          <div class="mode-card-title">create new vault</div>
          <p class="mode-card-blurb">
            I'm setting up Relicario for the first time. This will create a fresh
            encrypted vault on a new or empty git repository.
          </p>
        </button>
        <button class="mode-card glass ${isAttach ? 'active' : ''}" data-mode="attach">
          <div class="mode-card-title">attach this device</div>
          <p class="mode-card-blurb">
            I already have a vault on another device. Connect this browser to it
            using my passphrase and reference image.
          </p>
        </button>
      </div>
      <div class="form-actions" style="margin-top:24px;">
        <button class="btn-primary" id="next-btn" ${state.mode ? '' : 'disabled'}>next ${GLYPH_NEXT}</button>
      </div>
    </div>
  `;
}

function attachStep0(): void {
  document.querySelectorAll('.mode-card').forEach((btn) => {
    btn.addEventListener('click', () => {
      state.mode = (btn as HTMLElement).dataset.mode as 'new' | 'attach';
      render();
    });
  });
  document.getElementById('next-btn')?.addEventListener('click', () => {
    if (!state.mode) return;
    state.step = 1;
    state.error = null;
    render();
  });
}

// --- Step 3 (attach variant) ---

function renderStep3Attach(): string {
  const p = state.passphrase;
  const pType = state.passphraseVisible ? 'text' : 'password';
  const pToggle = state.passphraseVisible ? 'hide' : 'show';
  const hasImage = !!state.referenceImageBytesAttach;
  const gateDisabled = state.attaching || !p || !hasImage;

  return `
    <div class="wizard-step glass" style="padding: 24px; margin-top: 16px;">
      <h3>attach this device</h3>
      <p class="muted" style="margin-bottom:12px;">
        Use your existing passphrase and reference image to attach this browser
        to your vault. We'll verify both before registering this device.
      </p>

      <div class="form-group">
        <label class="label">reference image (JPEG)</label>
        <div class="file-drop ${hasImage ? 'has-file' : ''}" id="ref-drop">
          <input type="file" id="ref-input" accept="image/jpeg" style="display:none;">
          ${hasImage
            ? '<p class="secondary">reference image loaded</p>'
            : '<p class="secondary">click to select your reference JPEG</p>'}
        </div>
        <p class="muted" style="margin-top:4px;">
          The reference image is the JPEG you saved when you first created this vault —
          <strong>not the original photo</strong>. It has the 256-bit secret embedded.
        </p>
      </div>

      <div class="form-group">
        <label class="label" for="passphrase">passphrase</label>
        <div class="passphrase-field">
          <input id="passphrase" type="${pType}" value="${escapeHtml(p)}" placeholder="enter your passphrase" autocomplete="current-password">
          <button type="button" class="eye-btn" id="eye-btn">${pToggle}</button>
        </div>
      </div>

      <div class="form-actions">
        <button class="btn" id="back-btn">back</button>
        <button class="btn btn-primary" id="attach-btn" ${gateDisabled ? 'disabled' : ''}>
          ${state.attaching ? '<span class="spinner"></span> verifying...' : 'verify and attach'}
        </button>
      </div>
    </div>
  `;
}

function attachStep3Attach(): void {
  const refDrop = document.getElementById('ref-drop')!;
  const refInput = document.getElementById('ref-input') as HTMLInputElement;

  refDrop.addEventListener('click', () => refInput.click());
  refInput.addEventListener('change', () => {
    const file = refInput.files?.[0];
    if (!file) return;
    const reader = new FileReader();
    reader.onload = () => {
      state.referenceImageBytesAttach = new Uint8Array(reader.result as ArrayBuffer);
      state.error = null;
      render();
    };
    reader.readAsArrayBuffer(file);
  });

  const passInput = document.getElementById('passphrase') as HTMLInputElement | null;
  passInput?.addEventListener('input', (e) => {
    state.passphrase = (e.target as HTMLInputElement).value;
    const btn = document.getElementById('attach-btn') as HTMLButtonElement | null;
    if (btn) btn.disabled = state.attaching || !state.passphrase || !state.referenceImageBytesAttach;
  });

  document.getElementById('eye-btn')?.addEventListener('click', () => {
    state.passphraseVisible = !state.passphraseVisible;
    if (passInput) passInput.type = state.passphraseVisible ? 'text' : 'password';
    const btn = document.getElementById('eye-btn');
    if (btn) btn.textContent = state.passphraseVisible ? 'hide' : 'show';
    passInput?.focus();
  });

  document.getElementById('back-btn')?.addEventListener('click', () => {
    state.step = 2;
    state.error = null;
    render();
  });

  document.getElementById('attach-btn')?.addEventListener('click', async () => {
    if (!state.referenceImageBytesAttach || !state.passphrase) return;
    state.attaching = true;
    state.error = null;
    render();

    const log = (stage: string, detail?: unknown) => console.log(`[relicario setup] ${stage}`, detail ?? '');
    let stage = 'init';
    let handle: SessionHandle | null = null;
    try {
      stage = 'load wasm';
      log(stage);
      const w = await loadWasm();

      stage = 'fetch vault metadata';
      log(stage);
      const hostUrl = state.hostType === 'github' ? 'https://api.github.com' : state.hostUrl;
      const host = createGitHost(state.hostType, hostUrl, state.repoPath, state.apiToken);
      const [salt, paramsBytes, encryptedManifest] = await Promise.all([
        host.readFile('.relicario/salt'),
        host.readFile('.relicario/params.json'),
        host.readFile('manifest.enc'),
      ]);
      const paramsJson = new TextDecoder().decode(paramsBytes);

      stage = 'derive session handle';
      log(stage);
      handle = w.unlock(state.passphrase, state.referenceImageBytesAttach, salt, paramsJson);

      stage = 'decrypt manifest';
      log(stage);
      // Throws if AEAD verification fails — wrong passphrase or wrong image.
      w.manifest_decrypt(handle, encryptedManifest);

      log('attach verified — advancing');
      state.verifiedHandle = handle;
      state.attaching = false;
      state.step = 4;
      state.error = null;
      render();
    } catch (err: unknown) {
      console.error(`[relicario setup] attach FAILED during "${stage}":`, err);
      state.attaching = false;
      // Lock any partial handle to avoid leaking key material.
      if (handle !== null) {
        try { (await loadWasm()).lock(handle); } catch { /* best effort */ }
      }
      state.verifiedHandle = null;
      const detail = err instanceof Error ? err.message : String(err);
      // Stage-aware copy: if we got past 'fetch', this is a credential failure.
      if (stage === 'derive session handle' || stage === 'decrypt manifest') {
        state.error = 'Could not decrypt vault — wrong passphrase or reference image.';
      } else {
        state.error = `Attach failed at "${stage}": ${detail}`;
      }
      render();
    }
  });
}

// --- Step 1: Choose Host ---

function renderStep1(): string {
  const giteaInstructions = `
    <div class="step-instructions">
      <ol>
        <li>Create a new <strong>private</strong> repository on your Gitea instance (e.g. <code>vault</code>)</li>
        <li>Go to <strong>Settings &rarr; Applications</strong></li>
        <li>Generate a new token with <code>repo</code> (read/write) permission</li>
        <li>Copy the token &mdash; you will need it in the next step</li>
      </ol>
    </div>
  `;

  const githubInstructions = `
    <div class="step-instructions">
      <ol>
        <li>Create a new <strong>private</strong> repository on GitHub (e.g. <code>vault</code>)</li>
        <li>Go to <strong>Settings &rarr; Developer settings &rarr; Personal access tokens &rarr; Fine-grained tokens</strong></li>
        <li>Generate a new token scoped to the vault repo with <strong>Contents</strong> read/write permission</li>
        <li>Copy the token &mdash; you will need it in the next step</li>
      </ol>
    </div>
  `;

  return `
    <div class="wizard-step glass" style="padding: 24px; margin-top: 16px;">
      <h3>choose host</h3>
      <div class="form-group">
        <label class="label">host type</label>
        <div class="toggle-group">
          <button class="${state.hostType === 'gitea' ? 'active' : ''}" data-host="gitea">Gitea</button>
          <button class="${state.hostType === 'github' ? 'active' : ''}" data-host="github">GitHub</button>
        </div>
      </div>
      ${state.hostType === 'gitea' ? giteaInstructions : githubInstructions}
      <div class="form-actions">
        <button class="btn" id="back-btn">back</button>
        <button class="btn-primary" id="next-btn">next ${GLYPH_NEXT}</button>
      </div>
    </div>
  `;
}

function attachStep1(): void {
  document.getElementById('back-btn')?.addEventListener('click', () => {
    state.step = 0;
    state.error = null;
    render();
  });

  document.querySelectorAll('.toggle-group button').forEach(btn => {
    btn.addEventListener('click', () => {
      state.hostType = (btn as HTMLElement).dataset.host as 'gitea' | 'github';
      state.connectionTested = false;
      render();
    });
  });

  document.getElementById('next-btn')?.addEventListener('click', () => {
    state.step = 2;
    state.error = null;
    render();
  });
}

// --- Step 2: Configure Connection ---

function renderProbeBanner(): string {
  const probe = state.vaultProbe;
  if (!state.connectionTested || !probe) return '';
  const meta = probe.lastCommit
    ? `Last commit: <code>${escapeHtml(probe.lastCommit.sha)}</code> by ${escapeHtml(probe.lastCommit.author)} on ${escapeHtml(probe.lastCommit.date.slice(0, 10))}.`
    : '';
  if (state.mode === 'new' && probe.exists) {
    return `
      <div class="banner banner-warn">
        <strong>⚠ This repository already contains a Relicario vault.</strong>
        <p>${meta}</p>
        <p>Creating a new vault here would overwrite the existing one and <strong>destroy all data inside</strong>.
           To use this vault on this device, switch to <em>attach</em> mode instead.
           If you really mean to start over, delete the repository via your git host's web UI and come back here.</p>
        <div class="form-actions">
          <button class="btn" id="switch-mode-btn" data-target="attach">switch to attach mode</button>
        </div>
      </div>`;
  }
  if (state.mode === 'attach' && !probe.exists) {
    return `
      <div class="banner banner-warn">
        <strong>No vault found in this repo.</strong>
        <p>Did you mean to create a new vault?</p>
        <div class="form-actions">
          <button class="btn" id="switch-mode-btn" data-target="new">switch to new-vault mode</button>
        </div>
      </div>`;
  }
  if (state.mode === 'attach' && probe.exists) {
    return `
      <div class="banner banner-ok">
        <strong>✓ Existing vault found.</strong>
        <p>${meta}</p>
        <p>Continue to attach this device.</p>
      </div>`;
  }
  // mode = new, !exists
  return `
    <div class="banner banner-ok">
      <strong>✓ Repo is empty — ready to create a new vault.</strong>
    </div>`;
}

function renderStep2(): string {
  const probe = state.vaultProbe;
  const modeMismatch =
    !!probe && ((state.mode === 'new' && probe.exists) || (state.mode === 'attach' && !probe.exists));
  const nextDisabled = !state.connectionTested || !probe || modeMismatch;
  return `
    <div class="wizard-step glass" style="padding: 24px; margin-top: 16px;">
      <h3>configure connection</h3>
      <div class="form-group" ${state.hostType === 'github' ? 'style="display:none;"' : ''}>
        <label class="label" for="host-url">host url</label>
        <input id="host-url" type="text" value="${escapeHtml(state.hostUrl)}" placeholder="https://git.example.com">
      </div>
      <div class="form-group">
        <label class="label" for="repo-path">repository path</label>
        <input id="repo-path" type="text" value="${escapeHtml(state.repoPath)}" placeholder="user/vault">
      </div>
      <div class="form-group">
        <label class="label" for="api-token">api token</label>
        <input id="api-token" type="password" value="${escapeHtml(state.apiToken)}" placeholder="paste your token here">
      </div>
      <div class="form-actions">
        <button class="btn" id="test-btn">test connection</button>
        ${state.connectionTested ? '<span class="test-result pass">connected</span>' : ''}
      </div>
      ${renderProbeBanner()}
      <div class="form-actions" style="margin-top:12px;">
        <button class="btn" id="back-btn">back</button>
        <button class="btn-primary" id="next-btn" ${nextDisabled ? 'disabled' : ''}>next ${GLYPH_NEXT}</button>
      </div>
    </div>
  `;
}

function attachStep2(): void {
  document.getElementById('test-btn')?.addEventListener('click', async () => {
    state.connectionTested = false;
    state.vaultProbe = null;
    const hostUrl = state.hostType === 'github'
      ? 'https://api.github.com'
      : (document.getElementById('host-url') as HTMLInputElement).value.trim();
    const repoPath = (document.getElementById('repo-path') as HTMLInputElement).value.trim();
    const apiToken = (document.getElementById('api-token') as HTMLInputElement).value.trim();

    if (!repoPath || !apiToken) {
      state.error = 'Repository path and API token are required';
      render();
      return;
    }
    if (state.hostType === 'gitea' && !hostUrl) {
      state.error = 'Host URL is required for Gitea';
      render();
      return;
    }

    state.hostUrl = hostUrl;
    state.repoPath = repoPath;
    state.apiToken = apiToken;

    try {
      const host = createGitHost(state.hostType, hostUrl, repoPath, apiToken);
      await host.listDir('');
      state.connectionTested = true;
      state.error = null;
      try {
        state.vaultProbe = await probeVault(host);
      } catch (probeErr) {
        state.vaultProbe = null;
        state.error = `Could not check repo state: ${probeErr instanceof Error ? probeErr.message : String(probeErr)}`;
      }
    } catch (err: unknown) {
      state.connectionTested = false;
      state.vaultProbe = null;
      state.error = `Connection failed: ${err instanceof Error ? err.message : String(err)}`;
    }
    render();
  });

  document.getElementById('back-btn')?.addEventListener('click', () => {
    state.step = 1;
    state.error = null;
    render();
  });

  document.getElementById('next-btn')?.addEventListener('click', () => {
    if (!state.connectionTested) return;
    state.step = 3;
    state.error = null;
    render();
  });

  document.getElementById('switch-mode-btn')?.addEventListener('click', (e) => {
    const target = (e.currentTarget as HTMLElement).dataset.target as 'new' | 'attach';
    state.mode = target;
    state.error = null;
    render();
  });
}

// --- Step 3 (new-vault variant): Create Vault ---

function renderStep3New(): string {
  const score = state.passphraseScore;
  const guessesLog10 = state.passphraseGuessesLog10;
  const hasScore = score >= 0;
  const meterClass = hasScore ? `s${score}` : '';
  const labelMeta = hasScore ? STRENGTH_LABELS[score] : null;
  const labelClass = labelMeta?.cls ?? '';
  const labelText = labelMeta?.text ?? '&nbsp;';
  const entropy = entropyText(guessesLog10);

  const p = state.passphrase;
  const c = state.passphraseConfirm;
  const matchState = !p || !c ? '' : p === c ? 'ok' : 'bad';
  const matchGlyph = matchState === 'ok' ? '✓' : matchState === 'bad' ? '✗' : '';

  const pType = state.passphraseVisible ? 'text' : 'password';
  const cType = state.confirmVisible ? 'text' : 'password';
  const pToggle = state.passphraseVisible ? 'hide' : 'show';
  const cToggle = state.confirmVisible ? 'hide' : 'show';

  const matchOk = !c || p === c;
  const gateDisabled = state.creating || score < 3 || !c || !matchOk;

  const nChars = p.length;
  const counterText = nChars === 0 ? '' : `${nChars} character${nChars === 1 ? '' : 's'}`;

  return `
    <div class="wizard-step glass" style="padding: 24px; margin-top: 16px;">
      <h3>create vault</h3>

      <div class="form-group">
        <label class="label">carrier image (JPEG)</label>
        <div class="file-drop ${state.carrierImageBytes ? 'has-file' : ''}" id="file-drop">
          <input type="file" id="file-input" accept="image/jpeg" style="display:none;">
          ${state.carrierImageBytes
            ? '<p class="secondary">image loaded</p>'
            : '<p class="secondary">click to select a JPEG photo</p>'}
        </div>
        <p class="muted" style="margin-top:4px;">A 256-bit secret will be steganographically embedded in this image.</p>
      </div>

      <div class="pass-help">
        A long phrase of unrelated words is stronger than a short complex password.
        Your vault needs <strong>good</strong> (score&nbsp;≥&nbsp;3) to continue.
      </div>

      <div class="form-group">
        <label class="label" for="passphrase">passphrase</label>
        <div class="passphrase-field">
          <input id="passphrase" type="${pType}" value="${escapeHtml(p)}" placeholder="enter a strong passphrase" autocomplete="new-password">
          <button type="button" class="eye-btn" id="eye-btn" aria-label="toggle passphrase visibility">${pToggle}</button>
        </div>
        <div class="strength-bar ${meterClass}" id="strength-bar" aria-hidden="true">
          <div class="seg i0"></div>
          <div class="seg i1"></div>
          <div class="seg i2"></div>
          <div class="seg i3"></div>
          <div class="seg i4"></div>
        </div>
        <div class="strength-row">
          <p class="strength-label ${labelClass}" id="strength-label">${labelText}</p>
          <p class="char-counter" id="passphrase-counter">${escapeHtml(counterText)}</p>
        </div>
        <p class="entropy-line" id="entropy-line" style="visibility:${entropy ? 'visible' : 'hidden'};">${escapeHtml(entropy || ' ')}</p>
      </div>

      <div class="form-group">
        <label class="label" for="passphrase-confirm">confirm passphrase</label>
        <div class="passphrase-field">
          <input id="passphrase-confirm" type="${cType}" value="${escapeHtml(c)}" placeholder="re-enter passphrase" autocomplete="new-password">
          <span class="match-indicator ${matchState}" id="match-indicator">${matchGlyph}</span>
          <button type="button" class="eye-btn" id="confirm-eye-btn" aria-label="toggle confirm visibility">${cToggle}</button>
        </div>
      </div>

      <div class="form-actions">
        <button class="btn" id="back-btn">back</button>
        <button class="btn btn-primary" id="create-btn" ${gateDisabled ? 'disabled' : ''}>
          ${state.creating ? '<span class="spinner"></span> creating...' : 'create vault'}
        </button>
      </div>
    </div>
  `;
}

function attachStep3New(): void {
  const fileDrop = document.getElementById('file-drop')!;
  const fileInput = document.getElementById('file-input') as HTMLInputElement;

  fileDrop.addEventListener('click', () => fileInput.click());

  fileInput.addEventListener('change', () => {
    const file = fileInput.files?.[0];
    if (!file) return;
    const reader = new FileReader();
    reader.onload = () => {
      state.carrierImageBytes = new Uint8Array(reader.result as ArrayBuffer);
      state.error = null;
      render();
    };
    reader.readAsArrayBuffer(file);
  });

  // Track passphrase changes inline (no full re-render) so the input keeps focus.
  // zxcvbn score is computed via the SW on a 150ms debounce — see scheduleRate.
  const passInput = document.getElementById('passphrase') as HTMLInputElement | null;
  passInput?.addEventListener('input', (e) => {
    state.passphrase = (e.target as HTMLInputElement).value;
    // Update char counter + match indicator + button gate immediately on every keystroke.
    updateStrengthUi();
    // Score updates on the 150ms debounce to avoid SW hammering.
    scheduleRate(state.passphrase, (s) => {
      state.passphraseScore = s.score;
      state.passphraseGuessesLog10 = s.guessesLog10;
      updateStrengthUi();
    });
  });

  const confirmInput = document.getElementById('passphrase-confirm') as HTMLInputElement | null;
  confirmInput?.addEventListener('input', (e) => {
    state.passphraseConfirm = (e.target as HTMLInputElement).value;
    updateStrengthUi();
  });

  // Eye toggles — flip the input type and label without a full re-render so
  // focus + cursor position survive the click.
  document.getElementById('eye-btn')?.addEventListener('click', () => {
    state.passphraseVisible = !state.passphraseVisible;
    if (passInput) passInput.type = state.passphraseVisible ? 'text' : 'password';
    const btn = document.getElementById('eye-btn');
    if (btn) btn.textContent = state.passphraseVisible ? 'hide' : 'show';
    passInput?.focus();
  });

  document.getElementById('confirm-eye-btn')?.addEventListener('click', () => {
    state.confirmVisible = !state.confirmVisible;
    if (confirmInput) confirmInput.type = state.confirmVisible ? 'text' : 'password';
    const btn = document.getElementById('confirm-eye-btn');
    if (btn) btn.textContent = state.confirmVisible ? 'hide' : 'show';
    confirmInput?.focus();
  });

  document.getElementById('back-btn')?.addEventListener('click', () => {
    state.step = 2;
    state.error = null;
    render();
  });

  document.getElementById('create-btn')?.addEventListener('click', async () => {
    // Read current values from DOM
    state.passphrase = (document.getElementById('passphrase') as HTMLInputElement).value;
    state.passphraseConfirm = (document.getElementById('passphrase-confirm') as HTMLInputElement).value;

    if (!state.carrierImageBytes) {
      state.error = 'Please select a carrier JPEG image';
      render();
      return;
    }
    if (!state.passphrase) {
      state.error = 'Passphrase is required';
      render();
      return;
    }
    // Re-rate synchronously in case the button was clicked before the
    // debounced rater fired. Defence in depth — the button is already
    // disabled in the UI when score < 3 (audit H3).
    const strength = await ratePassphrase(state.passphrase);
    state.passphraseScore = strength.score;
    state.passphraseGuessesLog10 = strength.guessesLog10;
    if (state.passphraseScore < 3) {
      state.error = 'Passphrase is too weak (zxcvbn score must be ≥ 3).';
      render();
      return;
    }
    if (state.passphrase !== state.passphraseConfirm) {
      state.error = 'Passphrases do not match';
      render();
      return;
    }

    state.creating = true;
    state.error = null;
    render();

    // Structured logging so silent failures become visible in DevTools.
    // eslint-disable-next-line no-console
    const log = (stage: string, detail?: unknown) => console.log(`[relicario setup] ${stage}`, detail ?? '');

    let stage = 'init';
    try {
      stage = 'load wasm';
      log(stage);
      const w = await loadWasm();

      stage = 'generate image secret';
      log(stage);
      const imageSecret = new Uint8Array(32);
      crypto.getRandomValues(imageSecret);

      stage = 'embed image secret';
      log(stage, { carrierBytes: state.carrierImageBytes.byteLength });
      state.referenceImageBytes = new Uint8Array(
        w.embed_image_secret(state.carrierImageBytes, imageSecret),
      );
      log('embedded', { referenceBytes: state.referenceImageBytes.byteLength });

      stage = 'generate salt';
      const salt = new Uint8Array(32);
      crypto.getRandomValues(salt);
      const paramsJson = '{"argon2_m":65536,"argon2_t":3,"argon2_p":4}';

      stage = 'derive session handle';
      log(stage);
      // unlock() takes JPEG bytes with embedded secret (it extracts internally),
      // not the raw 32-byte secret.
      const handle = w.unlock(state.passphrase, state.referenceImageBytes, salt, paramsJson);
      log('handle acquired');

      stage = 'encrypt empty manifest';
      log(stage);
      const manifestJson = '{"schema_version":2,"items":{}}';
      const encryptedManifest = w.manifest_encrypt(handle, manifestJson);
      log('manifest encrypted', { bytes: encryptedManifest.length });

      stage = 'encrypt default settings';
      log(stage);
      const settingsJson = w.default_vault_settings_json();
      const encryptedSettings = w.settings_encrypt(handle, settingsJson);
      log('settings encrypted', { bytes: encryptedSettings.length });

      stage = 'push vault files';
      log(stage);
      const hostUrl = state.hostType === 'github' ? 'https://api.github.com' : state.hostUrl;
      const host = createGitHost(state.hostType, hostUrl, state.repoPath, state.apiToken);

      log('write .relicario/salt');
      await host.writeFileCreateOnly('.relicario/salt', salt, 'init: vault salt');

      log('write .relicario/params.json');
      const paramsBytes = new TextEncoder().encode(paramsJson);
      await host.writeFileCreateOnly('.relicario/params.json', paramsBytes, 'init: KDF parameters');

      log('write manifest.enc');
      await host.writeFileCreateOnly(
        'manifest.enc',
        new Uint8Array(encryptedManifest),
        'init: encrypted manifest',
      );

      log('write settings.enc');
      await host.writeFileCreateOnly(
        'settings.enc',
        new Uint8Array(encryptedSettings),
        'init: encrypted settings',
      );

      stage = 'release handle';
      w.lock(handle);

      log('vault created — advancing to step 4 (device name)');
      state.creating = false;
      state.step = 4;  // device name step
      state.error = null;
      render();
    } catch (err: unknown) {
      // eslint-disable-next-line no-console
      console.error(`[relicario setup] vault creation FAILED during "${stage}":`, err);
      state.creating = false;
      const detail = err instanceof Error ? err.message : String(err);
      if (/already exists/.test(detail)) {
        const path = detail.replace(/^.*?writeFileCreateOnly: /, '').replace(/ already exists$/, '');
        state.error = `A file at ${path} already exists on the remote — refusing to overwrite. Re-run setup; the wizard will offer to attach to the existing vault.`;
      } else {
        state.error = `Vault creation failed at "${stage}": ${detail}`;
      }
      render();
    }
  });
}

// --- Step 4: Device Name ---

function renderStep4(): string {
  const platform = navigator.platform.toLowerCase();
  const isChrome = /chrome/i.test(navigator.userAgent) && !/edg/i.test(navigator.userAgent);
  const isFirefox = /firefox/i.test(navigator.userAgent);
  const browser = isFirefox ? 'Firefox' : isChrome ? 'Chrome' : 'Browser';
  const os = platform.includes('mac') ? 'macOS' : platform.includes('win') ? 'Windows' : 'Linux';
  const defaultName = state.deviceName || `${browser} on ${os}`;

  return `
    <div class="wizard-step glass" style="padding: 24px; margin-top: 16px;">
      <h3>name this device</h3>
      <p class="muted" style="margin-bottom:12px;">
        This helps you identify which devices have access to your vault.
      </p>
      <div class="form-group">
        <label class="label" for="device-name">device name</label>
        <input id="device-name" type="text" value="${escapeHtml(defaultName)}" placeholder="e.g. Chrome on Linux">
      </div>
      <div class="form-actions">
        <button class="btn" id="back-btn">back</button>
        <button class="btn-primary" id="next-btn">continue ${GLYPH_NEXT}</button>
      </div>
    </div>
  `;
}

function attachStep4(): void {
  document.getElementById('back-btn')?.addEventListener('click', () => {
    state.step = 3;
    state.error = null;
    render();
  });

  document.getElementById('next-btn')?.addEventListener('click', async () => {
    const nameInput = document.getElementById('device-name') as HTMLInputElement;
    const name = nameInput.value.trim();
    if (!name) {
      state.error = 'Device name is required';
      render();
      return;
    }

    state.deviceName = name;
    state.step = 5;
    state.error = null;
    detectExtension();
    render();
  });
}

// --- Step 5: Finish ---

function detectExtension(): void {
  try {
    if (typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.sendMessage) {
      // Try to ping the extension
      chrome.runtime.sendMessage({ type: 'is_unlocked' }, (response) => {
        if (chrome.runtime.lastError) {
          state.extensionDetected = false;
        } else {
          state.extensionDetected = true;
        }
        render();
      });
    }
  } catch {
    state.extensionDetected = false;
  }
}

function renderStep5(): string {
  const config: VaultConfig = {
    hostType: state.hostType,
    hostUrl: state.hostType === 'github' ? 'https://api.github.com' : state.hostUrl,
    repoPath: state.repoPath,
    apiToken: state.apiToken,
  };
  const configJson = JSON.stringify(config, null, 2);
  const isAttach = state.mode === 'attach';

  return `
    <div class="wizard-step glass" style="padding: 24px; margin-top: 16px;">
      <div class="success-box">
        <h3>${isAttach ? 'device verified' : 'vault created'}</h3>
        <p class="secondary">
          ${isAttach
            ? 'Your passphrase and reference image decrypt the vault successfully.'
            : 'Your vault has been initialized and pushed to the repository.'}
        </p>
      </div>

      ${isAttach ? '' : `
        <div class="form-group">
          <label class="label">reference image</label>
          <p class="muted" style="margin-bottom:8px;">
            Download and store this image securely. It is your second factor for decryption.
            Without it, you cannot unlock the vault.
          </p>
          <button class="btn btn-primary" id="download-ref-btn">download reference.jpg</button>
        </div>
      `}

      ${state.extensionDetected ? `
        <div class="form-group" style="margin-top:16px;">
          <label class="label">register this device</label>
          <button class="btn btn-primary" id="push-config-btn" ${state.configPushed ? 'disabled' : ''}>
            ${state.configPushed ? 'device registered' : (isAttach ? 'attach this device' : 'register this device')}
          </button>
          ${state.configPushed ? '<span class="test-result pass" style="margin-left:8px;">done</span>' : ''}
        </div>
      ` : `
        <div class="form-group" style="margin-top:16px;">
          <label class="label">extension configuration</label>
          <p class="muted" style="margin-bottom:8px;">
            Copy this JSON and paste it into the extension setup, or save it for later.
          </p>
          <div class="config-blob" id="config-blob">${escapeHtml(configJson)}</div>
          <button class="btn" id="copy-config-btn">copy to clipboard</button>
        </div>
      `}
    </div>
  `;
}

function attachStep5(): void {
  document.getElementById('download-ref-btn')?.addEventListener('click', () => {
    if (!state.referenceImageBytes) return;
    const blob = new Blob([state.referenceImageBytes.buffer as ArrayBuffer], { type: 'image/jpeg' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = 'reference.jpg';
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(url);
  });

  document.getElementById('push-config-btn')?.addEventListener('click', async () => {
    state.error = null;
    render();

    const config: VaultConfig = {
      hostType: state.hostType,
      hostUrl: state.hostType === 'github' ? 'https://api.github.com' : state.hostUrl,
      repoPath: state.repoPath,
      apiToken: state.apiToken,
    };

    try {
      const w = await loadWasm();
      // register_device keeps private keys internal — only public keys returned
      const keypair = w.register_device(state.deviceName);

      // 1) Save device name locally (private keys stay in WASM memory).
      await chrome.storage.local.set({
        device_name: state.deviceName,
      });

      // 2) Save vault config + reference image to extension storage.
      const imageBytes = state.referenceImageBytes ?? state.referenceImageBytesAttach;
      const imageBase64 = imageBytes ? uint8ArrayToBase64(imageBytes) : '';
      const saveOk = await new Promise<boolean>((resolve) => {
        chrome.runtime.sendMessage(
          { type: 'save_setup', config, imageBase64 },
          (response: { ok: boolean; error?: string }) => {
            if (!response?.ok) {
              state.error = response?.error ?? 'Failed to save config to extension';
              resolve(false); return;
            }
            resolve(true);
          },
        );
      });
      if (!saveOk) {
        if (state.verifiedHandle !== null) {
          try { w.lock(state.verifiedHandle); } catch { /* best effort */ }
          state.verifiedHandle = null;
        }
        render();
        return;
      }

      // 3) Register device on the remote (read-modify-write devices.json).
      const hostUrl = state.hostType === 'github' ? 'https://api.github.com' : state.hostUrl;
      const host = createGitHost(state.hostType, hostUrl, state.repoPath, state.apiToken);
      await addDevice(host, {
        name: state.deviceName,
        public_key: keypair.signing_public_key,
        added_at: Math.floor(Date.now() / 1000),
      });

      // 4) Release any attach-mode WASM handle.
      if (state.verifiedHandle !== null) {
        try { w.lock(state.verifiedHandle); } catch { /* best effort */ }
        state.verifiedHandle = null;
      }

      state.configPushed = true;
      render();
    } catch (err: unknown) {
      console.error('[relicario setup] register device failed:', err);
      state.error = `Failed to register device: ${err instanceof Error ? err.message : String(err)}`;
      if (state.verifiedHandle !== null) {
        try { (await loadWasm()).lock(state.verifiedHandle); } catch { /* best effort */ }
        state.verifiedHandle = null;
      }
      render();
    }
  });

  document.getElementById('copy-config-btn')?.addEventListener('click', async () => {
    const blob = document.getElementById('config-blob');
    if (!blob) return;
    try {
      await navigator.clipboard.writeText(blob.textContent ?? '');
      const btn = document.getElementById('copy-config-btn')!;
      btn.textContent = 'copied!';
      setTimeout(() => { btn.textContent = 'copy to clipboard'; }, 2000);
    } catch {
      // Fallback: select the text
      const range = document.createRange();
      range.selectNodeContents(blob);
      const sel = window.getSelection();
      sel?.removeAllRanges();
      sel?.addRange(range);
    }
  });
}

// --- Boot ---

document.addEventListener('DOMContentLoaded', () => {
  render();
});
