import browser from 'webextension-polyfill'
import {validateEvent, finalizeEvent, getPublicKey} from 'nostr-tools/pure'
import * as nip19 from 'nostr-tools/nip19'
import * as nip04 from 'nostr-tools/nip04'
import * as nip44 from 'nostr-tools/nip44'
import {hexToBytes} from 'nostr-tools/utils'
import {Mutex} from 'async-mutex'
import {LRUCache} from './utils'

// Crypto operations - all happen in this service worker only
import {
  deriveNewKey,
  deriveKeyFromEncryptedVault,
  encryptWithKey,
  decryptWithKey,
  decryptLegacy,
  isLegacyFormat,
  extractSaltFromBlob
} from './crypto'

// Non-crypto utilities
import {
  NO_PERMISSIONS_REQUIRED,
  getPermissionStatus,
  updatePermission,
  showNotification,
  getPosition
} from './common'

import { clearAllCaches as clearProfileCaches, persistEncryptedCachesWithKey, restoreEncryptedCachesWithKey } from './services/cache'
import { closeDiscoveryPool } from './helpers/outbox'

let openPrompt = null
let promptMutex = new Mutex()
let releasePromptMutex = () => {}
let secretsCache = new LRUCache(100)
let lastUsedAccount = null

// ============================================================================
// SECURITY INVARIANTS
// ============================================================================
//
// INV-1: vaultKey in memory is the SINGLE SOURCE OF CRYPTOGRAPHIC TRUTH.
//        If vaultKey === null, the vault is LOCKED, regardless of any storage flags.
//
// INV-2: UX state ≠ Cryptographic state.
//        Storage flags (uiHintLocked) are HINTS for UI rendering only.
//        They may be stale after service worker restart.
//
// INV-3: Service worker termination = Automatic lock.
//        All in-memory secrets are garbage collected. No mechanism can prevent this.
//        Keep-alive is BEST-EFFORT only.
//
// INV-4: Private keys NEVER leave background memory.
//        UI receives only pubkeys, signed results, or encrypted blobs.
//
// INV-5: sessionStorage contains NO secrets.
//        All decrypted data lives in background memory only.
//
// ============================================================================
// IN-MEMORY STATE - PARTITIONED FOR SECURITY
// ============================================================================
//
// We partition in-memory vault data into two categories:
//
// 1. PRIVATE MATERIAL (High sensitivity)
//    - Private keys, seed phrases, signing material
//    - Wiped AGGRESSIVELY on lock, error, or any security event
//    - NEVER serialized or sent to UI
//
// 2. PUBLIC METADATA (Low sensitivity)
//    - Account names, relay lists, settings
//    - May be cached slightly longer but still memory-only
//    - Can be sent to UI for display
//
// ============================================================================

// CRYPTOGRAPHIC KEY - The single source of unlock truth
let vaultKey = null  // CryptoKey - non-extractable AES-GCM key

// PRIVATE MATERIAL - High sensitivity, wiped aggressively
// Contains: private keys, mnemonic, accountDefault (which is a prvKey)
let privateMaterial = null

// PUBLIC METADATA - Lower sensitivity, still memory-only
// Contains: account indices, settings, relay lists, profile names
let publicMetadata = null

// Note: vaultSalt is NOT stored here. It's extracted from the encrypted blob
// when needed for re-encryption. This minimizes exposure of crypto metadata.

/**
 * Check if the vault is unlocked (key is in memory)
 *
 * THIS IS THE SOLE AUTHORITATIVE SOURCE FOR UNLOCK STATE.
 *
 * Storage flags (uiHintLocked) are UX hints only and may be stale
 * after service worker restart. Always use this function or
 * GET_VAULT_STATUS message to check actual unlock state.
 */
function isVaultUnlocked() {
  return vaultKey !== null
}

/**
 * Clear all in-memory sensitive data
 *
 * Called on:
 * - Explicit lock (user clicks lock)
 * - Auto-lock timeout
 * - Any security-sensitive error
 * - Service worker termination (automatic via GC)
 *
 * SECURITY: privateMaterial is wiped FIRST as it's highest sensitivity
 */
function clearInMemorySecrets() {
  // Clear private material FIRST (highest sensitivity)
  if (privateMaterial) {
    // Attempt to overwrite sensitive fields before nulling
    // (defense in depth - helps with memory inspection attacks)
    if (privateMaterial.accounts) {
      privateMaterial.accounts.forEach(acc => {
        if (acc.prvKey) acc.prvKey = null
      })
    }
    if (privateMaterial.importedAccounts) {
      privateMaterial.importedAccounts.forEach(acc => {
        if (acc.prvKey) acc.prvKey = null
      })
    }
    privateMaterial.mnemonic = null
    privateMaterial.accountDefault = null
  }
  privateMaterial = null

  // Clear public metadata (lower sensitivity but still memory-only)
  publicMetadata = null

  // Clear the cryptographic key last
  vaultKey = null
}

/**
 * Store decrypted vault data in partitioned memory
 * Separates private material from public metadata
 */
function storeDecryptedVault(vaultData) {
  // Extract PRIVATE MATERIAL (high sensitivity)
  privateMaterial = {
    accounts: vaultData.accounts || [],           // Array of {prvKey}
    importedAccounts: vaultData.importedAccounts || [], // Array of {prvKey}
    mnemonic: vaultData.mnemonic,                 // BIP39 mnemonic (if stored)
    accountDefault: vaultData.accountDefault      // Currently selected prvKey
  }

  // Extract PUBLIC METADATA (lower sensitivity)
  publicMetadata = {
    // Account count and types (no private keys)
    accountCount: (vaultData.accounts?.length || 0),
    importedAccountCount: (vaultData.importedAccounts?.length || 0),
    // Any other non-sensitive settings can go here
  }
}

/**
 * Reconstruct full vault from partitioned memory
 * Used when vault needs to be re-encrypted
 */
function getFullVaultFromMemory() {
  if (!privateMaterial) return null

  return {
    accounts: privateMaterial.accounts,
    importedAccounts: privateMaterial.importedAccounts,
    mnemonic: privateMaterial.mnemonic,
    accountDefault: privateMaterial.accountDefault
  }
}

// Legacy accessor for backward compatibility
// TODO: Migrate all code to use partitioned access
function getDecryptedVault() {
  return getFullVaultFromMemory()
}

// Auto-lock timeout (default 5 minutes in milliseconds, converted to minutes for alarms API)
const DEFAULT_LOCK_TIMEOUT = 5 * 60 * 1000
const LOCK_ALARM_NAME = 'autoLockAlarm'

// ============================================================================
// KEEP-ALIVE MECHANISM - BEST EFFORT ONLY
// ============================================================================
//
// IMPORTANT: Keep-alive is a UX OPTIMIZATION, not a security guarantee.
//
// How it works:
// - A periodic alarm fires every ~24 seconds while vault is unlocked
// - This prevents Chrome from terminating the idle service worker
// - If successful, user doesn't need to re-enter password frequently
//
// CRITICAL LIMITATIONS:
// - Chrome may STILL terminate the worker at any time (memory pressure, updates, etc.)
// - Keep-alive alarms may fail to fire (browser bugs, system sleep, etc.)
// - There is NO mechanism that can guarantee the worker stays alive
//
// SECURITY IMPLICATIONS:
// - Service worker termination at ANY TIME must be treated as vault lock
// - UI must ALWAYS call GET_VAULT_STATUS on focus/reload to verify state
// - UI must NEVER assume the vault is unlocked based on previous state
// - Storage flags (uiHintLocked) may be stale after unexpected termination
//
// ============================================================================
const KEEPALIVE_ALARM_NAME = 'keepAliveAlarm'
const KEEPALIVE_INTERVAL_MINUTES = 0.4  // ~24 seconds (under Chrome's 30s idle limit)

/**
 * Start the keep-alive alarm to prevent service worker termination.
 * Called when vault is unlocked.
 *
 * BEST EFFORT ONLY - This is a UX optimization, not a security guarantee.
 * The service worker may still be terminated by Chrome at any time.
 * All security logic must assume termination can happen unexpectedly.
 */
function startKeepAlive() {
  chrome.alarms.create(KEEPALIVE_ALARM_NAME, {
    periodInMinutes: KEEPALIVE_INTERVAL_MINUTES
  })
}

/**
 * Stop the keep-alive alarm.
 * Called when vault is locked (explicit or auto-lock).
 * Once stopped, the service worker can terminate normally,
 * which will garbage collect all in-memory secrets.
 */
function stopKeepAlive() {
  chrome.alarms.clear(KEEPALIVE_ALARM_NAME)
}

// Track active UI connections (popup/options pages)
let activeConnections = new Set()

async function lockVault() {
  const { isAuthenticated, encryptedVault } = await browser.storage.local.get(['isAuthenticated', 'encryptedVault'])
  if (!isAuthenticated) return // Don't lock if not authenticated

  // Stop keep-alive - allow service worker to terminate after lock
  stopKeepAlive()

  // Persist encrypted caches before clearing key (if key is still in memory)
  if (isVaultUnlocked() && encryptedVault) {
    // Extract salt from existing blob - don't store it in memory
    const salt = extractSaltFromBlob(encryptedVault)
    await persistEncryptedCachesWithKey(vaultKey, salt).catch(err => {
      console.warn('Failed to persist caches:', err)
    })
  }

  // CRITICAL: Clear ALL in-memory secrets
  // Order matters: privateMaterial first, then publicMetadata, then vaultKey
  clearInMemorySecrets()

  // Update UX hint flag
  // NOTE: This is a UI HINT ONLY, not a security flag.
  // The authoritative lock state is vaultKey === null.
  // This flag may be stale after service worker restart.
  await browser.storage.local.set({ uiHintLocked: true })

  clearAllCaches()
}

async function resetLockTimer() {
  // Clear any existing alarm
  await chrome.alarms.clear(LOCK_ALARM_NAME)

  // Don't start timer if UI is actively open (popup or options page)
  if (activeConnections.size > 0) return

  // Check if vault is unlocked using AUTHORITATIVE source (in-memory key)
  // Storage flags are hints only and may be stale
  if (!isVaultUnlocked()) return

  const { isAuthenticated } = await browser.storage.local.get(['isAuthenticated'])
  if (!isAuthenticated) return

  // Get timeout setting (default 5 minutes)
  const { autoLockTimeout = DEFAULT_LOCK_TIMEOUT } = await browser.storage.local.get(['autoLockTimeout'])
  if (autoLockTimeout <= 0) return // Disabled if 0 or negative

  // Convert milliseconds to minutes for alarms API (minimum 0.5 minutes = 30 seconds in dev mode)
  const delayInMinutes = Math.max(autoLockTimeout / 60000, 0.5)
  await chrome.alarms.create(LOCK_ALARM_NAME, { delayInMinutes })
}

// Handle UI connections (popup/options) - pause timer while connected
browser.runtime.onConnect.addListener((port) => {
  if (port.name === 'ui-active') {
    activeConnections.add(port)
    // Pause the timer while UI is open
    chrome.alarms.clear(LOCK_ALARM_NAME)

    port.onDisconnect.addListener(() => {
      activeConnections.delete(port)
      // Resume timer when all UI connections are closed
      if (activeConnections.size === 0) {
        resetLockTimer()
      }
    })
  }
})

// Listen for alarm to trigger lock
chrome.alarms.onAlarm.addListener((alarm) => {
  if (alarm.name === LOCK_ALARM_NAME) {
    lockVault()
  } else if (alarm.name === KEEPALIVE_ALARM_NAME) {
    // Keep-alive: This handler firing is enough to keep the service worker alive.
    // No action needed - just receiving the alarm prevents termination.
    // The in-memory vaultKey remains intact.
  }
})

/**
 * Clear all caches on vault lock or account change
 * This is a SECURITY-CRITICAL function - ensures no sensitive data remains accessible
 */
function clearAllCaches() {
  secretsCache.clear()
  lastUsedAccount = null
  // Clear profile and relay caches from session storage
  clearProfileCaches()
  // Close discovery pool WebSocket connections
  // This prevents stale connections and ensures clean state
  closeDiscoveryPool()
}

function getSharedSecret(sk, peer) {
  // Clear cache if account changed
  if (lastUsedAccount !== sk) {
    secretsCache.clear()
    lastUsedAccount = sk
  }

  let key = secretsCache.get(peer)

  if (!key) {
    key = nip44.v2.utils.getConversationKey(hexToBytes(sk), peer)
    secretsCache.set(peer, key)
  }

  return key
}

/**
 * Get the current active account's private key from in-memory PRIVATE MATERIAL
 * Returns null if vault is locked or no default account set
 *
 * SECURITY: This accesses privateMaterial which contains sensitive keys.
 * The returned value must NEVER be sent to UI or stored outside this worker.
 */
function getCurrentAccount() {
  if (!privateMaterial || !privateMaterial.accountDefault) {
    return null
  }
  return privateMaterial.accountDefault
}

/**
 * Find a private key in the vault that matches a given public key.
 * Searches both derived and imported accounts.
 * Returns the private key (hex) or null if not found.
 *
 * SECURITY: The returned value must NEVER be sent to UI or stored outside this worker.
 */
function findAccountByPubkey(targetPubkey) {
  if (!privateMaterial || !targetPubkey) return null

  const allAccounts = [
    ...(privateMaterial.accounts || []),
    ...(privateMaterial.importedAccounts || [])
  ]

  for (const account of allAccounts) {
    try {
      if (getPublicKey(hexToBytes(account.prvKey)) === targetPubkey) {
        return account.prvKey
      }
    } catch (e) { /* skip invalid keys */ }
  }

  return null
}

const width = 400
const height = 600

browser.runtime.onInstalled.addListener(async (_, __, reason) => {
  if (reason === 'install') browser.runtime.openOptionsPage()

  // Set default auto-lock timeout (only if not already configured)
  const { autoLockTimeout } = await browser.storage.local.get(['autoLockTimeout'])
  if (autoLockTimeout === undefined) {
    await browser.storage.local.set({
      "autoLockTimeout": DEFAULT_LOCK_TIMEOUT
    })
  }

  // Cleanup: remove unencrypted vault and legacy relays from local storage
  await browser.storage.local.remove(['vault', 'relays'])
})

// Start lock timer on extension startup
resetLockTimer()

browser.runtime.onMessage.addListener(async (message, sender) => {
  // Reset lock timer on any message (user activity)
  resetLockTimer()

  // Handle vault operations (UI -> background)
  switch (message.type) {
    // ========================================================================
    // VAULT STATUS & LOCK OPERATIONS
    // ========================================================================
    case 'UNLOCK_VAULT':
      return handleUnlockVault(message.password)
    case 'LOCK_VAULT':
      await lockVault()
      return { success: true }
    case 'GET_VAULT_STATUS': {
      // Authoritative unlock check - this is the ONLY reliable way to check
      const { encryptedVault, isAuthenticated } = await browser.storage.local.get(['encryptedVault', 'isAuthenticated'])
      const unlocked = isVaultUnlocked()

      // Sync storage hint flag if desync detected (worker restarted)
      // NOTE: uiHintLocked is a UX hint ONLY, not authoritative state
      if (isAuthenticated && !unlocked) {
        await browser.storage.local.set({ uiHintLocked: true })
      }

      return {
        unlocked,
        hasVault: !!encryptedVault,
        isAuthenticated: !!isAuthenticated
      }
    }
    // Legacy alias - TODO: remove after migration
    case 'GET_LOCK_STATUS':
      return { unlocked: isVaultUnlocked() }

    // ========================================================================
    // VAULT DATA ACCESS (UI requests data, background returns public info only)
    // Private keys NEVER leave background memory
    // ========================================================================
    case 'GET_VAULT_DATA':
      return handleGetVaultData()
    case 'GET_ACCOUNTS_LIST':
      return handleGetAccountsList()
    case 'UPDATE_VAULT':
      return handleUpdateVault(message.vault)
    case 'GET_SESSION_VAULT':
      // Returns full vault from memory (replaces getSessionVault())
      // SECURITY: Vault is in memory only, NOT in session storage
      // This is for UI components that need to modify vault structure
      if (!isVaultUnlocked() || !privateMaterial) {
        return null
      }
      return getFullVaultFromMemory()
    case 'SET_SESSION_VAULT':
      // Updates vault in memory and encrypts to storage (replaces setSessionVault())
      // SECURITY: Vault is stored in memory only, NOT in session storage
      return handleUpdateVault(message.vault)

    // ========================================================================
    // VAULT MANAGEMENT
    // ========================================================================
    case 'ENCRYPT_VAULT':
      return handleEncryptVault(message.data)
    case 'CREATE_NEW_VAULT':
      return handleCreateNewVault(message.password, message.vaultData)
    case 'CHANGE_PASSWORD':
      return handleChangePassword(message.oldPassword, message.newPassword)
    case 'IMPORT_VAULT_BACKUP':
      return handleImportVaultBackup(message.encryptedVault, message.password)
    case 'VERIFY_PASSWORD':
      return handleVerifyPassword(message.password)
    // Legacy alias - TODO: remove after migration
    case 'DECRYPT_VAULT_WITH_PASSWORD':
      return handleDecryptVaultWithPassword(message.password)

    // ========================================================================
    // HIGH-SENSITIVITY SECRET EXPORT (Requires password re-verification)
    // ========================================================================
    case 'REQUEST_SECRET_FOR_DISPLAY':
      return handleRequestSecretForDisplay(message.password, message.secretType, message.accountIndex)

    // ========================================================================
    // SETTINGS
    // ========================================================================
    case 'AUTO_LOCK_TIMEOUT_CHANGED':
      resetLockTimer()
      return { success: true }
  }

  if (message.openSignUp) {
    openSignUpWindow()
    browser.windows.remove(sender.tab.windowId)
  } else {
    let {prompt} = message
    if (prompt) {
      handlePromptMessage(message, sender)
    } else {
      return handleContentScriptMessage(message)
    }
  }
})

// ============================================================================
// VAULT OPERATION HANDLERS - All crypto operations happen here
// ============================================================================

/**
 * Handle vault unlock - derives key, stores in memory, decrypts vault
 *
 * SECURITY: Decrypted vault is stored ONLY in background memory (decryptedVault).
 * sessionStorage contains NO secrets - UI accesses data via messages.
 * Salt is NOT stored in memory after derivation.
 *
 * @param {string} password - User's password
 * @returns {Promise<{success: boolean, error?: string}>}
 */
async function handleUnlockVault(password) {
  try {
    const { encryptedVault } = await browser.storage.local.get(['encryptedVault'])
    if (!encryptedVault) {
      return { success: false, error: 'No vault found' }
    }

    let vaultData
    let migrated = false

    // Check if legacy format - need migration
    if (isLegacyFormat(encryptedVault)) {
      // Decrypt with legacy format (10k iterations, AES-CBC)
      vaultData = await decryptLegacy(encryptedVault, password)

      // Generate new key with strong parameters for future use
      // Salt is used only here, then discarded (not stored in memory)
      const { key, salt } = await deriveNewKey(password)
      vaultKey = key

      // Re-encrypt with new format immediately
      const newEncryptedVault = await encryptWithKey(vaultData, vaultKey, salt)
      await browser.storage.local.set({ encryptedVault: newEncryptedVault })
      // salt goes out of scope here - automatically garbage collected

      migrated = true
    } else {
      // v2 format - derive key from existing salt (salt is in the blob)
      // We only keep the key, salt is extracted and discarded
      const { key } = await deriveKeyFromEncryptedVault(password, encryptedVault)
      vaultKey = key

      // Decrypt vault using already-derived key
      vaultData = await decryptWithKey(encryptedVault, vaultKey)
    }

    // Store decrypted vault in partitioned MEMORY ONLY (not session storage!)
    storeDecryptedVault(vaultData)

    // Update storage hint flag (UX hint only - not authoritative)
    await browser.storage.local.set({ uiHintLocked: false })

    // Restore encrypted caches from local storage
    await restoreEncryptedCachesWithKey(vaultKey).catch(err => {
      console.warn('Failed to restore caches:', err)
    })

    // Start keep-alive to maintain session
    startKeepAlive()

    return { success: true, migrated }
  } catch (err) {
    // Clear any partial state on failure
    clearInMemorySecrets()
    console.error('Unlock vault error:', err)
    return { success: false, error: 'Invalid password' }
  }
}

/**
 * Handle vault encryption - encrypts data using in-memory key
 * Salt is extracted from existing encrypted vault (not stored in memory)
 *
 * @param {any} data - Data to encrypt
 * @returns {Promise<{success: boolean, encryptedData?: string, error?: string}>}
 */
async function handleEncryptVault(data) {
  if (!isVaultUnlocked()) {
    return { success: false, error: 'Vault is locked' }
  }

  try {
    // Extract salt from existing encrypted vault (not stored in memory)
    const { encryptedVault } = await browser.storage.local.get(['encryptedVault'])
    if (!encryptedVault || isLegacyFormat(encryptedVault)) {
      return { success: false, error: 'No valid encrypted vault found' }
    }

    const salt = extractSaltFromBlob(encryptedVault)
    const encryptedData = await encryptWithKey(data, vaultKey, salt)

    // Also update in-memory vault (partitioned)
    storeDecryptedVault(data)

    return { success: true, encryptedData }
  } catch (err) {
    return { success: false, error: err.message }
  }
}

/**
 * Handle new vault creation - generates new key and encrypts vault
 * Salt is used for encryption then discarded (not stored in memory)
 *
 * @param {string} password - User's password
 * @param {any} vaultData - Initial vault data
 * @returns {Promise<{success: boolean, encryptedVault?: string, error?: string}>}
 */
async function handleCreateNewVault(password, vaultData) {
  try {
    // Generate new key with fresh salt
    // Salt is used only here, then discarded
    const { key, salt } = await deriveNewKey(password)
    vaultKey = key

    // Encrypt the vault data
    const encryptedVault = await encryptWithKey(vaultData, vaultKey, salt)
    // salt goes out of scope here

    // Store decrypted vault in partitioned MEMORY ONLY
    storeDecryptedVault(vaultData)

    // Start keep-alive
    startKeepAlive()

    return { success: true, encryptedVault }
  } catch (err) {
    clearInMemorySecrets()
    return { success: false, error: err.message }
  }
}

/**
 * Handle password change - verifies old password, re-encrypts with new password
 * Salt is used for encryption then discarded
 *
 * @param {string} oldPassword - Current password
 * @param {string} newPassword - New password
 * @returns {Promise<{success: boolean, encryptedVault?: string, error?: string}>}
 */
async function handleChangePassword(oldPassword, newPassword) {
  try {
    const { encryptedVault } = await browser.storage.local.get(['encryptedVault'])
    if (!encryptedVault) {
      return { success: false, error: 'No vault found' }
    }

    // Verify old password by decrypting
    let vaultData
    if (isLegacyFormat(encryptedVault)) {
      vaultData = await decryptLegacy(encryptedVault, oldPassword)
    } else {
      const { key } = await deriveKeyFromEncryptedVault(oldPassword, encryptedVault)
      vaultData = await decryptWithKey(encryptedVault, key)
    }

    // Generate new key with new password
    // Salt is used only here, then discarded
    const { key, salt } = await deriveNewKey(newPassword)

    // Update in-memory key
    vaultKey = key

    // Re-encrypt vault with new key
    const newEncryptedVault = await encryptWithKey(vaultData, vaultKey, salt)

    // Re-encrypt caches with new key (using the new salt)
    await persistEncryptedCachesWithKey(vaultKey, salt).catch(() => {})
    // salt goes out of scope here

    // Update in-memory vault (partitioned)
    storeDecryptedVault(vaultData)

    return { success: true, encryptedVault: newEncryptedVault }
  } catch (err) {
    return { success: false, error: 'Current password is incorrect' }
  }
}

/**
 * Handle vault backup import - decrypts backup, stores key in memory
 * Salt is used for encryption then discarded (not stored in memory)
 *
 * SECURITY: Returns ONLY public data (account count, etc.)
 * Private keys NEVER leave this function.
 *
 * @param {string} encryptedVault - Encrypted vault from backup file
 * @param {string} password - Password to decrypt the backup
 * @returns {Promise<{success: boolean, encryptedVault?: string, accountCount?: number, error?: string}>}
 */
async function handleImportVaultBackup(encryptedVault, password) {
  try {
    let vaultData
    let finalEncryptedVault = encryptedVault
    let migrated = false

    // Check if legacy format
    if (isLegacyFormat(encryptedVault)) {
      // Decrypt with legacy format (10k iterations, AES-CBC)
      vaultData = await decryptLegacy(encryptedVault, password)

      // Generate new key for storage
      // Salt is used only here, then discarded
      const { key, salt } = await deriveNewKey(password)
      vaultKey = key

      // Re-encrypt with new format
      finalEncryptedVault = await encryptWithKey(vaultData, vaultKey, salt)
      // salt goes out of scope here

      migrated = true
    } else {
      // v2 format - derive key from existing salt
      // We only keep the key, salt is extracted and discarded
      const { key } = await deriveKeyFromEncryptedVault(password, encryptedVault)
      vaultKey = key

      // Decrypt vault using already-derived key
      vaultData = await decryptWithKey(encryptedVault, vaultKey)
    }

    // Store decrypted vault in partitioned MEMORY ONLY
    storeDecryptedVault(vaultData)

    // Start keep-alive
    startKeepAlive()

    // Return only public info - NO private keys
    const accountCount = (vaultData.accounts?.length || 0) + (vaultData.importedAccounts?.length || 0)

    return {
      success: true,
      encryptedVault: finalEncryptedVault,
      accountCount,
      migrated
    }
  } catch (err) {
    clearInMemorySecrets()
    console.error('Import vault backup error:', err)
    return { success: false, error: 'Invalid vault file or wrong password' }
  }
}

/**
 * Handle decrypt vault with password - verifies password without changing state
 * Used for viewing secrets (requires password re-entry for security)
 *
 * WARNING: This returns decrypted vault data including private keys.
 * Only use for explicit "show secret" functionality with password re-verification.
 *
 * @param {string} password - Password to verify
 * @returns {Promise<{success: boolean, vaultData?: any, error?: string}>}
 * @deprecated Use VERIFY_PASSWORD + in-memory vault access instead
 */
async function handleDecryptVaultWithPassword(password) {
  try {
    const { encryptedVault } = await browser.storage.local.get(['encryptedVault'])
    if (!encryptedVault) {
      return { success: false, error: 'No vault found' }
    }

    // Decrypt vault to verify password
    let vaultData
    if (isLegacyFormat(encryptedVault)) {
      vaultData = await decryptLegacy(encryptedVault, password)
    } else {
      const { key } = await deriveKeyFromEncryptedVault(password, encryptedVault)
      vaultData = await decryptWithKey(encryptedVault, key)
    }

    return { success: true, vaultData }
  } catch (err) {
    return { success: false, error: 'Invalid password' }
  }
}

/**
 * Handle request for secret display - REQUIRES password re-verification
 *
 * SECURITY: This is a HIGH-SENSITIVITY operation.
 * - Requires password re-entry even if vault is unlocked
 * - Only returns ONE secret at a time
 * - Used for explicit "show private key" or "show mnemonic" UI actions
 *
 * @param {string} password - Password for verification
 * @param {string} secretType - Type of secret: 'privateKey' or 'mnemonic'
 * @param {number} accountIndex - For privateKey, index of account (derived accounts) or null for current
 * @returns {Promise<{success: boolean, secret?: string, error?: string}>}
 */
async function handleRequestSecretForDisplay(password, secretType, accountIndex) {
  // Step 1: Verify password (always required, even if unlocked)
  try {
    const { encryptedVault } = await browser.storage.local.get(['encryptedVault'])
    if (!encryptedVault) {
      return { success: false, error: 'No vault found' }
    }

    // Verify password by attempting decryption
    let vaultData
    if (isLegacyFormat(encryptedVault)) {
      vaultData = await decryptLegacy(encryptedVault, password)
    } else {
      const { key } = await deriveKeyFromEncryptedVault(password, encryptedVault)
      vaultData = await decryptWithKey(encryptedVault, key)
    }

    // Step 2: Extract the requested secret
    switch (secretType) {
      case 'mnemonic': {
        // Return the BIP39 mnemonic (seed phrase)
        if (!vaultData.mnemonic) {
          return { success: false, error: 'No mnemonic found in vault' }
        }
        return { success: true, secret: vaultData.mnemonic }
      }
      case 'privateKey': {
        // Return private key for a specific account
        // If accountIndex is null/undefined, use current default account
        let prvKey

        if (accountIndex === null || accountIndex === undefined) {
          // Get current default account's private key
          prvKey = vaultData.accountDefault
          if (!prvKey) {
            return { success: false, error: 'No default account set' }
          }
        } else {
          // Get account at specified index
          // accountIndex format: "derived:N" or "imported:N" or just N (assumes derived)
          let type = 'derived'
          let idx = accountIndex

          if (typeof accountIndex === 'string') {
            if (accountIndex.startsWith('imported:')) {
              type = 'imported'
              idx = parseInt(accountIndex.split(':')[1], 10)
            } else if (accountIndex.startsWith('derived:')) {
              idx = parseInt(accountIndex.split(':')[1], 10)
            } else {
              idx = parseInt(accountIndex, 10)
            }
          }

          const accounts = type === 'imported' ? vaultData.importedAccounts : vaultData.accounts
          if (!accounts || !accounts[idx]) {
            return { success: false, error: 'Account not found' }
          }
          prvKey = accounts[idx].prvKey
        }

        if (!prvKey) {
          return { success: false, error: 'Private key not found' }
        }

        return { success: true, secret: prvKey }
      }
      default:
        return { success: false, error: `Unknown secret type: ${secretType}` }
    }
  } catch (err) {
    // Password verification failed
    return { success: false, error: 'Invalid password' }
  }
}

/**
 * Handle password verification - checks if password is correct without returning secrets
 * Used for settings operations that require re-authentication
 *
 * @param {string} password - Password to verify
 * @returns {Promise<{success: boolean, valid?: boolean, error?: string}>}
 */
async function handleVerifyPassword(password) {
  try {
    const { encryptedVault } = await browser.storage.local.get(['encryptedVault'])
    if (!encryptedVault) {
      return { success: false, error: 'No vault found' }
    }

    // Try to decrypt - if it works, password is correct
    if (isLegacyFormat(encryptedVault)) {
      await decryptLegacy(encryptedVault, password)
    } else {
      const { key } = await deriveKeyFromEncryptedVault(password, encryptedVault)
      await decryptWithKey(encryptedVault, key)
    }

    return { success: true, valid: true }
  } catch (err) {
    return { success: true, valid: false }
  }
}

/**
 * Get vault data for UI display
 * Returns public info and account indices - NO private keys
 *
 * SECURITY: This function NEVER returns private keys.
 * Private keys stay in background memory only.
 *
 * @returns {Promise<{success: boolean, vault?: object, error?: string}>}
 */
function handleGetVaultData() {
  if (!isVaultUnlocked() || !privateMaterial) {
    return { success: false, error: 'Vault is locked' }
  }

  // Return vault structure without private keys
  // UI can use this to understand account structure
  const safeVault = {
    accountDefault: privateMaterial.accountDefault,
    // Return account indices, NOT private keys
    accounts: (privateMaterial.accounts || []).map((acc, index) => ({
      index,
      // prvKey is intentionally NOT included
    })),
    importedAccounts: (privateMaterial.importedAccounts || []).map((acc, index) => ({
      index,
      // prvKey is intentionally NOT included
    })),
    mnemonic: undefined // NEVER expose mnemonic
  }

  return { success: true, vault: safeVault }
}

/**
 * Get list of accounts with public info only
 * Used by UI to display account list
 *
 * SECURITY: This function NEVER returns private keys.
 *
 * @returns {Promise<{success: boolean, accounts?: array, error?: string}>}
 */
function handleGetAccountsList() {
  if (!isVaultUnlocked() || !privateMaterial) {
    return { success: false, error: 'Vault is locked' }
  }

  const accounts = []

  // Process derived accounts
  const derivedAccounts = privateMaterial.accounts || []
  for (let i = 0; i < derivedAccounts.length; i++) {
    const prvKey = derivedAccounts[i].prvKey
    const pubKey = getPublicKey(hexToBytes(prvKey))
    accounts.push({
      index: i,
      type: 'derived',
      pubKey,
      // prvKey is intentionally NOT included
      isDefault: privateMaterial.accountDefault === prvKey
    })
  }

  // Process imported accounts
  const importedAccounts = privateMaterial.importedAccounts || []
  for (let i = 0; i < importedAccounts.length; i++) {
    const prvKey = importedAccounts[i].prvKey
    const pubKey = getPublicKey(hexToBytes(prvKey))
    accounts.push({
      index: i,
      type: 'imported',
      pubKey,
      // prvKey is intentionally NOT included
      isDefault: privateMaterial.accountDefault === prvKey
    })
  }

  return { success: true, accounts }
}

/**
 * Update vault with new data
 * Encrypts and persists to storage, updates in-memory vault
 *
 * @param {object} newVault - Updated vault data (includes private keys for storage)
 * @returns {Promise<{success: boolean, error?: string}>}
 */
async function handleUpdateVault(newVault) {
  if (!isVaultUnlocked()) {
    return { success: false, error: 'Vault is locked' }
  }

  try {
    // Extract salt from existing encrypted vault
    const { encryptedVault } = await browser.storage.local.get(['encryptedVault'])
    if (!encryptedVault || isLegacyFormat(encryptedVault)) {
      return { success: false, error: 'No valid encrypted vault found' }
    }

    const salt = extractSaltFromBlob(encryptedVault)

    // Encrypt and save
    const newEncryptedVault = await encryptWithKey(newVault, vaultKey, salt)
    await browser.storage.local.set({ encryptedVault: newEncryptedVault })

    // Update in-memory vault (partitioned)
    storeDecryptedVault(newVault)

    return { success: true }
  } catch (err) {
    console.error('Update vault error:', err)
    return { success: false, error: err.message }
  }
}

browser.runtime.onMessageExternal.addListener(
  async ({type, params}, sender) => {
    // Reset lock timer on external message (user activity)
    resetLockTimer()

    let extensionId = new URL(sender.url).host
    return handleContentScriptMessage({type, params, host: extensionId})
  }
)

browser.windows.onRemoved.addListener(_ => {
  if (openPrompt) {
    // calling this with a simple "no" response will not store anything, so it's fine
    // it will just return a failure
    handlePromptMessage({accept: false}, null)
  }
})

browser.storage.onChanged.addListener((changes, area) => {
  // Handle UX hint flag changes (hint flags in localStorage)
  // NOTE: These are UX hints only, not authoritative lock state
  if (changes.uiHintLocked?.newValue === true) {
    clearAllCaches()
    chrome.alarms.clear(LOCK_ALARM_NAME)
  }
  // Reset timer when vault is unlocked (UX hint)
  if (changes.uiHintLocked?.newValue === false) {
    resetLockTimer()
  }
})

// Note: We no longer listen for session storage changes because
// decrypted vault is now stored ONLY in background memory, not session storage

async function handleContentScriptMessage({type, params, host, favicon}) {
  if (NO_PERMISSIONS_REQUIRED[type]) {
    // authorized, and we won't do anything with private key here, so do a separate handler
    switch (type) {
      case 'replaceURL': {
        let {protocol_handler: ph} = await browser.storage.local.get([
          'protocol_handler'
        ])
        if (!ph) return false

        let {url} = params
        let raw = url.split('nostr:')[1]
        let {type, data} = nip19.decode(raw)
        let replacements = {
          raw,
          hrp: type,
          hex:
            type === 'npub' || type === 'note'
              ? data
              : type === 'nprofile'
              ? data.pubkey
              : type === 'nevent'
              ? data.id
              : null,
          p_or_e: {npub: 'p', note: 'e', nprofile: 'p', nevent: 'e'}[type],
          u_or_n: {npub: 'u', note: 'n', nprofile: 'u', nevent: 'n'}[type],
          relay0: type === 'nprofile' ? data.relays[0] : null,
          relay1: type === 'nprofile' ? data.relays[1] : null,
          relay2: type === 'nprofile' ? data.relays[2] : null
        }
        let result = ph
        const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
        Object.entries(replacements).forEach(([pattern, value]) => {
          result = result.replace(new RegExp(`\\{ *${escapeRegex(pattern)} *\\}`, 'g'), value)
        })

        return result
      }
    }

    return
  } else {
    // For signEvent, if the event has a pubkey, verify we own that key
    // This ensures we sign with the correct account regardless of the default
    const requestedPubkey = (type === 'signEvent' && params?.event?.pubkey) ? params.event.pubkey : null

    if (requestedPubkey && isVaultUnlocked()) {
      const match = findAccountByPubkey(requestedPubkey)
      if (!match) {
        // Extension doesn't have this key - reject without prompt
        return {
          error: {message: `no account found for pubkey ${requestedPubkey.slice(0, 8)}...`}
        }
      }
    }

    // acquire mutex here before reading policies
    releasePromptMutex = await promptMutex.acquire()

    // Check if vault is locked (key not in memory) - if so, we must show prompt for unlock
    const isVaultLocked = !isVaultUnlocked()

    let allowed = await getPermissionStatus(
      host,
      type,
      type === 'signEvent' ? params.event : undefined
    )

    // If vault is locked, always show prompt for unlock (even if permission granted)
    const needsPrompt = isVaultLocked || allowed === undefined

    if (allowed === false) {
      // denied, just refuse immediately
      releasePromptMutex()
      showNotification(host, allowed, type, params)
      return {
        error: {message: 'denied'}
      }
    } else if (needsPrompt) {
      // vault locked OR permission not yet granted - show prompt
      try {
        const array = new Uint32Array(2)
        crypto.getRandomValues(array)
        let id = Array.from(array, x => x.toString(16)).join('')

        // Get the account that will be used for signing
        // If event has a pubkey, use that account; otherwise use default
        const currentAccount = await getCurrentAccount()
        const currentPubkey = requestedPubkey || (currentAccount ? getPublicKey(hexToBytes(currentAccount)) : null)

        // Get account name and picture from profile cache if available
        let accountName = ''
        let accountPicture = ''
        if (currentPubkey) {
          try {
            const { getCachedProfile } = await import('./services/cache.js')
            const cached = await getCachedProfile(currentPubkey)
            if (cached?.data) {
              accountName = cached.data.name || cached.data.display_name || ''
              accountPicture = cached.data.picture || ''
            }
          } catch (e) { /* cache unavailable */ }
        }

        let qs = new URLSearchParams({
          host,
          id,
          params: JSON.stringify(params),
          type,
          pubkey: currentPubkey || '',
          accountName,
          accountPicture,
          favicon: favicon || '',
          // If permission already granted, just need unlock (no permission UI needed)
          unlockOnly: allowed === true ? 'true' : ''
        })
        // center prompt
        const {top, left} = await getPosition(width, height)
        // prompt will be resolved with true or false
        let accept = await new Promise((resolve, reject) => {
          openPrompt = {resolve, reject}

          browser.windows.create({
            url: `${browser.runtime.getURL('prompt.html')}?${qs.toString()}`,
            type: 'popup',
            width: width,
            height: height,
            top: top,
            left: left
          })
        })

        // denied, stop here
        if (!accept) return {error: {message: 'denied'}}
      } catch (err) {
        // errored, stop here
        releasePromptMutex()
        return {
          error: {message: err.message, stack: err.stack}
        }
      }
    } else {
      // authorized and unlocked, proceed without prompt
      releasePromptMutex()
      showNotification(host, allowed, type, params)
    }
  }

  // if we're here this means it was accepted
  // Check AUTHORITATIVE unlock state (in-memory key), not UX hint flag
  if (!isVaultUnlocked()) {
    return {error: {message: 'vault is locked, please unlock it first'} }
  }

  // ALWAYS get fresh account - never use cached value
  const activeAccount = await getCurrentAccount()

  if (!activeAccount) {
    return {error: {message: 'no private key found'} }
  }

  try {
    switch (type) {
      case 'getPublicKey': {
        return getPublicKey(hexToBytes(activeAccount))
      }
      case 'signEvent': {
        // Use the account matching event.pubkey if available, otherwise default
        let signingKey = activeAccount
        const eventPubkey = params?.event?.pubkey
        if (eventPubkey) {
          const matched = findAccountByPubkey(eventPubkey)
          if (matched) {
            signingKey = matched
          } else {
            // Event requests a pubkey we don't own - reject
            return {error: {message: `no account found for pubkey ${eventPubkey.slice(0, 8)}...`}}
          }
        }

        const event = finalizeEvent(params.event, hexToBytes(signingKey))

        return validateEvent(event)
          ? event
          : {error: {message: 'invalid event'}}
      }
      case 'nip04.encrypt': {
        let {peer, plaintext} = params
        return nip04.encrypt(hexToBytes(activeAccount), peer, plaintext)
      }
      case 'nip04.decrypt': {
        let {peer, ciphertext} = params
        return nip04.decrypt(hexToBytes(activeAccount), peer, ciphertext)
      }
      case 'nip44.encrypt': {
        const {peer, plaintext} = params
        const key = getSharedSecret(activeAccount, peer)

        return nip44.v2.encrypt(plaintext, key)
      }
      case 'nip44.decrypt': {
        const {peer, ciphertext} = params
        const key = getSharedSecret(activeAccount, peer)

        return nip44.v2.decrypt(ciphertext, key)
      }
    }
  } catch (error) {
    return {error: {message: error.message, stack: error.stack}}
  }
}

async function handlePromptMessage({host, type, accept, conditions}, sender) {
  // return response
  openPrompt?.resolve?.(accept)

  // Only store permission if conditions is explicitly set (not null/undefined)
  // "authorize just this" passes null, "authorize forever" passes {remember: 'forever'}
  if (conditions !== null && conditions !== undefined && typeof conditions === 'object') {
    await updatePermission(host, type, accept, conditions)
  }

  // cleanup this
  openPrompt = null

  // release mutex here after updating policies
  releasePromptMutex()

  // close prompt
  if (sender) {
    browser.windows.remove(sender.tab.windowId)
  }
}

async function openSignUpWindow() {
  const {top, left} = await getPosition(width, height)

  browser.windows.create({
    url: `${browser.runtime.getURL('signup.html')}`,
    type: 'popup',
    width: width,
    height: height,
    top: top,
    left: left
  })
}
