/* ═══════════════════════════════════════════════════════════════════════════
   WCAHT Wallet – Background Service Worker (Manifest V3)
   ═══════════════════════════════════════════════════════════════════════════
   Manages wallet state, handles signing requests from dApps,
   and coordinates between popup ↔ content script.
*/

// MV3 loads dependencies into the worker. Firefox MV2 lists the same files as
// background scripts because its background page does not expose importScripts.
if (typeof importScripts === 'function') {
  importScripts('../lib/nacl-fast.min.js');
  importScripts('../lib/wcaht-evm.min.js');
  importScripts('../lib/ethers.umd.min.js');
}

// ── Constants ───────────────────────────────────────────────────────────────
// Wallet talks to the chain over HTTPS via the dashboard's /node reverse-proxy
// (Cloudflare TLS) rather than a raw http://IP:8901 node — a secure
// chrome-extension:// context blocks plain-http (mixed content), and the proxy
// forwards to the freshest validator so one node down != wallet offline.
const DEFAULT_NETWORK = { name: 'WCAHT Mainnet', rpcUrl: 'https://dashboard.denvion.com/node' };
const DASHBOARD_ORIGIN = 'https://dashboard.denvion.com';
const KAK_PER_WCAHT = 1_000_000_000; // 1 WCAHT = 1e9 kak
const NATIVE_FEE_KAK = 5000;
const EVM_CHAIN_ID = 7789;
const WEI_PER_KAK = 1_000_000_000n;
const EVM_BILLED_GAS = 21_000n;

// ── State ───────────────────────────────────────────────────────────────────
let state = {
  wallets: [],        // [{id, name, publicKey, scheme, evmAddress, encryptedKey}]
  activeWalletId: null,
  network: DEFAULT_NETWORK,
  connectedSites: {}, // origin -> { publicKey, addressType, evmAddress, autoApprove }
  locked: true,
  passwordHash: null,
  authMethod: 'password',      // 'password' | 'google' | 'webauthn'
  googleSalt: null,            // random hex salt for Google key derivation
  googleIdHash: null,          // SHA-256 of Google user ID (to detect account mismatch)
  webauthnCredId: null,        // base64 credential ID from WebAuthn registration
  webauthnVaultPassword: null, // random hex vault key for WebAuthn auth
  pinEncryptedPassword: null,  // vault password encrypted with PIN (for PIN unlock)
  primaryAuthMethod: 'password', // 'password' | 'pin' — what the vault key IS
};

// ── Base58 ──────────────────────────────────────────────────────────────────
const B58 = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';

function b58encode(bytes) {
  let num = BigInt(0);
  for (const b of bytes) num = num * 256n + BigInt(b);
  let str = '';
  while (num > 0n) { str = B58[Number(num % 58n)] + str; num /= 58n; }
  for (const b of bytes) { if (b === 0) str = '1' + str; else break; }
  return str || '1';
}

function b58decode(str) {
  let num = BigInt(0);
  for (const c of str) {
    const i = B58.indexOf(c);
    if (i < 0) throw new Error('Invalid base58 char: ' + c);
    num = num * 58n + BigInt(i);
  }
  const hex = num.toString(16);
  const padded = hex.length % 2 ? '0' + hex : hex;
  const bytes = [];
  for (let i = 0; i < padded.length; i += 2) bytes.push(parseInt(padded.substr(i, 2), 16));
  for (const c of str) { if (c === '1') bytes.unshift(0); else break; }
  return new Uint8Array(bytes);
}

function hexToBytes(hex) {
  const bytes = new Uint8Array(hex.length / 2);
  for (let i = 0; i < hex.length; i += 2) bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
  return bytes;
}

function bytesToHex(bytes) {
  return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
}

// ── Little-endian encoders ──────────────────────────────────────────────────
function u32le(n) {
  const buf = new ArrayBuffer(4);
  new DataView(buf).setUint32(0, n, true);
  return new Uint8Array(buf);
}

function u64le(n) {
  const buf = new ArrayBuffer(8);
  const view = new DataView(buf);
  view.setUint32(0, n & 0xFFFFFFFF, true);
  view.setUint32(4, Math.floor(n / 0x100000000) & 0xFFFFFFFF, true);
  return new Uint8Array(buf);
}

function u128le(n) {
  const big = BigInt(n);
  const bytes = new Uint8Array(16);
  let v = big;
  for (let i = 0; i < 16; i++) { bytes[i] = Number(v & 0xFFn); v >>= 8n; }
  return bytes;
}

// ── Encryption (AES-GCM via SubtleCrypto) ───────────────────────────────────
async function deriveKey(password) {
  const enc = new TextEncoder();
  const keyMaterial = await crypto.subtle.importKey(
    'raw', enc.encode(password), 'PBKDF2', false, ['deriveKey']
  );
  return crypto.subtle.deriveKey(
    { name: 'PBKDF2', salt: enc.encode('wcaht-wallet-salt'), iterations: 100000, hash: 'SHA-256' },
    keyMaterial,
    { name: 'AES-GCM', length: 256 },
    false,
    ['encrypt', 'decrypt']
  );
}

async function encrypt(password, plaintext) {
  const key = await deriveKey(password);
  const iv = crypto.getRandomValues(new Uint8Array(12));
  const enc = new TextEncoder();
  const ct = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, enc.encode(plaintext));
  return { iv: bytesToHex(iv), ct: bytesToHex(new Uint8Array(ct)) };
}

async function decrypt(password, encrypted) {
  const key = await deriveKey(password);
  const iv = hexToBytes(encrypted.iv);
  const ct = hexToBytes(encrypted.ct);
  const pt = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, ct);
  return new TextDecoder().decode(pt);
}

// ── Password hashing (for lock/unlock check) ───────────────────────────────
async function hashPassword(password) {
  const enc = new TextEncoder();
  const hash = await crypto.subtle.digest('SHA-256', enc.encode('wcaht-pw-check:' + password));
  return bytesToHex(new Uint8Array(hash));
}

// ── Google Auth helpers ──────────────────────────────────────────────────────
async function sha256hex(text) {
  const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(text));
  return bytesToHex(new Uint8Array(buf));
}

async function deriveGooglePassword(googleId, salt) {
  const enc = new TextEncoder();
  const km = await crypto.subtle.importKey('raw', enc.encode(googleId), 'PBKDF2', false, ['deriveKey']);
  const aesKey = await crypto.subtle.deriveKey(
    { name: 'PBKDF2', salt: enc.encode('wcaht-google-' + salt), iterations: 200000, hash: 'SHA-256' },
    km, { name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']
  );
  const raw = await crypto.subtle.exportKey('raw', aesKey);
  return bytesToHex(new Uint8Array(raw));
}

// ── Storage helpers ─────────────────────────────────────────────────────────
async function saveState() {
  await chrome.storage.local.set({
    wallets: state.wallets,
    activeWalletId: state.activeWalletId,
    network: state.network,
    connectedSites: state.connectedSites,
    passwordHash: state.passwordHash,
    authMethod: state.authMethod,
    googleSalt: state.googleSalt,
    googleIdHash: state.googleIdHash,
    webauthnCredId: state.webauthnCredId,
    webauthnVaultPassword: state.webauthnVaultPassword,
    pinEncryptedPassword: state.pinEncryptedPassword,
    primaryAuthMethod: state.primaryAuthMethod,
  });
}

async function loadState() {
  const data = await chrome.storage.local.get([
    'wallets', 'activeWalletId', 'network', 'connectedSites', 'passwordHash',
    'authMethod', 'googleSalt', 'googleIdHash', 'webauthnCredId', 'webauthnVaultPassword',
    'pinEncryptedPassword', 'primaryAuthMethod',
  ]);
  state.wallets = data.wallets || [];
  state.activeWalletId = data.activeWalletId || null;
  state.network = data.network || DEFAULT_NETWORK;
  // Auto-heal stored networks that point at a decommissioned or plain-http node
  // (localhost, the old N5 IP, or the retired sslip.io host). These caused the
  // "Node offline" state on existing installs; migrate them to the HTTPS proxy.
  const staleRpc = /(localhost|127\.0\.0\.1|139\.99\.150\.23|51\.79\.176\.134|52-206-203-33\.sslip\.io|:8901)/;
  if (state.network && (staleRpc.test(state.network.rpcUrl || '') || /^http:\/\//.test(state.network.rpcUrl || ''))) {
    state.network = DEFAULT_NETWORK;
    chrome.storage.local.set({ network: state.network });
  }
  state.connectedSites = data.connectedSites || {};
  state.passwordHash = data.passwordHash || null;
  state.authMethod = data.authMethod || 'password';
  state.googleSalt = data.googleSalt || null;
  state.googleIdHash = data.googleIdHash || null;
  state.webauthnCredId = data.webauthnCredId || null;
  state.webauthnVaultPassword = data.webauthnVaultPassword || null;
  state.pinEncryptedPassword = data.pinEncryptedPassword || null;
  state.primaryAuthMethod = data.primaryAuthMethod || 'password';
  state.locked = !!state.passwordHash; // locked if password was set
}

// ── In-memory password cache (cleared on lock) ──────────────────────────────
let _cachedPassword = null;

// ── TX Building ─────────────────────────────────────────────────────────────
async function blake3hash(msg) {
  // Use node API for blake3
  try {
    const resp = await fetch(DASHBOARD_ORIGIN + '/api/wallet/blake3', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ data: Array.from(msg) }),
    });
    if (resp.ok) {
      const j = await resp.json();
      return j.hash;
    }
  } catch {}
  // Fallback to SHA-256
  const hashBuf = await crypto.subtle.digest('SHA-256', msg);
  return bytesToHex(new Uint8Array(hashBuf));
}

async function buildAndSignTx(wallet, password, toAddress, amountKak) {
  // Decrypt secret key
  const secretB58 = await decrypt(password, wallet.encryptedKey);
  const secretBytes = b58decode(secretB58);

  // Get recent blockhash
  const bhResp = await fetch(state.network.rpcUrl + '/blockchain/recent_blockhash');
  if (!bhResp.ok) throw new Error('Failed to get recent blockhash');
  const bhData = await bhResp.json();
  const bhBytes = hexToBytes(bhData.recent_blockhash);
  const lastValidSlot = bhData.last_valid_slot;

  // Build TX::v2 message
  const ts = Date.now();
  const fromStr = new TextEncoder().encode(wallet.publicKey);
  const toStr = new TextEncoder().encode(toAddress);
  const FEE = 5000;
  const COMPUTE = 1000;

  const parts = [
    new TextEncoder().encode('TX::v2|'),
    bhBytes,
    new TextEncoder().encode('|'),
    u32le(fromStr.length), fromStr,
    u32le(toStr.length), toStr,
    u128le(amountKak),
    u64le(FEE),
    u64le(COMPUTE),
    u32le(0), u32le(0), u32le(0), // read/write/kv empty
    u64le(0),            // priority_fee
    u64le(lastValidSlot),
    u64le(ts),
    new Uint8Array([0, 0, 0, 0]), // flags
  ];

  const totalLen = parts.reduce((s, p) => s + p.length, 0);
  const msg = new Uint8Array(totalLen);
  let off = 0;
  for (const p of parts) { msg.set(p, off); off += p.length; }

  // Sign. New universal wallets use secp256k1 so the same key has a native
  // base58 form plus an EVM 0x address. Legacy wallets continue to use Ed25519.
  let sig;
  if (wallet.scheme === 'secp256k1') {
    if (secretBytes.length !== 32) throw new Error('Invalid secp256k1 key length');
    sig = self.WCAHT_EVM.signDigest64(secretBytes, self.WCAHT_EVM.keccakBytes(msg));
  } else {
    let kp;
    if (secretBytes.length === 64) kp = self.nacl.sign.keyPair.fromSecretKey(secretBytes);
    else kp = self.nacl.sign.keyPair.fromSeed(secretBytes);
    sig = self.nacl.sign.detached(msg, kp.secretKey);
  }
  const signature = b58encode(sig);
  const txHash = await blake3hash(msg);

  return {
    recent_blockhash: bhData.recent_blockhash,
    last_valid_slot: lastValidSlot,
    from: wallet.publicKey,
    to: toAddress,
    amount: amountKak,
    compute_units: COMPUTE,
    fee: FEE,
    signature,
    public_key: wallet.publicKey,
    timestamp: ts,
    tx_hash: txHash,
    transaction_type: 'Transfer',
    processor: null,
    wasm: null,
    priority_fee: 0,
    idempotency_key: null,
    origin: 'Local',
    read_set: [],
    write_set: [],
    kv_access_list: [],
  };
}

async function submitTx(tx) {
  const resp = await fetch(state.network.rpcUrl + '/rpc', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'sendTransaction', params: [JSON.stringify(tx)] }),
  });
  const payload = await resp.json();
  if (!resp.ok || payload.error) {
    throw new Error(payload.error?.message || `Native transaction failed with HTTP ${resp.status}`);
  }
  return payload.result;
}

async function fetchKakBalance(address) {
  if (!address) return 0;
  const resp = await fetch(state.network.rpcUrl + '/balance/' + encodeURIComponent(address), { cache: 'no-store' });
  const text = await resp.text();
  if (!resp.ok) throw new Error(`Balance request failed with HTTP ${resp.status}`);
  try {
    const parsed = JSON.parse(text);
    const value = typeof parsed === 'number' ? parsed : parsed.balance;
    return Number.isFinite(Number(value)) ? Number(value) : 0;
  } catch {
    const value = Number(text.trim());
    return Number.isFinite(value) ? value : 0;
  }
}

async function getBalanceParts(wallet = getActiveWallet()) {
  if (!wallet) return { native: 0, evm: 0, total: 0 };
  const [native, evm] = await Promise.all([
    fetchKakBalance(wallet.publicKey),
    wallet.evmAddress ? fetchKakBalance(wallet.evmAddress) : Promise.resolve(0),
  ]);
  return { native, evm, total: native + evm };
}

async function evmRpc(method, params = []) {
  const resp = await fetch(state.network.rpcUrl + '/eth', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ jsonrpc: '2.0', id: Date.now(), method, params }),
    cache: 'no-store',
  });
  const payload = await resp.json();
  if (!resp.ok || payload.error) {
    throw new Error(payload.error?.message || `EVM RPC ${method} failed with HTTP ${resp.status}`);
  }
  return payload.result;
}

function evmQuantity(value) {
  return '0x' + BigInt(value).toString(16);
}

function isEvmAddress(address) {
  return /^0x[0-9a-fA-F]{40}$/.test(String(address || '').trim());
}

async function prepareEvmTransfer(wallet, recipient, amountKak) {
  if (wallet.scheme !== 'secp256k1' || !wallet.evmAddress) {
    throw new Error('This legacy wallet has no paired EVM account');
  }
  let target = String(recipient || '').trim();
  let data = '0x';
  if (!isEvmAddress(target)) {
    const bridge = await evmRpc('wcaht_encodeNativeTransfer', [target]);
    target = bridge.to;
    data = bridge.data;
  }
  const valueWei = BigInt(amountKak) * WEI_PER_KAK;
  const estimate = { from: wallet.evmAddress, to: target, value: evmQuantity(valueWei), data };
  const [nonceHex, gasPriceHex, gasLimitHex] = await Promise.all([
    evmRpc('eth_getTransactionCount', [wallet.evmAddress, 'pending']),
    evmRpc('eth_gasPrice'),
    evmRpc('eth_estimateGas', [estimate]),
  ]);
  const gasPrice = BigInt(gasPriceHex);
  const feeWei = gasPrice * EVM_BILLED_GAS;
  return {
    target,
    data,
    valueWei,
    nonce: Number(BigInt(nonceHex)),
    gasPrice,
    gasLimit: BigInt(gasLimitHex),
    feeKak: Number((feeWei + WEI_PER_KAK - 1n) / WEI_PER_KAK),
  };
}

async function buildAndSendEvmTx(wallet, password, recipient, amountKak, prepared) {
  const ethersApi = self.ethers?.Wallet ? self.ethers : self.ethers?.ethers;
  if (!ethersApi?.Wallet) throw new Error('EVM signing library is unavailable');
  const secretBytes = b58decode(await decrypt(password, wallet.encryptedKey));
  if (secretBytes.length !== 32) throw new Error('Invalid secp256k1 key length');
  const signer = new ethersApi.Wallet('0x' + bytesToHex(secretBytes));
  if (signer.address.toLowerCase() !== wallet.evmAddress.toLowerCase()) {
    throw new Error('Private key does not match the selected EVM account');
  }
  const tx = prepared || await prepareEvmTransfer(wallet, recipient, amountKak);
  const rawTransaction = await signer.signTransaction({
    type: 0,
    chainId: EVM_CHAIN_ID,
    nonce: tx.nonce,
    gasPrice: tx.gasPrice,
    gasLimit: tx.gasLimit,
    to: tx.target,
    value: tx.valueWei,
    data: tx.data,
  });
  return evmRpc('eth_sendRawTransaction', [rawTransaction]);
}

// ── Message Handler ─────────────────────────────────────────────────────────
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
  handleMessage(msg, sender).then(sendResponse).catch(e => sendResponse({ error: e.message }));
  return true; // async
});

async function handleMessage(msg, sender) {
  await stateReady;
  switch (msg.type) {
    // ── Wallet Lifecycle ──────────────────────────────────────────────────
    case 'GET_STATE':
      return {
        locked: state.locked,
        hasWallet: state.wallets.length > 0,
        activeWallet: state.locked ? null : walletSummary(getActiveWallet()),
        network: state.network,
        walletCount: state.wallets.length,
        authMethod: state.authMethod,
        webauthnCredId: state.webauthnCredId,
        pinEncryptedPassword: !!state.pinEncryptedPassword, // just a flag, not the value
        primaryAuthMethod: state.primaryAuthMethod,
      };

    case 'SETUP_PASSWORD': {
      state.passwordHash = await hashPassword(msg.password);
      _cachedPassword = msg.password;
      state.locked = false;
      // Track whether the vault key itself is a PIN
      state.primaryAuthMethod = msg.password.startsWith('__PIN__') ? 'pin' : 'password';
      await saveState();
      void refreshActionBadge();
      return { ok: true };
    }

    case 'CHANGE_VAULT_PASSWORD': {
      // Re-encrypt all wallet keys from old vault password to new password.
      // Used when switching from PIN-as-primary to password.
      if (state.locked || !_cachedPassword) return { error: 'Wallet is locked. Unlock first.' };
      const newPw = msg.newPassword;
      if (!newPw || newPw.length < 6) return { error: 'Password must be at least 6 characters' };
      try {
        const reencrypted = [];
        for (const w of state.wallets) {
          const plainKey = await decrypt(_cachedPassword, w.encryptedKey);
          const newEncKey = await encrypt(newPw, plainKey);
          reencrypted.push({ ...w, encryptedKey: newEncKey });
        }
        state.wallets = reencrypted;
      } catch {
        return { error: 'Failed to re-encrypt wallets. Wrong current vault key.' };
      }
      state.passwordHash = await hashPassword(newPw);
      _cachedPassword = newPw;
      state.pinEncryptedPassword = null;
      state.primaryAuthMethod = 'password';
      await saveState();
      return { ok: true };
    }

    case 'UNLOCK': {
      const hash = await hashPassword(msg.password);
      if (hash !== state.passwordHash) return { error: 'Wrong password' };
      state.locked = false;
      _cachedPassword = msg.password;
      void refreshActionBadge();
      return { ok: true };
    }

    case 'LOCK':
      state.locked = true;
      _cachedPassword = null;
      void refreshActionBadge();
      return { ok: true };

    case 'SET_PIN': {
      // Encrypt the cached vault password with the PIN so user can unlock later without typing password
      if (state.locked) return { error: 'Wallet is locked. Unlock first.' };
      if (!_cachedPassword) return { error: 'Session expired. Re-unlock first.' };
      const verifyHash = await hashPassword(msg.password);
      if (verifyHash !== state.passwordHash) return { error: 'Wrong password' };
      state.pinEncryptedPassword = await encrypt('__PIN_KEY__' + msg.pin, _cachedPassword);
      await saveState();
      return { ok: true };
    }

    case 'REMOVE_PIN': {
      state.pinEncryptedPassword = null;
      await saveState();
      return { ok: true };
    }

    case 'UNLOCK_WITH_PIN': {
      if (!state.pinEncryptedPassword) return { error: 'PIN not set up' };
      let vaultPw;
      try {
        vaultPw = await decrypt('__PIN_KEY__' + msg.pin, state.pinEncryptedPassword);
      } catch {
        return { error: 'Incorrect PIN' };
      }
      const hash = await hashPassword(vaultPw);
      if (hash !== state.passwordHash) return { error: 'Incorrect PIN' };
      state.locked = false;
      _cachedPassword = vaultPw;
      void refreshActionBadge();
      return { ok: true };
    }

    case 'RESET_VAULT':
      // Wipe all wallet data and password; leaves node/network config intact
      state.wallets = [];
      state.activeWalletId = null;
      state.passwordHash = null;
      state.authMethod = 'password';
      state.googleSalt = null;
      state.googleIdHash = null;
      state.webauthnCredId = null;
      state.webauthnVaultPassword = null;
      state.locked = true;
      _cachedPassword = null;
      await saveState();
      void refreshActionBadge();
      return { ok: true };

    // ── Google Authentication ────────────────────────────────────────────────────────────
    case 'GOOGLE_AUTH_SETUP': {
      // Derives a stable vault key from the user's Google ID + a local random salt.
      // Requires "identity" permission and a valid oauth2.client_id in manifest.json.
      if (!chrome.identity || typeof chrome.identity.getAuthToken !== 'function') {
        return { error: 'Google Sign-In is only supported in Chrome and Edge' };
      }
      try {
        const token = await new Promise((res, rej) =>
          chrome.identity.getAuthToken({ interactive: true }, t =>
            chrome.runtime.lastError ? rej(new Error(chrome.runtime.lastError.message)) : res(t)));
        const profile = await fetch('https://www.googleapis.com/oauth2/v2/userinfo',
          { headers: { Authorization: `Bearer ${token}` } }).then(r => r.json());
        if (!profile.id) return { error: 'Could not read Google profile' };

        const salt = bytesToHex(crypto.getRandomValues(new Uint8Array(16)));
        const vaultPassword = await deriveGooglePassword(profile.id, salt);

        state.authMethod = 'google';
        state.googleSalt = salt;
        state.googleIdHash = await sha256hex(profile.id);
        state.passwordHash = await hashPassword(vaultPassword);
        state.locked = false;
        _cachedPassword = vaultPassword;
        await saveState();
        return { ok: true, email: profile.email };
      } catch (e) {
        return { error: e.message || 'Google auth failed' };
      }
    }

    case 'GOOGLE_AUTH_UNLOCK': {
      if (!chrome.identity || typeof chrome.identity.getAuthToken !== 'function') {
        return { error: 'Google Sign-In is only supported in Chrome and Edge' };
      }
      try {
        const token = await new Promise((res, rej) =>
          chrome.identity.getAuthToken({ interactive: true }, t =>
            chrome.runtime.lastError ? rej(new Error(chrome.runtime.lastError.message)) : res(t)));
        const profile = await fetch('https://www.googleapis.com/oauth2/v2/userinfo',
          { headers: { Authorization: `Bearer ${token}` } }).then(r => r.json());
        if (!profile.id) return { error: 'Could not read Google profile' };

        const idHash = await sha256hex(profile.id);
        if (idHash !== state.googleIdHash) return { error: 'Wrong Google account — use the account you registered with' };

        const vaultPassword = await deriveGooglePassword(profile.id, state.googleSalt);
        const hash = await hashPassword(vaultPassword);
        if (hash !== state.passwordHash) return { error: 'Google auth mismatch — vault may be corrupt' };

        state.locked = false;
        _cachedPassword = vaultPassword;
        return { ok: true };
      } catch (e) {
        return { error: e.message || 'Google auth failed' };
      }
    }

    // ── Windows Hello / WebAuthn ──────────────────────────────────────────────────
    case 'WEBAUTHN_SETUP': {
      // Popup has already done navigator.credentials.create() and passes us the credential ID.
      // We generate a random vault password and protect it behind WebAuthn presence check.
      const waPass = bytesToHex(crypto.getRandomValues(new Uint8Array(32)));
      state.authMethod = 'webauthn';
      state.webauthnCredId = msg.credentialId;       // base64 string from popup
      state.webauthnVaultPassword = waPass;
      state.passwordHash = await hashPassword(waPass);
      state.locked = false;
      _cachedPassword = waPass;
      await saveState();
      return { ok: true };
    }

    case 'WEBAUTHN_UNLOCK': {
      // Popup has already done navigator.credentials.get() successfully.
      // We trust the WebAuthn assertion succeeded and return the vault password.
      if (!state.webauthnVaultPassword) return { error: 'No Windows Hello vault configured' };
      const waHash = await hashPassword(state.webauthnVaultPassword);
      if (waHash !== state.passwordHash) return { error: 'Vault integrity check failed' };
      state.locked = false;
      _cachedPassword = state.webauthnVaultPassword;
      void refreshActionBadge();
      return { ok: true };
    }

    // ── Wallet CRUD ───────────────────────────────────────────────────────
    case 'CREATE_WALLET': {
      if (state.locked) return { error: 'Wallet is locked' };
      const acct = self.WCAHT_EVM.generateAccount();
      const pubB58 = b58encode(acct.publicKeyCompressed);
      const secB58 = b58encode(hexToBytes(acct.privateKeyHex));
      const enc = await encrypt(_cachedPassword, secB58);
      const wallet = {
        id: crypto.randomUUID(),
        name: msg.name || 'Wallet ' + (state.wallets.length + 1),
        publicKey: pubB58,
        scheme: 'secp256k1',
        evmAddress: acct.evmAddress,
        encryptedKey: enc,
        createdAt: Date.now(),
        backupRequired: true,
      };
      state.wallets.push(wallet);
      state.activeWalletId = wallet.id;
      await saveState();
      void refreshActionBadge();
      return { ok: true, walletId: wallet.id, publicKey: pubB58, scheme: wallet.scheme, evmAddress: wallet.evmAddress, backupRequired: true };
    }

    case 'IMPORT_WALLET': {
      if (state.locked) return { error: 'Wallet is locked' };
      let kp;
      try { kp = resolveWalletKey(msg.key, msg.scheme); } catch (e) { return { error: e.message }; }
      const pubB58 = b58encode(kp.publicKey);
      const secB58 = b58encode(kp.secretKey);
      // If it already exists, make it active and let the popup attach it to
      // the current dashboard profile.
      const existing = state.wallets.find(w => w.publicKey === pubB58);
      if (existing) {
        state.activeWalletId = existing.id;
        await saveState();
        void refreshActionBadge();
        return { ok: true, publicKey: pubB58, scheme: existing.scheme || 'ed25519', evmAddress: existing.evmAddress || null, existing: true };
      }
      const enc = await encrypt(_cachedPassword, secB58);
      const wallet = {
        id: crypto.randomUUID(),
        name: msg.name || 'Imported Wallet',
        publicKey: pubB58,
        scheme: kp.scheme || 'ed25519',
        evmAddress: kp.evmAddress || null,
        encryptedKey: enc,
        createdAt: Date.now(),
        backupRequired: false,
      };
      state.wallets.push(wallet);
      state.activeWalletId = wallet.id;
      await saveState();
      void refreshActionBadge();
      return { ok: true, walletId: wallet.id, publicKey: pubB58, scheme: wallet.scheme, evmAddress: wallet.evmAddress, backupRequired: false };
    }

    case 'DELETE_WALLET': {
      if (state.locked) return { error: 'Wallet is locked' };
      state.wallets = state.wallets.filter(w => w.id !== msg.walletId);
      if (state.activeWalletId === msg.walletId) {
        state.activeWalletId = state.wallets[0]?.id || null;
      }
      await saveState();
      void refreshActionBadge();
      return { ok: true };
    }

    case 'SELECT_WALLET': {
      state.activeWalletId = msg.walletId;
      await saveState();
      void refreshActionBadge();
      return { ok: true };
    }

    case 'GET_WALLETS': {
      if (state.locked) return { error: 'Wallet is locked' };
      return { wallets: state.wallets.map(w => ({
        id: w.id,
        name: w.name,
        publicKey: w.publicKey,
        scheme: w.scheme || 'ed25519',
        evmAddress: w.evmAddress || null,
        backupRequired: !!w.backupRequired,
      })) };
    }

    case 'RENAME_WALLET': {
      if (state.locked) return { error: 'Wallet is locked' };
      const w = state.wallets.find(w => w.id === msg.walletId);
      if (w) { w.name = msg.name; await saveState(); }
      return { ok: true };
    }

    // ── Balance & Network ─────────────────────────────────────────────────
    case 'GET_BALANCE': {
      const addr = msg.address || getActiveWallet()?.publicKey;
      if (!addr) return { balance: 0 };
      try {
        return { balance: await fetchKakBalance(addr) };
      } catch (e) { return { error: 'Node offline: ' + e.message }; }
    }

    case 'GET_BALANCE_PARTS': {
      try {
        return await getBalanceParts();
      } catch (e) {
        return { error: 'Node offline: ' + e.message };
      }
    }

    case 'GET_MAX_SEND': {
      const wallet = getActiveWallet();
      if (!wallet) return { error: 'No active wallet' };
      try {
        const balances = await getBalanceParts(wallet);
        const nativeMax = Math.max(0, balances.native - NATIVE_FEE_KAK);
        let evmMax = 0;
        let evmFee = 0;
        if (wallet.evmAddress) {
          const recipient = msg.to || '0x000000000000000000000000000000000000dead';
          const prepared = await prepareEvmTransfer(wallet, recipient, 0);
          evmFee = prepared.feeKak;
          evmMax = Math.max(0, balances.evm - evmFee);
        }
        const fundingSource = evmMax > nativeMax ? 'EVM' : 'Native';
        return {
          amount: fundingSource === 'EVM' ? evmMax : nativeMax,
          fee: fundingSource === 'EVM' ? evmFee : NATIVE_FEE_KAK,
          fundingSource,
          balances,
        };
      } catch (e) {
        return { error: e.message };
      }
    }

    case 'GET_STATUS': {
      try {
        const resp = await fetch(state.network.rpcUrl + '/status');
        return await resp.json();
      } catch (e) { return { error: 'Node offline' }; }
    }

    case 'SET_NETWORK': {
      state.network = { name: msg.name, rpcUrl: msg.rpcUrl };
      await saveState();
      void refreshActionBadge();
      return { ok: true };
    }

    // ── Send Transaction ──────────────────────────────────────────────────
    case 'SEND_TX': {
      if (state.locked) return { error: 'Wallet is locked' };
      const wallet = getActiveWallet();
      if (!wallet) return { error: 'No active wallet' };
      try {
        const amountKak = Number(msg.amount);
        if (!Number.isSafeInteger(amountKak) || amountKak <= 0) throw new Error('Invalid transaction amount');
        const balances = await getBalanceParts(wallet);
        if (balances.native >= amountKak + NATIVE_FEE_KAK) {
          const tx = await buildAndSignTx(wallet, _cachedPassword, msg.to, amountKak);
          const result = await submitTx(tx);
          void refreshActionBadge();
          return { ok: true, txHash: tx.tx_hash, result, fundingSource: 'Native', fee: NATIVE_FEE_KAK };
        }
        if (!wallet.evmAddress) {
          throw new Error(`Insufficient native balance. Need ${(amountKak + NATIVE_FEE_KAK) / KAK_PER_WCAHT} WCAHT including fee.`);
        }
        const prepared = await prepareEvmTransfer(wallet, msg.to, amountKak);
        if (balances.evm < amountKak + prepared.feeKak) {
          throw new Error(
            `Insufficient balance in either account. Native ${balances.native / KAK_PER_WCAHT} WCAHT; ` +
            `EVM ${balances.evm / KAK_PER_WCAHT} WCAHT. Balances cannot be combined into one transfer.`
          );
        }
        const txHash = await buildAndSendEvmTx(wallet, _cachedPassword, msg.to, amountKak, prepared);
        void refreshActionBadge();
        return { ok: true, txHash, fundingSource: 'EVM', fee: prepared.feeKak };
      } catch (e) {
        return { error: e.message };
      }
    }

    // ── dApp connect (from content script) ──────────────────────────────
    case 'DAPP_CONNECT': {
      if (state.locked) return { error: 'Wallet is locked' };
      const origin = sender.origin || sender.url;
      const wallet = getActiveWallet();
      if (!wallet) return { error: 'No wallet' };
      const addressType = wallet.scheme === 'secp256k1' ? 'universal-secp256k1' : 'native-ed25519';
      state.connectedSites[origin] = { publicKey: wallet.publicKey, addressType, evmAddress: wallet.evmAddress || null };
      await saveState();
      return { publicKey: wallet.publicKey, addressType, evmAddress: wallet.evmAddress || null };
    }

    case 'DAPP_DISCONNECT': {
      const origin = sender.origin || sender.url;
      delete state.connectedSites[origin];
      await saveState();
      return { ok: true };
    }

    case 'DAPP_SIGN_TX': {
      if (state.locked) return { error: 'Wallet is locked' };
      const wallet = getActiveWallet();
      if (!wallet) return { error: 'No wallet' };
      try {
        const tx = await buildAndSignTx(wallet, _cachedPassword, msg.to, msg.amount);
        const result = await submitTx(tx);
        return { ok: true, txHash: tx.tx_hash, signature: tx.signature, result };
      } catch (e) {
        return { error: e.message };
      }
    }

    case 'EXPORT_KEY': {
      if (state.locked) return { error: 'Wallet is locked' };
      const w2 = state.wallets.find(w => w.id === msg.walletId);
      if (!w2) return { error: 'Wallet not found' };
      const secretB58 = await decrypt(_cachedPassword, w2.encryptedKey);
      const out = { secretKey: secretB58, scheme: w2.scheme || 'ed25519', evmAddress: w2.evmAddress || null };
      if (w2.scheme === 'secp256k1') out.privateKey = '0x' + bytesToHex(b58decode(secretB58));
      return out;
    }

    case 'ACK_WALLET_BACKUP': {
      if (state.locked) return { error: 'Wallet is locked' };
      const wallet = state.wallets.find(w => w.id === msg.walletId);
      if (!wallet) return { error: 'Wallet not found' };
      wallet.backupRequired = false;
      wallet.backupConfirmedAt = Date.now();
      await saveState();
      return { ok: true };
    }

    case 'SIGN_MESSAGE': {
      if (state.locked) return { error: 'Wallet is locked' };
      const wSign = msg.walletId
        ? state.wallets.find(w => w.id === msg.walletId)
        : getActiveWallet();
      if (!wSign) return { error: 'Wallet not found' };
      try {
        const secretB58 = await decrypt(_cachedPassword, wSign.encryptedKey);
        const msgBytes = new TextEncoder().encode(msg.message);
        const key = resolveWalletKey(secretB58, wSign.scheme);
        if (key.scheme === 'secp256k1') {
          const sig = self.WCAHT_EVM.signPersonalMessage(key.secretKey, msgBytes);
          return { signature: '0x' + bytesToHex(sig), scheme: key.scheme, evmAddress: wSign.evmAddress || key.evmAddress || null };
        }
        const sig = self.nacl.sign.detached(msgBytes, key.secretKey);
        return { signature: bytesToHex(sig), scheme: key.scheme, evmAddress: null };
      } catch (e) {
        return { error: 'Signing failed: ' + e.message };
      }
    }

    case 'GET_TICKER': {
      try {
        const resp = await fetch('https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=100&page=1&sparkline=false&price_change_percentage=24h');
        if (!resp.ok) return { error: 'HTTP ' + resp.status };
        const coins = await resp.json();
        return { coins };
      } catch (e) {
        return { error: e.message };
      }
    }

    default:
      return { error: 'Unknown message type: ' + msg.type };
  }
}

function getActiveWallet() {
  return state.wallets.find(w => w.id === state.activeWalletId) || state.wallets[0] || null;
}

function walletSummary(wallet) {
  if (!wallet) return null;
  return {
    id: wallet.id,
    name: wallet.name,
    publicKey: wallet.publicKey,
    scheme: wallet.scheme || 'ed25519',
    evmAddress: wallet.evmAddress || null,
    backupRequired: !!wallet.backupRequired,
  };
}

function resolveKP(raw) {
  const s = (typeof raw === 'string') ? raw.trim() : null;
  if (s && (s.startsWith('[') || s.startsWith('{'))) return resolveKP(JSON.parse(s));

  if (Array.isArray(raw)) {
    const bytes = new Uint8Array(raw);
    if (bytes.length === 64) return self.nacl.sign.keyPair.fromSecretKey(bytes);
    if (bytes.length === 32) return self.nacl.sign.keyPair.fromSeed(bytes);
    throw new Error('Byte array must be 32 or 64 bytes');
  }

  if (raw && typeof raw === 'object') {
    if (raw.keypair) return resolveKP(raw.keypair);
    if (raw.secret_key_bytes) return resolveKP(raw.secret_key_bytes);
    if (raw.secretKey) return resolveKP(raw.secretKey);
    throw new Error('Unrecognised key format');
  }

  if (s) {
    if (/^[0-9a-fA-F]{128}$/.test(s)) return self.nacl.sign.keyPair.fromSecretKey(hexToBytes(s));
    if (/^[0-9a-fA-F]{64}$/.test(s)) return self.nacl.sign.keyPair.fromSeed(hexToBytes(s));
    const bytes = b58decode(s);
    if (bytes.length === 64) return self.nacl.sign.keyPair.fromSecretKey(bytes);
    if (bytes.length === 32) return self.nacl.sign.keyPair.fromSeed(bytes);
    throw new Error('base58 must decode to 32 or 64 bytes');
  }

  throw new Error('Cannot resolve key');
}

function normalizeKeyString(raw) {
  return (typeof raw === 'string') ? raw.trim() : null;
}

function isExplicitEvmKeyString(s) {
  return !!s && (
    /^0x[0-9a-fA-F]{64}$/.test(s) ||
    /^secp256k1:/i.test(s) ||
    /^evm:/i.test(s)
  );
}

function resolveSecpKey(raw) {
  const s = normalizeKeyString(raw);
  if (s && (s.startsWith('[') || s.startsWith('{'))) return resolveSecpKey(JSON.parse(s));

  if (Array.isArray(raw)) {
    const bytes = new Uint8Array(raw);
    if (bytes.length !== 32) throw new Error('secp256k1 private key must be 32 bytes');
    const acct = self.WCAHT_EVM.privateKeyToAccount(bytes);
    return { scheme: 'secp256k1', publicKey: acct.publicKeyCompressed, secretKey: bytes, evmAddress: acct.evmAddress };
  }

  if (raw && typeof raw === 'object') {
    const inner = raw.evmPrivateKey || raw.evm_private_key || raw.privateKey || raw.private_key ||
      raw.secretKey || raw.secret_key || raw.key;
    if (!inner) throw new Error('Missing EVM private key');
    return resolveSecpKey(inner);
  }

  if (s) {
    let v = s;
    if (/^secp256k1:/i.test(v)) v = v.replace(/^secp256k1:/i, '').trim();
    if (/^evm:/i.test(v)) v = v.replace(/^evm:/i, '').trim();

    let bytes;
    if (/^0x[0-9a-fA-F]{64}$/.test(v)) {
      bytes = hexToBytes(v.slice(2));
    } else if (/^[0-9a-fA-F]{64}$/.test(v)) {
      bytes = hexToBytes(v);
    } else {
      bytes = b58decode(v);
    }

    if (bytes.length !== 32) throw new Error('secp256k1 private key must be 32 bytes');
    const acct = self.WCAHT_EVM.privateKeyToAccount(bytes);
    return { scheme: 'secp256k1', publicKey: acct.publicKeyCompressed, secretKey: bytes, evmAddress: acct.evmAddress };
  }

  throw new Error('Cannot resolve EVM key');
}

function resolveWalletKey(raw, preferredScheme) {
  const s = normalizeKeyString(raw);
  if (s && (s.startsWith('{') || s.startsWith('['))) {
    const parsed = JSON.parse(s);
    const parsedScheme = parsed && !Array.isArray(parsed)
      ? (parsed.scheme || parsed.keyScheme || parsed.type || preferredScheme)
      : preferredScheme;
    return resolveWalletKey(parsed, parsedScheme);
  }
  const scheme = String(preferredScheme || '').toLowerCase();

  if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
    const rawScheme = String(raw.scheme || raw.keyScheme || raw.type || '').toLowerCase();
    if (rawScheme.includes('secp') || rawScheme.includes('evm') || raw.evmPrivateKey || raw.evm_private_key) {
      return resolveSecpKey(raw);
    }
  }

  if (scheme.includes('secp') || scheme.includes('evm') || isExplicitEvmKeyString(s)) {
    return resolveSecpKey(raw);
  }

  const kp = resolveKP(raw);
  return { scheme: 'ed25519', publicKey: kp.publicKey, secretKey: kp.secretKey, evmAddress: null };
}

function compactBadgeBalance(kak) {
  const value = kak / KAK_PER_WCAHT;
  if (value >= 1_000_000) return Math.floor(value / 1_000_000) + 'M';
  if (value >= 1_000) return Math.floor(value / 1_000) + 'K';
  if (value >= 100) return String(Math.floor(value));
  if (value >= 10) return value.toFixed(1).replace(/\.0$/, '');
  return value.toFixed(2).replace(/0+$/, '').replace(/\.$/, '') || '0';
}

async function setActionStatus(text, color, title) {
  await chrome.action.setBadgeText({ text });
  if (text && color) await chrome.action.setBadgeBackgroundColor({ color });
  if (chrome.action.setBadgeTextColor && text) {
    await chrome.action.setBadgeTextColor({ color: '#ffffff' });
  }
  await chrome.action.setTitle({ title });
}

async function refreshActionBadge() {
  try {
    if (state.locked) {
      await setActionStatus('', null, 'WCAHT Wallet - Locked');
      return;
    }
    const wallet = getActiveWallet();
    if (!wallet) {
      await setActionStatus('', null, 'WCAHT Wallet - No wallet');
      return;
    }
    const balances = await getBalanceParts(wallet);
    const native = (balances.native / KAK_PER_WCAHT).toFixed(4);
    const evm = (balances.evm / KAK_PER_WCAHT).toFixed(4);
    const total = (balances.total / KAK_PER_WCAHT).toFixed(4);
    await setActionStatus(
      compactBadgeBalance(balances.total),
      '#08916b',
      `WCAHT Wallet - Total ${total} WCAHT (Native ${native}, EVM ${evm})`
    );
  } catch {
    if (!state.locked && getActiveWallet()) {
      await setActionStatus('OFF', '#d92d20', 'WCAHT Wallet - Network offline');
    }
  }
}

// ── Init ────────────────────────────────────────────────────────────────────
const stateReady = loadState().then(refreshActionBadge);

// Keepalive alarm — fires every ~25s to prevent MV3 service worker from sleeping
chrome.alarms?.create('keepalive', { periodInMinutes: 0.4 });

// Badge update on alarm
chrome.alarms?.create('balance-update', { periodInMinutes: 1 });
chrome.alarms?.onAlarm?.addListener(async (alarm) => {
  if (alarm.name === 'keepalive') return; // just wakes the worker
  if (alarm.name === 'balance-update') await refreshActionBadge();
});

chrome.runtime.onStartup?.addListener(() => void refreshActionBadge());
chrome.runtime.onInstalled?.addListener(() => void refreshActionBadge());
