import { createGitHost } from '../service-worker/git-host';
import { probeVault } from './probe';
import type { VaultProbe } 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 { Request, Response } from '../shared/messages';
import { lookupErrorCopy } from '../shared/error-copy';
import { encodeBinary, decodeBinary } from '../shared/message-binary';

// --- SW messaging ---

export function swSend(msg: Request): Promise<Response> {
  // Binary fields (carrier/reference images) are base64-enveloped for the
  // channel and decoded out of the response — see message-binary.ts.
  return new Promise((resolve) =>
    chrome.runtime.sendMessage(encodeBinary(msg), (r: Response) => resolve(decodeBinary(r))));
}

// --- Step registry types ---

export type StepId = 'mode' | 'host' | 'connection' | 'vault' | 'device' | 'done';

export interface StepContext {
  state: WizardState;
  rerender: () => void;
  goto: (id: StepId) => void;
}

export interface SetupStep {
  id: StepId;
  render: (ctx: StepContext) => string;
  attach: (root: HTMLElement, ctx: StepContext) => () => void;
}

// --- State ---

export interface WizardState {
  stepId: StepId;
  mode: 'new' | 'attach' | null;
  hostType: 'gitea' | 'github';
  hostUrl: string;
  repoPath: string;
  apiToken: string;
  connectionTested: boolean;
  vaultProbe: VaultProbe | null;
  carrierImageBytes: Uint8Array | null;
  referenceImageBytesAttach: Uint8Array | null;
  passphrase: string;
  passphraseConfirm: string;
  passphraseScore: number;
  passphraseGuessesLog10: number;
  passphraseVisible: boolean;
  confirmVisible: boolean;
  referenceImageBytes: Uint8Array | null;
  creating: boolean;
  attaching: boolean;
  error: string | null;
  deviceName: string;
}

export const state: WizardState = {
  stepId: 'mode', 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,
  creating: false, attaching: false, error: null, deviceName: '',
};

// --- State-coupled helpers ---

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;

  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(state.passphraseGuessesLog10);
    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, 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' : '')
      : '';
  }
}

function vaultConfig(): VaultConfig {
  return {
    hostType: state.hostType,
    hostUrl: state.hostType === 'github' ? 'https://api.github.com' : state.hostUrl,
    repoPath: state.repoPath,
    apiToken: state.apiToken,
  };
}

// --- mode ---

const modeStep: SetupStep = {
  id: 'mode',
  render() {
    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">
          <span class="mode-card__icon" style="font-size:28px;">◈</span>
          <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">
          <span class="mode-card__icon" style="font-size:28px;">⌥</span>
          <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>`;
  },
  attach(_root, ctx) {
    document.querySelectorAll('.mode-card').forEach((btn) => {
      btn.addEventListener('click', () => {
        state.mode = (btn as HTMLElement).dataset.mode as 'new' | 'attach';
        ctx.rerender();
      });
    });
    document.getElementById('next-btn')?.addEventListener('click', () => {
      if (state.mode) ctx.goto('host');
    });
    return () => {};
  },
};

// --- host ---

const GITEA_INSTRUCTIONS = `
    <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 GITHUB_INSTRUCTIONS = `
    <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>`;

const hostStep: SetupStep = {
  id: 'host',
  render() {
    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' ? GITEA_INSTRUCTIONS : GITHUB_INSTRUCTIONS}
      <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>`;
  },
  attach(_root, ctx) {
    document.getElementById('back-btn')?.addEventListener('click', () => ctx.goto('mode'));
    document.querySelectorAll('.toggle-group button').forEach((btn) => {
      btn.addEventListener('click', () => {
        state.hostType = (btn as HTMLElement).dataset.host as 'gitea' | 'github';
        state.connectionTested = false;
        ctx.rerender();
      });
    });
    document.getElementById('next-btn')?.addEventListener('click', () => ctx.goto('connection'));
    return () => {};
  },
};

// --- 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>`;
}

const connectionStep: SetupStep = {
  id: 'connection',
  render() {
    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>`;
  },
  attach(_root, ctx) {
    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';
        ctx.rerender();
        return;
      }
      if (state.hostType === 'gitea' && !hostUrl) {
        state.error = 'Host URL is required for Gitea';
        ctx.rerender();
        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)}`;
      }
      ctx.rerender();
    });
    document.getElementById('back-btn')?.addEventListener('click', () => ctx.goto('host'));
    document.getElementById('next-btn')?.addEventListener('click', () => {
      if (state.connectionTested) ctx.goto('vault');
    });
    document.getElementById('switch-mode-btn')?.addEventListener('click', (e) => {
      state.mode = (e.currentTarget as HTMLElement).dataset.target as 'new' | 'attach';
      state.error = null;
      ctx.rerender();
    });
    return () => {};
  },
};

// --- vault ---

function renderVaultAttach(): string {
  const p = state.passphrase;
  const pType = state.passphraseVisible ? 'text' : 'password';
  const pToggle = state.passphraseVisible ? 'hide' : 'show';
  const hasImage = !!state.referenceImageBytesAttach;
  const gateDisabled = !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 when you register 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' : ''}>continue ${GLYPH_NEXT}</button>
      </div>
    </div>`;
}

function renderVaultNew(): string {
  const score = state.passphraseScore;
  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(state.passphraseGuessesLog10);
  const p = state.passphrase, 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' : ''}>continue ${GLYPH_NEXT}</button>
      </div>
    </div>`;
}

const vaultStep: SetupStep = {
  id: 'vault',
  render(ctx) {
    return ctx.state.mode === 'attach' ? renderVaultAttach() : renderVaultNew();
  },
  attach(_root, ctx) {
    return state.mode === 'attach' ? attachVaultAttach(ctx) : attachVaultNew(ctx);
  },
};

function attachVaultAttach(ctx: StepContext): () => 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;
      ctx.rerender();
    };
    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.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', () => ctx.goto('connection'));
  document.getElementById('attach-btn')?.addEventListener('click', () => {
    if (!state.referenceImageBytesAttach) {
      state.error = 'Please select your reference JPEG image';
      ctx.rerender();
      return;
    }
    if (!state.passphrase) {
      state.error = 'Passphrase is required';
      ctx.rerender();
      return;
    }
    ctx.goto('device');
  });
  return () => {};
}

function attachVaultNew(ctx: StepContext): () => 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;
      ctx.rerender();
    };
    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;
    updateStrengthUi();
    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', () => ctx.goto('connection'));
  document.getElementById('create-btn')?.addEventListener('click', async () => {
    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';
      ctx.rerender();
      return;
    }
    if (!state.passphrase) {
      state.error = 'Passphrase is required';
      ctx.rerender();
      return;
    }
    // Re-rate synchronously in case the button was clicked before the debounced
    // rater fired. Defence in depth — the button is already disabled when score < 3.
    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).';
      ctx.rerender();
      return;
    }
    if (state.passphrase !== state.passphraseConfirm) {
      state.error = 'Passphrases do not match';
      ctx.rerender();
      return;
    }
    ctx.goto('device');
  });
  return () => {};
}

// --- device ---

const deviceStep: SetupStep = {
  id: 'device',
  render() {
    const busy = state.creating || state.attaching;
    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}`;
    const busyLabel = state.attaching ? 'attaching…' : 'creating…';
    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" ${busy ? 'disabled' : ''}>
      </div>
      <div class="form-actions">
        <button class="btn" id="back-btn" ${busy ? 'disabled' : ''}>back</button>
        <button class="btn-primary" id="next-btn" ${busy ? 'disabled' : ''}>${busy ? `<span class="spinner"></span> ${busyLabel}` : `continue ${GLYPH_NEXT}`}</button>
      </div>
    </div>`;
  },
  attach(_root, ctx) {
    document.getElementById('back-btn')?.addEventListener('click', () => {
      if (!state.creating && !state.attaching) ctx.goto('vault');
    });
    document.getElementById('next-btn')?.addEventListener('click', async () => {
      if (state.creating || state.attaching) return;
      const name = (document.getElementById('device-name') as HTMLInputElement).value.trim();
      if (!name) {
        state.error = 'Device name is required';
        ctx.rerender();
        return;
      }
      state.deviceName = name;
      state.error = null;
      if (state.mode === 'attach') {
        state.attaching = true;
        ctx.rerender();
        const resp = await swSend({
          type: 'attach_vault',
          config: vaultConfig(),
          passphrase: state.passphrase,
          referenceImageBytes: state.referenceImageBytesAttach!.buffer as ArrayBuffer,
          deviceName: state.deviceName,
        });
        state.attaching = false;
        if (resp.ok) ctx.goto('done');
        else { state.error = lookupErrorCopy(resp.error).body; ctx.rerender(); }
      } else {
        state.creating = true;
        ctx.rerender();
        const resp = await swSend({
          type: 'create_vault',
          config: vaultConfig(),
          passphrase: state.passphrase,
          carrierImageBytes: state.carrierImageBytes!.buffer as ArrayBuffer,
          deviceName: state.deviceName,
        });
        state.creating = false;
        if (resp.ok) {
          const data = resp.data as { referenceImageBytes: Uint8Array };
          state.referenceImageBytes = new Uint8Array(data.referenceImageBytes);
          ctx.goto('done');
        } else { state.error = lookupErrorCopy(resp.error).body; ctx.rerender(); }
      }
    });
    return () => {};
  },
};

// --- done ---

const doneStep: SetupStep = {
  id: 'done',
  render() {
    const isAttach = state.mode === 'attach';
    const qrBannerHtml = isAttach ? '' : `
      <div class="recovery-qr-banner" id="recovery-qr-banner" style="margin-bottom:16px;">
        <div class="recovery-qr-banner__header">
          <span style="font-size:20px;">◫</span>
          <strong>Generate a recovery QR before you go</strong>
        </div>
        <p class="muted" style="font-size:12px;margin:4px 0 8px;">If you lose your reference image, this QR lets you recover your vault. Print it and store it safely.</p>
        <div class="recovery-qr-banner__actions">
          <button class="btn btn-primary" id="setup-gen-qr">Generate now</button>
          <button class="btn" id="setup-skip-qr">Skip — I'll do this in Settings</button>
        </div>
      </div>`;
    const refSection = 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>`;
    return `
    <div class="wizard-step glass" style="padding: 24px; margin-top: 16px;">
      <div class="success-box">
        <h3>${isAttach ? 'device attached' : 'vault created'}</h3>
        <p class="secondary">${isAttach ? 'This device is now attached to your vault.' : 'Your vault has been initialized and pushed to the repository.'}</p>
      </div>
      ${qrBannerHtml}
      ${refSection}
      <div class="form-group" style="margin-top:16px;">
        <label class="label">extension configuration</label>
        <p class="muted" style="margin-bottom:8px;">
          Copy this JSON to configure Relicario on another setup, or save it for later.
        </p>
        <div class="config-blob" id="config-blob">${escapeHtml(JSON.stringify(vaultConfig(), null, 2))}</div>
        <button class="btn" id="copy-config-btn">copy to clipboard</button>
      </div>
      <div class="form-actions" style="margin-top:16px;">
        <button class="btn btn-primary" id="open-vault-btn">open vault</button>
      </div>
    </div>`;
  },
  attach(_root, _ctx) {
    document.getElementById('setup-gen-qr')?.addEventListener('click', async () => {
      const btn = document.getElementById('setup-gen-qr') as HTMLButtonElement | null;
      if (btn) { btn.disabled = true; btn.textContent = 'Generating…'; }
      try {
        const resp = await swSend({ type: 'generate_recovery_qr', passphrase: state.passphrase });
        if (!resp.ok || !resp.data) throw new Error(resp.ok ? 'unknown error' : resp.error);
        const svg = (resp.data as { svg: string }).svg;
        await new Promise<void>((resolve) => {
          chrome.storage.local.set({ recovery_qr_generated_at: Date.now() }, resolve);
        });
        const banner = document.getElementById('recovery-qr-banner');
        if (banner) {
          banner.innerHTML = `
            <div style="text-align:center;">${svg}</div>
            <p style="font-size:12px;color:var(--success,#238636);margin:8px 0 0;">◉ Recovery QR generated — save or print this now.</p>
            <div style="margin-top:8px;"><button class="btn" id="setup-qr-done">Done</button></div>`;
          document.getElementById('setup-qr-done')?.addEventListener('click', () => {
            banner.style.display = 'none';
          });
        }
      } catch (err) {
        if (btn) { btn.disabled = false; btn.textContent = 'Generate now'; }
        alert(`Failed to generate QR: ${err instanceof Error ? err.message : String(err)}`);
      }
    });
    document.getElementById('setup-skip-qr')?.addEventListener('click', () => {
      const banner = document.getElementById('recovery-qr-banner');
      if (banner) banner.style.display = 'none';
    });
    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('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 {
        const range = document.createRange();
        range.selectNodeContents(blob);
        const sel = window.getSelection();
        sel?.removeAllRanges();
        sel?.addRange(range);
      }
    });
    document.getElementById('open-vault-btn')?.addEventListener('click', () => void finishSetup());
    return () => {};
  },
};

// --- Registry ---

export const STEPS: ReadonlyArray<SetupStep> = [
  modeStep, hostStep, connectionStep, vaultStep, deviceStep, doneStep,
];

// --- Sensitive-state cleanup ---

export function clearWizardState(): void {
  // Best-effort wipe — JS strings are GC-only (see spec Risks); zero-fill the Uint8Arrays.
  state.carrierImageBytes?.fill(0);
  state.referenceImageBytes?.fill(0);
  state.referenceImageBytesAttach?.fill(0);
  state.mode = null;
  state.hostType = 'gitea';
  state.hostUrl = '';
  state.repoPath = '';
  state.apiToken = '';
  state.connectionTested = false;
  state.vaultProbe = null;
  state.carrierImageBytes = null;
  state.referenceImageBytesAttach = null;
  state.passphrase = '';
  state.passphraseConfirm = '';
  state.passphraseScore = -1;
  state.passphraseGuessesLog10 = -1;
  state.passphraseVisible = false;
  state.confirmVisible = false;
  state.referenceImageBytes = null;
  state.creating = false;
  state.attaching = false;
  state.error = null;
  state.deviceName = '';
}

// --- Completion handoff ---

/// Open the fullscreen vault tab and best-effort close the setup tab.
export async function finishSetup(): Promise<void> {
  const vaultUrl = chrome.runtime.getURL('vault.html');
  await chrome.tabs.create({ url: vaultUrl });
  try {
    const current = await chrome.tabs.getCurrent();
    if (current?.id !== undefined) {
      await chrome.tabs.remove(current.id);
    }
  } catch {
    // Setup tab may not be closeable (e.g., opened as popup rather than a tab).
    // The vault tab is open — that's the user-visible success.
  }
}
