// API-key at-rest obfuscation, shared by Options (encrypt) and the background
// worker (decrypt). Consolidates the previously split implementations; the
// wire format is unchanged so keys stored by older builds still decrypt:
//
//   encKey    = base64(32-byte secretbox key)
//   apiKeyEnc = base64(24-byte nonce || secretbox ciphertext)
//
// THREAT MODEL (be honest about it): the secretbox key lives in the same
// chrome.storage.local as the ciphertext, so this protects against casual
// inspection only — anyone who can read the extension's storage (profile
// disk access, another privileged process) can also read the key and
// decrypt. Without a backend or a user passphrase there is no stronger
// at-rest story for a browser extension; UI copy must not over-promise.

import nacl from 'tweetnacl';

const NONCE_LENGTH = 24;

export function bytesToBase64(bytes: Uint8Array): string {
  let bin = '';
  for (const b of bytes) bin += String.fromCharCode(b);
  return btoa(bin);
}

export function base64ToBytes(b64: string): Uint8Array {
  return Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
}

export function generateEncKey(): Uint8Array {
  return nacl.randomBytes(32);
}

// Returns the stored secretbox key, creating and persisting one on first use.
export async function getOrCreateEncKey(): Promise<Uint8Array> {
  return new Promise((resolve) => {
    chrome.storage.local.get(['encKey'], (result) => {
      if (result.encKey) {
        resolve(base64ToBytes(result.encKey as string));
      } else {
        const key = generateEncKey();
        chrome.storage.local.set({ encKey: bytesToBase64(key) });
        resolve(key);
      }
    });
  });
}

// Encrypts a plaintext API key -> base64(nonce || ciphertext).
export function encryptApiKey(plaintext: string, key: Uint8Array): string {
  const nonce = nacl.randomBytes(NONCE_LENGTH);
  // Uint8Array.from re-wraps in the current realm — TextEncoder can hand back a
  // foreign-realm array (e.g. under jsdom) that fails tweetnacl's instanceof check.
  const encoded = Uint8Array.from(new TextEncoder().encode(plaintext));
  const encrypted = nacl.secretbox(encoded, nonce, key);
  const combined = new Uint8Array(nonce.length + encrypted.length);
  combined.set(nonce);
  combined.set(encrypted, nonce.length);
  return bytesToBase64(combined);
}

// Migrates a legacy plaintext `apiKey` left by older builds to the encrypted
// path, then removes the plaintext copy. Safe to run on every worker start —
// no-op when no plaintext key exists. Write order matters: the encrypted copy
// is persisted (and, when one already exists, verified to decrypt) BEFORE the
// plaintext is removed, so an interruption can never lose the only key.
// Returns true when a migration (or plaintext cleanup) happened.
export async function migratePlaintextApiKey(): Promise<boolean> {
  const stored = await new Promise<Record<string, unknown>>((resolve) => {
    chrome.storage.local.get(['apiKey', 'apiKeyEnc', 'encKey'], (result) => resolve(result));
  });
  const plaintext = stored.apiKey as string | undefined;
  if (!plaintext) return false;

  if (stored.apiKeyEnc && stored.encKey) {
    // An encrypted key already exists — drop the stale plaintext copy, but
    // only if the ciphertext actually decrypts (else it stays as fallback).
    if (decryptApiKey(stored.encKey as string, stored.apiKeyEnc as string) === null) return false;
    await new Promise<void>((resolve) => chrome.storage.local.remove('apiKey', resolve));
    return true;
  }

  const key = await getOrCreateEncKey();
  const apiKeyEnc = encryptApiKey(plaintext, key);
  // Re-check for a concurrent Options save: if an encrypted key appeared while
  // we were encrypting, don't clobber it — the next worker start cleans up the
  // plaintext via the verified branch above.
  const recheck = await new Promise<Record<string, unknown>>((resolve) => {
    chrome.storage.local.get(['apiKeyEnc'], (result) => resolve(result));
  });
  if (recheck.apiKeyEnc) return false;
  // Persist the key material together and AWAIT it before removing the
  // plaintext, so loss-safety doesn't depend on Chrome's implicit FIFO write
  // ordering (getOrCreateEncKey's own set() is fire-and-forget).
  await new Promise<void>((resolve) =>
    chrome.storage.local.set({ encKey: bytesToBase64(key), apiKeyEnc }, resolve),
  );
  await new Promise<void>((resolve) => chrome.storage.local.remove('apiKey', resolve));
  return true;
}

// Decrypts base64(nonce || ciphertext) with the base64 key.
// Returns null on any tamper/mismatch (secretbox authentication failure).
export function decryptApiKey(encKeyB64: string, apiKeyEncB64: string): string | null {
  const key = base64ToBytes(encKeyB64);
  const combined = base64ToBytes(apiKeyEncB64);
  const nonce = combined.slice(0, NONCE_LENGTH);
  const cipher = combined.slice(NONCE_LENGTH);
  const decrypted = nacl.secretbox.open(cipher, nonce, key);
  if (!decrypted) return null;
  return new TextDecoder().decode(decrypted);
}
